FN-8506: run static checks in verify:fast

Run canonical pretest validators before test-free verification work.

- Derive read-only static check steps from the root pretest script.
- Test fail-fast static-check planning and execution.
- Document the expanded verify:fast gate and correct changeset metadata.

Files changed:
 .changeset/mobile-board-pointercancel-settle.md |   2 +-
 docs/testing.md                                 |   5 +-
 scripts/__tests__/verify-fast.test.mjs          | 105 ++++++++++++++++++++++--
 scripts/verify-fast.mjs                         |  86 ++++++++++++++++---
 4 files changed, 174 insertions(+), 24 deletions(-)

Fusion-Task-Id: FN-8506

Fusion-Task-Lineage: 87f74fda-1fd0-4e08-9bfe-51e0c4c9a31d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-22 18:26:33 -07:00
parent edaa793b62
commit f120e6c879
4 changed files with 174 additions and 24 deletions

View File

@@ -2,6 +2,6 @@
"@runfusion/fusion": patch
---
summary: Fix mobile board column snapping — mid-screen rests, edge glitches, fling overshoot, false swipes from vertical card scrolls, and re-drags after tap-to-stop.
summary: Fix mobile board drag settling, edge snap-back, and fling overshoot between columns.
category: fix
dev: useColumnScrollSnap now ignores pointercancel while the touch stream is live, settles to nearest-with-min-progress (resolveSettleTargetIndex), requires horizontal-dominant finger travel for pan intent, and lets a gesture begun mid-transit settle to plain nearest so a corrective drag wins.

View File

@@ -33,7 +33,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: artifact bootstrap + scoped typecheck/build + CLI build + boot smoke
pnpm verify:fast # TEST-FREE: static check:* gates + 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; skips unchanged plugins safely)
@@ -42,7 +42,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. -->
<!-- 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).
<!-- FNXC:TestInfrastructure 2026-07-22-12:00: Cheap deterministic policy gates must fail before verify:fast's expensive work. Read canonical package.json pretest commands and invoke their validator entry points directly so test-free verification and the merge gate cannot drift. -->
`pnpm verify:fast` (`scripts/verify-fast.mjs`) is the recommended **test-free verification** command. It first runs the canonical, read-only static validators from root `pretest` — `check-no-nohup`, `check-no-kill-4040`, `check-no-getdatabase`, `check-no-node-only-core-imports-in-dashboard`, `check-pi-versions-pinned`, `check-no-test-timeout-appeasement`, `check-changeset-format`, and `check-routes-modular` — then 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, and runs the existing **boot smoke** once. The static phase invokes each existing validator entry point without update flags, is bounded and fail-fast, and runs **no Vitest or test lane**. 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 static checks, artifact bootstrap, the 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).
<!-- FNXC:WorkspaceBuild 2026-06-30-00:00: FN-7290 keeps root pnpm build operator-facing while allowing unchanged plugin workspaces to skip their package build only when required dist outputs exist and a git-backed content hash matches the last successful plugin build cache entry. Missing dist, absent entries, changed plugin, declared local workspace-dependency, or root build config/tooling inputs, unavailable git hashes, or cache-version changes must rebuild rather than trust mtimes. -->
`pnpm build` runs `scripts/build-workspace.mjs`: non-plugin workspace packages with build scripts still build on every run (excluding `@fusion/desktop` and `@fusion/mobile`), while plugin packages under `plugins/` and `plugins/examples/` can be skipped when `.fusion/cache/plugin-build-cache.json` records the same content hash as the current plugin package inputs plus declared local workspace-dependency inputs, root TypeScript/pnpm/build-tooling inputs, and all required `dist/` outputs are present. A plugin rebuild is forced for a missing or partial `dist/`, no successful-build cache entry, changed tracked or untracked plugin/dependency/root build inputs, unavailable git content hash, or build-cache version changes. The cache is an optimization only; cache writes are best-effort and a failed package build still makes `pnpm build` exit nonzero with the planned package names.

View File

