FN-7051: restore verify:fast prerequisite builds

Restore verify:fast so fresh worktrees prepare required artifacts before boot smoke.\n\n- Bootstrap missing or stale workspace dist artifacts before scoped verification.\n- Always build @runfusion/fusion for source-checkout boot smoke, even when no package diff maps to the CLI.\n- Update verify:fast tests and docs to capture the prerequisite build flow.\n\nFiles changed:\n AGENTS.md                              |  4 +-\n docs/testing.md                        |  5 +-\n scripts/__tests__/verify-fast.test.mjs | 29 +++++++++--\n scripts/verify-fast.mjs                | 92 ++++++++++++++++++++++++++--------\n 4 files changed, 101 insertions(+), 29 deletions(-)

Fusion-Task-Id: FN-7051

Fusion-Task-Lineage: b8980f15-0103-4f40-9f8d-83907461988a
This commit is contained in:
gsxdsm
2026-06-26 01:10:00 -07:00
parent d739223de2
commit 29aee2196a
4 changed files with 101 additions and 29 deletions

View File

@@ -107,7 +107,7 @@ The merge gate is thin and trusted: CI blocks PRs on exactly Lint, Typecheck, Bu
pnpm test # gate suite + changed-only affected tests (bounded; never full-suite)
pnpm test:gate # the merge gate: curated engine-core suite + CI-shape test
pnpm smoke:boot # boot smoke: CLI --help + real serve /api/health
pnpm verify:fast # TEST-FREE verification: typecheck + build (scoped to changed packages) + boot smoke; recommended non-test verification/testCommand. Additive — changes no default
pnpm verify:fast # TEST-FREE verification: artifact bootstrap + scoped typecheck/build + CLI build + boot smoke; recommended non-test verification/testCommand. Additive — changes no default
pnpm test:velocity # weekly report-only test velocity baseline; use -- --measure --write-report to refresh
pnpm test:full # full workspace suite — explicit opt-in only
pnpm lint
@@ -115,7 +115,7 @@ pnpm build
pnpm verify:workspace # deep opt-in verification (lint -> test:full -> build); NOT the merge gate
```
`pnpm verify:fast` is the recommended **test-free verification** path: typecheck + build scoped to the changed packages (it reuses `pnpm test`'s changed-package resolution) plus the boot smoke once, with **no test run**. It is deterministic and flake-free, suitable as a project `testCommand`/verification command when you want non-test verification; the full suite stays available and runs non-blocking. It is additive and does not change `pnpm test`, the gate, or CI. See `docs/testing.md`.
`pnpm verify:fast` is the recommended **test-free verification** path: bootstrap missing/stale workspace dist artifacts, typecheck + build scoped to the changed packages (it reuses `pnpm test`'s changed-package resolution), an always-on `@runfusion/fusion` CLI build required by the source-checkout boot smoke, plus the boot smoke once, with **no test run**. It is deterministic and flake-free, suitable as a project `testCommand`/verification command when you want non-test verification; the full suite stays available and runs non-blocking. It is additive and does not change `pnpm test`, the gate, or CI. See `docs/testing.md`.
### Standing Rule: Flaky Tests Are Quarantined on Sight (Deletion Ratchet)

View File

@@ -32,7 +32,7 @@ Use the narrowest command that exercises the behavior you changed, then broaden
pnpm test # gate suite + changed-only affected tests (bounded; never full-suite)
pnpm test:gate # the merge gate: curated engine-core suite + CI-shape test
pnpm smoke:boot # boot smoke: CLI --help + real serve /api/health
pnpm verify:fast # TEST-FREE verification: typecheck + build (scoped to changed packages) + boot smoke
pnpm verify:fast # TEST-FREE verification: artifact bootstrap + scoped typecheck/build + CLI build + boot smoke
pnpm test:full # full workspace suite — explicit opt-in only
pnpm lint # lint all packages
pnpm build # build workspace packages (excludes desktop/mobile)
@@ -40,7 +40,8 @@ pnpm verify:workspace # deep opt-in verification: lint -> test:full -> build (N
```
<!-- FNXC:TestInfrastructure 2026-06-25-00:00: verify:fast is the opt-in test-free verification path. docs/testing.md observes the broad test gate caught no recalled real bugs while consuming ~70% of shipping time in flake triage; typecheck+build+boot-smoke gives deterministic, flake-free signal without running tests. It changes no default — pnpm test, the merge gate, and CI are untouched; the full suite stays available and runs non-blocking. -->
`pnpm verify:fast` (`scripts/verify-fast.mjs`) is the recommended **test-free verification** command: it runs **typecheck + build scoped to the changed packages** (reusing the same git-diff / changed-package resolution as `pnpm test`) followed by the existing **boot smoke** once — and runs **no test suite**. It gives deterministic, flake-free signal in seconds, so it is a sound project `testCommand`/verification command when you want non-test verification. With no affected package (root/docs-only diff) it runs the boot smoke only. Each step is bounded by the shared `runWithWatchdog` (class `changed`) so a hang fails fast, and it exits nonzero on the first failing step. This is purely additive: it does not change `pnpm test`, the merge gate, or CI, and the full suite stays available (`pnpm test:full`, non-blocking on push to main).
<!-- FNXC:TestInfrastructure 2026-06-26-00:49: verify:fast must bootstrap missing workspace dist artifacts and build @runfusion/fusion even when the CLI package is not in the changed-package set because package builds and the boot smoke invoke source-checkout wrappers that require dist outputs in fresh worktrees. -->
`pnpm verify:fast` (`scripts/verify-fast.mjs`) is the recommended **test-free verification** command: it bootstraps missing/stale workspace dist artifacts, runs **typecheck + build scoped to the changed packages** (reusing the same git-diff / changed-package resolution as `pnpm test`), always builds the `@runfusion/fusion` CLI package required by the source-checkout boot smoke, then runs the existing **boot smoke** once — and runs **no test suite**. It gives deterministic, flake-free signal in seconds, so it is a sound project `testCommand`/verification command when you want non-test verification. With no affected package (root/docs-only diff) it runs only the artifact bootstrap, CLI prerequisite build, and boot smoke. Each step is bounded by the shared `runWithWatchdog` (class `changed`) so a hang fails fast, and it exits nonzero on the first failing step. This is purely additive: it does not change `pnpm test`, the merge gate, or CI, and the full suite stays available (`pnpm test:full`, non-blocking on push to main).
`pnpm test:full` runs each package's default test script with capped worker fanout (`FUSION_TEST_TOTAL_WORKERS=4 FUSION_TEST_CONCURRENCY=2 pnpm -r --workspace-concurrency=2 test`). Do not casually raise worker counts; dashboard/jsdom and integration-heavy packages destabilize when oversubscribed. Use `VITEST_MAX_WORKERS=<n>` only for targeted package-level investigation.

