From de3156e4ad0483ff4b9fdb5fe3bc6e3e94158430 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 3 Jun 2026 20:34:51 -0700 Subject: [PATCH] fix(ci): per-package timing files, acp flake timeout, 4040-guard marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci-test-shard: timing outputFile is now RELATIVE — one pnpm invocation fans out to several packages whose vitests all received the same absolute path, so every package overwrote the same timings file (last writer wins). Each package now writes /.timings/; discovery (discoverWorkspaceTimingFiles) and the CI artifact globs scan the tree - acp event-bridge-bounds: 20s timeout on the CPU-bound plan-flood test (timed out at default 5s under loaded CI shard, passes in isolation) - acp process-manager: port-4040-allowlist marker for its doc comments (main-side; local guard flagged it after merging main) --- .github/workflows/pr-checks.yml | 8 ++++- .../src/__tests__/event-bridge-bounds.test.ts | 5 ++- .../src/process-manager.ts | 1 + .../__tests__/ci-test-shard-timings.test.mjs | 21 +++++++++++ scripts/ci-test-shard.mjs | 36 ++++++++++++++++--- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index c63e74bea7..987f54b820 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -96,7 +96,13 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-timings-shard-${{ matrix.shard }} - path: .timings/timings-*.json + # Relative outputFile paths mean each package writes its own + # /.timings/ file — glob the whole tree, not just the root. + path: | + .timings/timings-*.json + packages/*/.timings/timings-*.json + plugins/*/.timings/timings-*.json + plugins/examples/*/.timings/timings-*.json if-no-files-found: ignore retention-days: 14 diff --git a/plugins/fusion-plugin-acp-runtime/src/__tests__/event-bridge-bounds.test.ts b/plugins/fusion-plugin-acp-runtime/src/__tests__/event-bridge-bounds.test.ts index 1c089ebcfa..74c7223593 100644 --- a/plugins/fusion-plugin-acp-runtime/src/__tests__/event-bridge-bounds.test.ts +++ b/plugins/fusion-plugin-acp-runtime/src/__tests__/event-bridge-bounds.test.ts @@ -203,7 +203,10 @@ describe("plan output bounds (S5)", () => { }); }); - it("a plan-ONLY stream stops emitting once the per-turn cap is crossed", async () => { + // Generous timeout: this test does CPU-bound string flooding (~25 plan + // events x 100 entries x 2k chars) and has timed out at the default 5s + // under loaded CI shards while passing easily in isolation. + it("a plan-ONLY stream stops emitting once the per-turn cap is crossed", { timeout: 20_000 }, async () => { const { createEventBridge, PER_CHUNK_CAP_CHARS, PER_TURN_OUTPUT_CAP_CHARS, MAX_PLAN_ENTRIES } = await import("../event-bridge.js"); const thinking: string[] = []; diff --git a/plugins/fusion-plugin-acp-runtime/src/process-manager.ts b/plugins/fusion-plugin-acp-runtime/src/process-manager.ts index db5f369503..2e52335f5b 100644 --- a/plugins/fusion-plugin-acp-runtime/src/process-manager.ts +++ b/plugins/fusion-plugin-acp-runtime/src/process-manager.ts @@ -1,3 +1,4 @@ +// port-4040-allowlist: doc comments below reference the "never kill port 4040" rule; no kill targets it. // Subprocess lifecycle for the ACP runtime. // // Mirrors the hardening conventions in diff --git a/scripts/__tests__/ci-test-shard-timings.test.mjs b/scripts/__tests__/ci-test-shard-timings.test.mjs index e3b8c5241c..df27dcd169 100644 --- a/scripts/__tests__/ci-test-shard-timings.test.mjs +++ b/scripts/__tests__/ci-test-shard-timings.test.mjs @@ -18,6 +18,7 @@ import { buildTimingsSnapshot, writeTimings, TIMINGS_SNAPSHOT_RELATIVE, + discoverWorkspaceTimingFiles, } from "../ci-test-shard.mjs"; const PACKAGES = [ @@ -203,3 +204,23 @@ test("writeTimings warns and does not write when there are no input files", () = rmSync(root, { recursive: true, force: true }); } }); + +test("discoverWorkspaceTimingFiles finds root and per-package .timings files", (t) => { + const root = mkdtempSync(path.join(tmpdir(), "wts-discover-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + mkdirSync(path.join(root, ".timings"), { recursive: true }); + mkdirSync(path.join(root, "packages/aaa/.timings"), { recursive: true }); + mkdirSync(path.join(root, "plugins/bbb/.timings"), { recursive: true }); + mkdirSync(path.join(root, "packages/no-timings-here"), { recursive: true }); + writeFileSync(path.join(root, ".timings/timings-shard1-0.json"), "{}"); + writeFileSync(path.join(root, "packages/aaa/.timings/timings-shard1-0.json"), "{}"); + writeFileSync(path.join(root, "plugins/bbb/.timings/timings-shard2-0.json"), "{}"); + writeFileSync(path.join(root, "packages/aaa/.timings/not-a-match.txt"), ""); + + const found = discoverWorkspaceTimingFiles(root).map((f) => path.relative(root, f)); + assert.deepEqual(found.sort(), [ + ".timings/timings-shard1-0.json", + "packages/aaa/.timings/timings-shard1-0.json", + "plugins/bbb/.timings/timings-shard2-0.json", + ]); +}); diff --git a/scripts/ci-test-shard.mjs b/scripts/ci-test-shard.mjs index 729604fd6b..aba7f31500 100644 --- a/scripts/ci-test-shard.mjs +++ b/scripts/ci-test-shard.mjs @@ -964,6 +964,27 @@ export function discoverTimingFiles(dir) { .sort(); } +/** + * Discover timing files across the whole workspace: the root .timings/ dir + * plus every package/plugin's own .timings/ dir (shard runs emit RELATIVE + * outputFile paths, so each package writes under its own directory — see + * the timingFlags comment in main()). + * + * @param {string} projectRoot + * @returns {string[]} Absolute paths, sorted. + */ +export function discoverWorkspaceTimingFiles(projectRoot) { + const dirs = [ + path.join(projectRoot, ".timings"), + ...globSync("{packages,plugins,plugins/examples}/*/.timings", { cwd: projectRoot }).map((d) => + path.join(projectRoot, d), + ), + ]; + const files = new Set(); + for (const dir of dirs) for (const f of discoverTimingFiles(dir)) files.add(f); + return [...files].sort(); +} + /** * Merge per-shard JSON reporter outputs into the committed snapshot. * Refuses to overwrite a snapshot whose capturedAt is newer than this run's. @@ -975,7 +996,9 @@ export function writeTimings(options = {}) { const projectRoot = options.projectRoot ?? process.cwd(); const snapshotPath = options.snapshotPath ?? path.join(projectRoot, TIMINGS_SNAPSHOT_RELATIVE); const inputs = options.inputs - ?? discoverTimingFiles(options.inputDir ?? path.join(projectRoot, ".timings")); + ?? (options.inputDir + ? discoverTimingFiles(options.inputDir) + : discoverWorkspaceTimingFiles(projectRoot)); if (inputs.length === 0) { console.warn("[ci-test-shard] no timing input files found; snapshot unchanged."); @@ -1236,11 +1259,16 @@ export function main(argv = process.argv.slice(2), env = process.env) { // Per-shard timing telemetry (U1 / R4): each test invocation also emits a // vitest JSON reporter file under .timings/. These are uploaded as CI // artifacts and consumed by `--write-timings` to refresh the snapshot. - const timingsDir = path.join(process.cwd(), ".timings"); - mkdirSync(timingsDir, { recursive: true }); + // + // The path MUST be RELATIVE: one pnpm invocation can fan out to several + // packages, each spawning its own vitest with these identical forwarded + // flags. A relative path resolves against each package's cwd, giving every + // package its own /.timings/ file; an absolute path would make all + // packages in the invocation overwrite the same file (last writer wins, + // silently dropping every other package's timings). let invocationIndex = 0; const timingFlags = () => { - const outputFile = path.join(timingsDir, `timings-shard${shard}-${invocationIndex++}.json`); + const outputFile = path.join(".timings", `timings-shard${shard}-${invocationIndex++}.json`); return ["--reporter=json", `--outputFile.json=${outputFile}`]; };