From 6e2e5c6b62c6c00686b51be1bce0adb491c6b0b8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 19 Jun 2026 22:05:09 -0700 Subject: [PATCH] WIP: scope affected test runs to changed-related tests (vitest --changed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Affected packages with a vitest config now run `vitest run --changed ` (only tests in the changed files' module graph) under the tight "changed" watchdog, instead of each package's ENTIRE suite under a 60-min backstop. Packages without a vitest config fall back to their full `test` script. The curated gate suite still runs as the cross-cutting safety net. Measured: a dashboard hub-component change drops from 822 files (5-8min) to 40 files (~98s wall incl. gate). Common/leaf changes scope to far fewer. NOT yet <60s for hub-component changes — the fixed jsdom/setup floor still dominates (needs the overhead-reduction follow-up), and one scoped-run test failure needs isolation triage before this gates verification. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/test-changed.mjs | 82 +++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/scripts/test-changed.mjs b/scripts/test-changed.mjs index 2625912de0..fc83a6d365 100644 --- a/scripts/test-changed.mjs +++ b/scripts/test-changed.mjs @@ -1255,6 +1255,29 @@ export function emitModeDecision(plan, log = console.log) { return line; } +const VITEST_CONFIG_BASENAMES = [ + "vitest.config.ts", + "vitest.config.mts", + "vitest.config.cts", + "vitest.config.js", + "vitest.config.mjs", + "vitest.config.cjs", +]; + +/** + * Whether a package can be run with `vitest --changed` scoping. True only when + * the package directory has a vitest config — otherwise the caller falls back to + * the package's own `test` script (e.g. desktop's `tsx scripts/test.ts`). + * + * @param {string|undefined} pkgDir repo-relative package dir (e.g. "packages/engine") + * @param {string} [projectRoot] + * @returns {boolean} + */ +export function packageHasVitestConfig(pkgDir, projectRoot = rootDir) { + if (!pkgDir) return false; + return VITEST_CONFIG_BASENAMES.some((name) => existsSync(path.join(projectRoot, pkgDir, name))); +} + export function normalizeForwardedArgs(argv) { const normalized = []; @@ -1444,21 +1467,60 @@ export async function main(argv = process.argv.slice(2)) { label: "test:gate (pre-affected)", }); - const filterArgs = activePackages.flatMap((pkg) => ["--filter", pkg]); console.log(`[test-changed] running tests for changed packages: ${activePackages.join(", ")}`); if (cachedPackages.length > 0) { console.log(`[test-changed] skipping cached packages: ${cachedPackages.join(", ")}`); } - await runMaybeIsolated("pnpm", [...filterArgs, `--workspace-concurrency=${workspaceConcurrency}`, "test", ...forwardedArgs], { - env: isolatedHomeEnv, - onBeforeAfterCheck: cleanupIsolatedHome, - // Affected sets can include dashboard (13 inner-watchdog'd lanes); use the - // generous full-suite backstop rather than the tight changed ceiling so a - // legitimately long local run is never false-killed. - budgetMs: FULL_SUITE_BUDGET_MS, - label: `affected: ${activePackages.join(", ")}`, - }); + // Scope the affected run to only the tests in the module graph of the changed + // files (`vitest --changed `) instead of each package's ENTIRE suite. + // A dashboard task otherwise re-ran all 822 dashboard test files (~5-8 min); + // scoping keeps verification proportional to the diff. Packages without a + // vitest config (or when we have no base to diff against) fall back to their + // full `test` script so coverage is never silently dropped. The curated gate + // suite already ran above as the cross-cutting safety net. + const scopable = comparisonBase + ? activePackages.filter((pkg) => packageHasVitestConfig(packageDirByName.get(pkg))) + : []; + const fallbackPkgs = activePackages.filter((pkg) => !scopable.includes(pkg)); + + for (const { packages, mode } of [ + { packages: scopable, mode: "scoped" }, + { packages: fallbackPkgs, mode: "full" }, + ]) { + if (packages.length === 0) continue; + const filterArgs = packages.flatMap((pkg) => ["--filter", pkg]); + const commandArgs = + mode === "scoped" + ? [ + ...filterArgs, + `--workspace-concurrency=${workspaceConcurrency}`, + "exec", + "vitest", + "run", + "--changed", + comparisonBase, + "--passWithNoTests", + "--silent=passed-only", + "--reporter=dot", + ...forwardedArgs, + ] + : [...filterArgs, `--workspace-concurrency=${workspaceConcurrency}`, "test", ...forwardedArgs]; + console.log( + mode === "scoped" + ? `[test-changed] scoped (vitest --changed) run for: ${packages.join(", ")}` + : `[test-changed] full package-suite run for: ${packages.join(", ")} (no vitest config / no base)`, + ); + await runMaybeIsolated("pnpm", commandArgs, { + env: isolatedHomeEnv, + onBeforeAfterCheck: cleanupIsolatedHome, + // Scoped runs are proportional to the diff, so the tight "changed" ceiling + // applies; a hang fails fast instead of blocking the 60-min full backstop. + // Full fallback runs keep the generous backstop. + budgetMs: mode === "scoped" ? deriveBudgetMs({ klass: "changed" }) : FULL_SUITE_BUDGET_MS, + label: `affected (${mode}): ${packages.join(", ")}`, + }); + } // Tests passed — record in cache (never cache failures; process.exit on failure above). recordCachePass(activePackages, packageDirByName, {