View File

@@ -15,13 +15,16 @@ import {
buildTypecheckStep,
buildBuildStep,
buildBootSmokeStep,
buildArtifactBootstrapStep,
buildVerifyPlan,
VERIFY_EXCLUDED_PACKAGES,
BOOT_SMOKE_REQUIRED_BUILD_PACKAGES,
} from "../verify-fast.mjs";
import { resolveAffectedPackages } from "../test-changed.mjs";
const SMOKE = "/repo/scripts/boot-smoke.mjs";
const BOOTSTRAP = "/repo/scripts/ensure-test-artifacts.mjs";
const NODE = "/usr/bin/node";
function stepIds(plan) {
@@ -53,7 +56,7 @@ test("buildTypecheckStep: defaults to the tsc fallback when meta omitted", () =>
});
// ---------------------------------------------------------------------------
// buildBuildStep / buildBootSmokeStep
// buildBuildStep / buildBootSmokeStep / buildArtifactBootstrapStep
// ---------------------------------------------------------------------------
test("buildBuildStep: scoped pnpm build for the package", () => {
@@ -69,14 +72,22 @@ test("buildBootSmokeStep: runs the boot-smoke script via node", () => {
assert.equal(step.kind, "boot-smoke");
});
test("buildArtifactBootstrapStep: runs the artifact bootstrap script via node", () => {
const step = buildArtifactBootstrapStep(BOOTSTRAP, NODE);
assert.equal(step.command, NODE);
assert.deepEqual(step.args, [BOOTSTRAP]);
assert.equal(step.kind, "bootstrap-artifacts");
});
// ---------------------------------------------------------------------------
// buildVerifyPlan
// ---------------------------------------------------------------------------
test("buildVerifyPlan: no packages -> boot smoke only", () => {
test("buildVerifyPlan: no packages -> CLI prerequisite build then boot smoke", () => {
const plan = buildVerifyPlan({ packages: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), ["boot-smoke"]);
assert.deepEqual(stepIds(plan), ["bootstrap-artifacts", "build:@runfusion/fusion", "boot-smoke"]);
assert.deepEqual(plan.eligiblePackages, []);
assert.deepEqual(plan.requiredBootBuildPackages, ["@runfusion/fusion"]);
});
test("buildVerifyPlan: typecheck for all eligible, then builds, then boot smoke (ordered)", () => {
@@ -86,10 +97,12 @@ test("buildVerifyPlan: typecheck for all eligible, then builds, then boot smoke
]);
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/core"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
"bootstrap-artifacts",
"typecheck:@fusion/engine",
"typecheck:@fusion/core",
"build:@fusion/engine",
"build:@fusion/core",
"build:@runfusion/fusion",
"boot-smoke",
]);
});
@@ -101,9 +114,11 @@ test("buildVerifyPlan: a package without a build script gets a typecheck step bu
]);
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/test-only"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
"bootstrap-artifacts",
"typecheck:@fusion/engine",
"typecheck:@fusion/test-only",
"build:@fusion/engine",
"build:@runfusion/fusion",
"boot-smoke",
]);
// The test-only package's typecheck uses the tsc fallback (no typecheck script).
@@ -125,7 +140,7 @@ test("buildVerifyPlan: desktop/mobile are excluded from scoped steps but boot sm
});
assert.deepEqual(plan.eligiblePackages, ["@fusion/engine"]);
assert.deepEqual(plan.excludedPackages.sort(), ["@fusion/desktop", "@fusion/mobile"]);
assert.deepEqual(stepIds(plan), ["typecheck:@fusion/engine", "build:@fusion/engine", "boot-smoke"]);
assert.deepEqual(stepIds(plan), ["bootstrap-artifacts", "typecheck:@fusion/engine", "build:@fusion/engine", "build:@runfusion/fusion", "boot-smoke"]);
});
test("VERIFY_EXCLUDED_PACKAGES mirrors the root build/typecheck exclusions", () => {
@@ -133,6 +148,10 @@ test("VERIFY_EXCLUDED_PACKAGES mirrors the root build/typecheck exclusions", ()
assert.ok(VERIFY_EXCLUDED_PACKAGES.has("@fusion/mobile"));
});
test("BOOT_SMOKE_REQUIRED_BUILD_PACKAGES includes the source-checkout CLI", () => {
assert.deepEqual(BOOT_SMOKE_REQUIRED_BUILD_PACKAGES, ["@runfusion/fusion"]);
});
// ---------------------------------------------------------------------------
// Integration: reuse test-changed's resolveAffectedPackages to scope the plan
// ---------------------------------------------------------------------------
@@ -151,5 +170,5 @@ test("buildVerifyPlan: scopes to exactly the packages resolveAffectedPackages se
const packageMeta = new Map([["@fusion/engine", { hasTypecheck: true, hasBuild: true }]]);
const plan = buildVerifyPlan({ packages: affected, packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), ["typecheck:@fusion/engine", "build:@fusion/engine", "boot-smoke"]);
assert.deepEqual(stepIds(plan), ["bootstrap-artifacts", "typecheck:@fusion/engine", "build:@fusion/engine", "build:@runfusion/fusion", "boot-smoke"]);
});