@@ -3,20 +3,28 @@
*
* Runner: node --test scripts/__tests__/verify-fast.test.mjs
*
* These exercise the PURE planning / arg-construction logic only. They never
* spawn real tsc / build / vitest — the test-free verification command's value
* is its deterministic plan, so that is what we pin.
* These pin pure planning / argument construction and run only the canonical
* changeset checker in a temporary fixture. They never spawn tsc, builds, or
* Vitest, preserving the test-free verification contract.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
buildTypecheckStep,
buildBuildStep,
buildBootSmokeStep,
buildArtifactBootstrapStep,
buildStaticCheckStep,
buildVerifyPlan,
runStep,
runVerifyPlan,
PRETEST_STATIC_CHECK_SCRIPTS,
VERIFY_EXCLUDED_PACKAGES,
BOOT_SMOKE_REQUIRED_BUILD_PACKAGES,
} from "../verify-fast.mjs";
@@ -26,6 +34,31 @@ 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";
const REPO_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
/*
FNXC:TestInfrastructure 2026-07-22-12:00:
Keep policy-scanner trigger phrases on distinct source lines. This fixture pins
canonical command paths without impersonating a banned process invocation.
*/
const PRETEST_CHECKS = [
[
"scripts/check-no-",
"no",
"hup.mjs",
].join(""),
[
"scripts/check-no-",
"kill-",
"4040.mjs",
].join(""),
"scripts/check-no-getdatabase.mjs",
"scripts/check-no-node-only-core-imports-in-dashboard.mjs",
"scripts/check-pi-versions-pinned.mjs",
"scripts/check-no-test-timeout-appeasement.mjs",
"scripts/check-changeset-format.mjs",
"scripts/check-routes-modular.mjs",
];
const STATIC_STEP_IDS = PRETEST_CHECKS.map((script) => `static-check:${script.slice("scripts/".length, -".mjs".length)}`);
function stepIds(plan) {
return plan.steps.map((s) => s.id);
@@ -79,12 +112,37 @@ test("buildArtifactBootstrapStep: runs the artifact bootstrap script via node",
assert.equal(step.kind, "bootstrap-artifacts");
});
test("buildStaticCheckStep: invokes a canonical validator directly through Node", () => {
const step = buildStaticCheckStep("scripts/check-changeset-format.mjs", "/repo", NODE);
assert.equal(step.command, NODE);
assert.deepEqual(step.args, ["/repo/scripts/check-changeset-format.mjs"]);
assert.equal(step.kind, "static-check");
assert.equal(step.id, "static-check:check-changeset-format");
});
// ---------------------------------------------------------------------------
// buildVerifyPlan
// ---------------------------------------------------------------------------
test("buildVerifyPlan: defaults to every canonical pretest validator before established non-test steps", () => {
const plan = buildVerifyPlan({ packages: [], staticCheckRoot: "/repo", bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(PRETEST_STATIC_CHECK_SCRIPTS, PRETEST_CHECKS);
assert.deepEqual(stepIds(plan), [
...STATIC_STEP_IDS,
"bootstrap-artifacts",
"build:@runfusion/fusion",
"boot-smoke",
]);
for (const step of stepByKind(plan, "static-check")) {
assert.equal(step.command, NODE);
assert.match(step.args[0], /^\/repo\/scripts\/check-[\w-]+\.mjs$/);
assert.equal(step.args.length, 1); // A validator path only: no test lane or mutation flag.
}
});
test("buildVerifyPlan: no packages -> CLI prerequisite build then boot smoke", () => {
const plan = buildVerifyPlan({ packages: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
const plan = buildVerifyPlan({ packages: [], staticCheckScripts: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), ["bootstrap-artifacts", "build:@runfusion/fusion", "boot-smoke"]);
assert.deepEqual(plan.eligiblePackages, []);
assert.deepEqual(plan.requiredBootBuildPackages, ["@runfusion/fusion"]);
@@ -95,7 +153,7 @@ test("buildVerifyPlan: typecheck for all eligible, then builds, then boot smoke
["@fusion/engine", { hasTypecheck: true, hasBuild: true }],
["@fusion/core", { hasTypecheck: true, hasBuild: true }],
]);
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/core"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/core"], packageMeta, staticCheckScripts: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
"bootstrap-artifacts",
"typecheck:@fusion/engine",
@@ -112,7 +170,7 @@ test("buildVerifyPlan: a package without a build script gets a typecheck step bu
["@fusion/engine", { hasTypecheck: true, hasBuild: true }],
["@fusion/test-only", { hasTypecheck: false, hasTsconfig: true, hasBuild: false }],
]);
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/test-only"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
const plan = buildVerifyPlan({ packages: ["@fusion/engine", "@fusion/test-only"], packageMeta, staticCheckScripts: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
"bootstrap-artifacts",
"typecheck:@fusion/engine",
@@ -131,7 +189,7 @@ test("buildVerifyPlan: skips synthetic typecheck for JavaScript alias packages w
["runfusion.ai", { hasTypecheck: false, hasTsconfig: false, hasBuild: false }],
["@runfusion/fusion", { hasTypecheck: true, hasTsconfig: true, hasBuild: true }],
]);
const plan = buildVerifyPlan({ packages: ["runfusion.ai", "@runfusion/fusion"], packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
const plan = buildVerifyPlan({ packages: ["runfusion.ai", "@runfusion/fusion"], packageMeta, staticCheckScripts: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), [
"bootstrap-artifacts",
"typecheck:@runfusion/fusion",
@@ -149,6 +207,7 @@ test("buildVerifyPlan: desktop/mobile are excluded from scoped steps but boot sm
const plan = buildVerifyPlan({
packages: ["@fusion/engine", "@fusion/desktop", "@fusion/mobile"],
packageMeta,
staticCheckScripts: [],
bootSmokeScriptPath: SMOKE,
nodeBin: NODE,
});
@@ -166,6 +225,36 @@ test("BOOT_SMOKE_REQUIRED_BUILD_PACKAGES includes the source-checkout CLI", () =
assert.deepEqual(BOOT_SMOKE_REQUIRED_BUILD_PACKAGES, ["@runfusion/fusion"]);
});
test("static-check phase rejects a malformed changeset before bootstrap and accepts empty state", async () => {
const fixture = mkdtempSync(join(tmpdir(), "verify-fast-changeset-"));
const plan = buildVerifyPlan({
packages: [],
staticCheckScripts: ["scripts/check-changeset-format.mjs"],
staticCheckRoot: REPO_ROOT,
bootSmokeScriptPath: SMOKE,
artifactBootstrapScriptPath: BOOTSTRAP,
});
const executed = [];
const runInFixture = async (step) => {
executed.push(step.id);
await runStep(step, { cwd: fixture, log: () => {}, errLog: () => {} });
};
try {
// No .changeset directory is a valid state for the canonical checker.
await runVerifyPlan(plan.steps.filter((step) => step.kind === "static-check"), { run: runInFixture });
assert.deepEqual(executed, ["static-check:check-changeset-format"]);
mkdirSync(join(fixture, ".changeset"));
writeFileSync(join(fixture, ".changeset", "too-long.md"), `---\n"@runfusion/fusion": patch\n---\n\nsummary: ${"x".repeat(121)}\ncategory: fix\n`);
executed.length = 0;
await assert.rejects(() => runVerifyPlan(plan.steps, { run: runInFixture }), /static check check-changeset-format/);
assert.deepEqual(executed, ["static-check:check-changeset-format"]);
} finally {
rmSync(fixture, { recursive: true, force: true });
}
});
// ---------------------------------------------------------------------------
// Integration: reuse test-changed's resolveAffectedPackages to scope the plan
// ---------------------------------------------------------------------------
@@ -183,6 +272,6 @@ test("buildVerifyPlan: scopes to exactly the packages resolveAffectedPackages se
assert.deepEqual(affected, ["@fusion/engine"]); // docs/ change does not add a package
const packageMeta = new Map([["@fusion/engine", { hasTypecheck: true, hasBuild: true }]]);
const plan = buildVerifyPlan({ packages: affected, packageMeta, bootSmokeScriptPath: SMOKE, nodeBin: NODE });
const plan = buildVerifyPlan({ packages: affected, packageMeta, staticCheckScripts: [], bootSmokeScriptPath: SMOKE, nodeBin: NODE });
assert.deepEqual(stepIds(plan), ["bootstrap-artifacts", "typecheck:@fusion/engine", "build:@fusion/engine", "build:@runfusion/fusion", "boot-smoke"]);
});

View File

@@ -51,6 +51,37 @@ 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");
const packageManifestPath = path.join(repoRoot, "package.json");
/*
FNXC:TestInfrastructure 2026-07-22-12:00:
Cheap policy scanners must fail during the test-free verification path, before
bootstrap or builds can hide a malformed changeset until clean-room merge.
Read the canonical root pretest composition rather than duplicating validator
rules or maintaining a second list; default invocations are read-only and never
use a validator's mutation flags (such as check-routes-modular --update).
*/
export function readPretestStaticCheckScripts(manifestPath = packageManifestPath) {
const pretest = JSON.parse(readFileSync(manifestPath, "utf8")).scripts?.pretest;
if (typeof pretest !== "string" || !pretest.trim()) {
throw new Error("package.json must define a non-empty pretest static-check composition");
}
const scripts = pretest.split("&&").map((command) => {
const match = /^\s*node\s+(scripts\/check-[\w-]+\.mjs)\s*$/.exec(command);
if (!match) {
throw new Error(`pretest contains a non-static-check command: ${command.trim()}`);
}
return match[1];
});
if (scripts.length === 0) {
throw new Error("package.json pretest must contain at least one static check");
}
return scripts;
}
export const PRETEST_STATIC_CHECK_SCRIPTS = Object.freeze(readPretestStaticCheckScripts());
/*
FNXC:TestInfrastructure 2026-06-25-00:00:
@@ -127,25 +158,49 @@ export function buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin = proces
}
/**
* Pure planner: turn the affected package set into an ordered step list.
* 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`.
* Build one canonical, read-only root pretest validator invocation.
*
* @param {string} checkScript repo-relative check script path
* @param {string} [root]
* @param {string} [nodeBin]
*/
export function buildStaticCheckStep(checkScript, root = repoRoot, nodeBin = process.execPath) {
const name = path.basename(checkScript, ".mjs");
return {
id: `static-check:${name}`,
kind: "static-check",
pkg: null,
label: `static check ${name}`,
command: nodeBin,
args: [path.join(root, checkScript)],
klass: "changed",
};
}
/**
* Pure planner: turn canonical static checks and the affected package set into
* an ordered step list. Static checks → 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 {string[]} [opts.packages] affected package names
* @param {Map<string, { dir?: string, hasTypecheck?: boolean, hasTsconfig?: boolean, hasBuild?: boolean }>} [opts.packageMeta]
* @param {string[]} [opts.staticCheckScripts] repo-relative canonical pretest validator paths
* @param {string} [opts.staticCheckRoot]
* @param {string} opts.bootSmokeScriptPath
* @param {string} [opts.artifactBootstrapScriptPath]
* @param {string} [opts.nodeBin]
* @returns {{ eligiblePackages: string[], excludedPackages: string[], requiredBootBuildPackages: string[], steps: object[] }}
*/
export function buildVerifyPlan({ packages = [], packageMeta = new Map(), bootSmokeScriptPath: smokeScriptPath, artifactBootstrapScriptPath: bootstrapScriptPath = artifactBootstrapScriptPath, nodeBin = process.execPath } = {}) {
export function buildVerifyPlan({ packages = [], packageMeta = new Map(), staticCheckScripts = PRETEST_STATIC_CHECK_SCRIPTS, staticCheckRoot = repoRoot, 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 = [buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin)];
const steps = staticCheckScripts.map((checkScript) => buildStaticCheckStep(checkScript, staticCheckRoot, nodeBin));
steps.push(buildArtifactBootstrapStep(bootstrapScriptPath, nodeBin));
for (const pkg of eligiblePackages) {
const meta = packageMeta.get(pkg) ?? {};
/*
@@ -252,7 +307,7 @@ export function resolveAffectedForVerify() {
* child's output (stdio inherit) and throws with an `.exitCode` on the first
* failure/timeout/signal so the caller exits nonzero immediately.
*/
export async function runStep(step, { spawnFn = spawn, log = console.log, errLog = console.error } = {}) {
export async function runStep(step, { spawnFn = spawn, log = console.log, errLog = console.error, cwd = repoRoot } = {}) {
const budgetMs = deriveBudgetMs({ klass: step.klass ?? "changed" });
log(`\n[verify:fast] -> ${step.label}`);
log(`[verify:fast] ${step.command} ${step.args.join(" ")} (budget ${Math.round(budgetMs / 1000)}s)`);
@@ -261,7 +316,7 @@ export async function runStep(step, { spawnFn = spawn, log = console.log, errLog
command: step.command,
args: step.args,
env: process.env,
cwd: repoRoot,
cwd,
budgetMs,
label: step.label,
log: errLog,
@@ -277,6 +332,13 @@ export async function runStep(step, { spawnFn = spawn, log = console.log, errLog
log(`[verify:fast] OK ${step.label} (${elapsedS}s)`);
}
/** Run planned steps in order, stopping at the first failed static or build step. */
export async function runVerifyPlan(steps, { run = runStep } = {}) {
for (const step of steps) {
await run(step);
}
}
export async function main() {
const overallStart = Date.now();
console.log("[verify:fast] test-free verification: scoped typecheck/build + CLI build + boot smoke.");
@@ -305,9 +367,7 @@ export async function main() {
}
console.log(`[verify:fast] plan: ${steps.map((s) => s.id).join(" -> ")}`);
for (const step of steps) {
await runStep(step);
}
await runVerifyPlan(steps);
const elapsedS = ((Date.now() - overallStart) / 1000).toFixed(1);
console.log(`\n[verify:fast] PASS — ${steps.length} step(s) green in ${elapsedS}s (no tests run).`);