fix(ci): per-package timing files, acp flake timeout, 4040-guard marker

- 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 <pkgDir>/.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)
This commit is contained in:
gsxdsm
2026-06-03 20:34:51 -07:00
parent b9e7d5f9ab
commit de3156e4ad
5 changed files with 65 additions and 6 deletions

View File

@@ -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
# <pkgDir>/.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

View File

@@ -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[] = [];

View File

@@ -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

View File

@@ -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",
]);
});

View File

@@ -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 <pkgDir>/.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}`];
};