View File

@@ -3,10 +3,18 @@
FNXC:TestInfrastructure 2026-06-25-00:00:
verify:fast is the opt-in, TEST-FREE verification command. It gives deterministic,
flake-free signal in seconds without running the test suite, by doing exactly:
1. typecheck — scoped to the changed packages (their `typecheck` script, or
1. bootstrap — rebuild missing/stale workspace dist prerequisites used by package builds.
2. typecheck — scoped to the changed packages (their `typecheck` script, or
`pnpm --filter <pkg> exec tsc --noEmit -p .` when none exists).
2. build — scoped to the changed packages (`pnpm --filter <pkg> build`).
3. boot smoke — once (scripts/boot-smoke.mjs: CLI --help + real serve /api/health).
3. build — scoped to the changed packages, plus the CLI package needed by boot smoke.
4. boot smoke — once (scripts/boot-smoke.mjs: CLI --help + real serve /api/health).
FNXC:TestInfrastructure 2026-06-26-00:49:
A fresh worktree can have no plugin runtime dist artifacts or `packages/cli/dist/bin.js`.
Since package builds import those plugin artifacts and boot-smoke invokes the
source-checkout CLI wrapper, verify:fast must bootstrap workspace dist artifacts
and always build @runfusion/fusion before the smoke so the command proves a
runnable checkout instead of failing on a missing prerequisite.
Rationale: docs/testing.md observes the broad test gate "caught no recalled real
bugs while consuming ~70% of shipping time in flake triage." typecheck+build+boot
@@ -42,6 +50,7 @@ import { deriveBudgetMs, runWithWatchdog } from "./lib/run-vitest-watchdog.mjs";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..");
const bootSmokeScriptPath = path.join(scriptDir, "boot-smoke.mjs");
const artifactBootstrapScriptPath = path.join(scriptDir, "ensure-test-artifacts.mjs");
/*
FNXC:TestInfrastructure 2026-06-25-00:00:
@@ -50,6 +59,7 @@ scripts (heavy native/electron + RN toolchains), so verify:fast mirrors that
policy and skips them with a note rather than failing on an unbuildable filter.
*/
export const VERIFY_EXCLUDED_PACKAGES = new Set(["@fusion/desktop", "@fusion/mobile"]);
export const BOOT_SMOKE_REQUIRED_BUILD_PACKAGES = ["@runfusion/fusion"];
/**
* Build the scoped typecheck step for a package. Prefers the package's own
@@ -96,35 +106,73 @@ export function buildBootSmokeStep(smokeScriptPath, nodeBin = process.execPath)
};
}
/**
* Build the prerequisite artifact bootstrap step. It is intentionally first so
* fresh worktrees have the bundled plugin/runtime dist outputs before scoped
* package builds (especially @runfusion/fusion) import them.
*
* @param {string} bootstrapScriptPath
* @param {string} [nodeBin]
*/
export function buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin = process.execPath) {
return {
id: "bootstrap-artifacts",
kind: "bootstrap-artifacts",
pkg: null,
label: "bootstrap workspace dist artifacts",
command: nodeBin,
args: [bootstrapScriptPath],
klass: "changed",
};
}
/**
* Pure planner: turn the affected package set into an ordered step list.
* typecheck (all eligible) → build (eligible with a build script) → boot smoke.
* With no eligible packages this is just the boot-smoke step, satisfying the
* "no packages changed ⇒ boot smoke only" contract.
* bootstrap missing/stale dist artifacts → typecheck (all eligible) → build
* (eligible with a build script) → required boot-smoke build prerequisites →
* boot smoke. With no eligible packages this still builds the source-checkout
* CLI before the smoke so fresh worktrees have `packages/cli/dist/bin.js`.
*
* @param {object} opts
* @param {string[]} [opts.packages] affected package names
* @param {Map<string, { dir?: string, hasTypecheck?: boolean, hasBuild?: boolean }>} [opts.packageMeta]
* @param {string} opts.bootSmokeScriptPath
* @param {string} [opts.artifactBootstrapScriptPath]
* @param {string} [opts.nodeBin]
* @returns {{ eligiblePackages: string[], excludedPackages: string[], steps: object[] }}
* @returns {{ eligiblePackages: string[], excludedPackages: string[], requiredBootBuildPackages: string[], steps: object[] }}
*/
export function buildVerifyPlan({ packages = [], packageMeta = new Map(), bootSmokeScriptPath: smokeScriptPath, nodeBin = process.execPath } = {}) {
export function buildVerifyPlan({ packages = [], packageMeta = new Map(), bootSmokeScriptPath: smokeScriptPath, artifactBootstrapScriptPath: bootstrapScriptPath = artifactBootstrapScriptPath, nodeBin = process.execPath } = {}) {
const eligiblePackages = packages.filter((pkg) => !VERIFY_EXCLUDED_PACKAGES.has(pkg));
const excludedPackages = packages.filter((pkg) => VERIFY_EXCLUDED_PACKAGES.has(pkg));
const steps = [];
const steps = [buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin)];
for (const pkg of eligiblePackages) {
steps.push(buildTypecheckStep(pkg, packageMeta.get(pkg) ?? {}));
}
const builtPackages = new Set();
for (const pkg of eligiblePackages) {
const meta = packageMeta.get(pkg) ?? {};
// Only build packages that declare a build script; pure test/config packages
// have nothing to emit and a `pnpm --filter <pkg> build` would error.
if (meta.hasBuild !== false) steps.push(buildBuildStep(pkg));
if (meta.hasBuild !== false) {
steps.push(buildBuildStep(pkg));
builtPackages.add(pkg);
}
}
const requiredBootBuildPackages = [];
for (const pkg of BOOT_SMOKE_REQUIRED_BUILD_PACKAGES) {
if (builtPackages.has(pkg) || VERIFY_EXCLUDED_PACKAGES.has(pkg)) continue;
const meta = packageMeta.get(pkg) ?? { hasBuild: true };
if (meta.hasBuild === false) continue;
requiredBootBuildPackages.push(pkg);
steps.push(buildBuildStep(pkg));
builtPackages.add(pkg);
}
steps.push(buildBootSmokeStep(smokeScriptPath, nodeBin));
return { eligiblePackages, excludedPackages, steps };
return { eligiblePackages, excludedPackages, requiredBootBuildPackages, steps };
}
/**
@@ -173,21 +221,21 @@ export function resolveAffectedForVerify() {
const packageDirByName = buildPackageDirByName(workspacePackages);
if (!comparisonBase) {
return { packages: [], packageDirByName, note: `could not resolve merge-base with ${baseBranch}; running boot smoke only` };
return { packages: [], packageDirByName, note: `could not resolve merge-base with ${baseBranch}; running boot-smoke prerequisite build only` };
}
const changedFiles = changedFilesSince(comparisonBase);
if (changedFiles === null) {
return { packages: [], packageDirByName, note: "failed to read git diff; running boot smoke only" };
return { packages: [], packageDirByName, note: "failed to read git diff; running boot-smoke prerequisite build only" };
}
if (changedFiles.length === 0) {
return { packages: [], packageDirByName, note: "no changes detected against base; running boot smoke only" };
return { packages: [], packageDirByName, note: "no changes detected against base; running boot-smoke prerequisite build only" };
}
const affected = resolveAffectedPackages(changedFiles, packageNameByDir);
if (affected === null) {
return { packages: [], packageDirByName, note: "changed file did not map to a workspace package; running boot smoke only" };
return { packages: [], packageDirByName, note: "changed file did not map to a workspace package; running boot-smoke prerequisite build only" };
}
if (affected.length === 0) {
return { packages: [], packageDirByName, note: "no affected workspace package (root/docs-only changes); running boot smoke only" };
return { packages: [], packageDirByName, note: "no affected workspace package (root/docs-only changes); running boot-smoke prerequisite build only" };
}
return { packages: affected, packageDirByName, note: `affected packages: ${affected.join(", ")}` };
}
@@ -224,26 +272,30 @@ export async function runStep(step, { spawnFn = spawn, log = console.log, errLog
export async function main() {
const overallStart = Date.now();
console.log("[verify:fast] test-free verification: typecheck + build (scoped to changed packages) + boot smoke.");
console.log("[verify:fast] test-free verification: scoped typecheck/build + CLI build + boot smoke.");
const { packages, packageDirByName, note } = resolveAffectedForVerify();
console.log(`[verify:fast] ${note}`);
const packageMeta = readPackageMeta(packages, packageDirByName);
const { eligiblePackages, excludedPackages, steps } = buildVerifyPlan({
const packageMeta = readPackageMeta([...new Set([...packages, ...BOOT_SMOKE_REQUIRED_BUILD_PACKAGES])], packageDirByName);
const { eligiblePackages, excludedPackages, requiredBootBuildPackages, steps } = buildVerifyPlan({
packages,
packageMeta,
bootSmokeScriptPath,
artifactBootstrapScriptPath,
});
if (excludedPackages.length > 0) {
console.log(`[verify:fast] skipping excluded packages (also excluded from root build/typecheck): ${excludedPackages.join(", ")}`);
}
if (eligiblePackages.length === 0) {
console.log("[verify:fast] no scoped packages to verify; running boot smoke only.");
console.log("[verify:fast] no scoped packages to verify; running boot-smoke prerequisite build and boot smoke only.");
} else {
console.log(`[verify:fast] scoped to: ${eligiblePackages.join(", ")}`);
}
if (requiredBootBuildPackages.length > 0) {
console.log(`[verify:fast] boot-smoke prerequisite build: ${requiredBootBuildPackages.join(", ")}`);
}
console.log(`[verify:fast] plan: ${steps.map((s) => s.id).join(" -> ")}`);
for (const step of steps) {