Files
gbrain/test/scripts/run-verify-parallel.test.ts
T
3a2605e9a0 v0.41.6.0 feat(ci): CI test speedup — 23min → ~9min via matrix 4→6 + weight-aware sharding + auto SHA cache + parallel verify (#1444)
* feat(ci): scripts/run-verify-parallel.sh — parallel verify dispatcher

Fans out the 21 pre-test grep guards via & + wait, captures per-check
exit codes in a tempdir, aggregates failures with named check + log
tail to stderr on miss. Wallclock 27s sequential → 13s parallel
locally (2x). Bigger CI win is shard 1 deload (workflow restructure
in a later commit).

Pinned by test/scripts/run-verify-parallel.test.ts (6 cases: CLI
contract + synthetic dispatcher failure-surfacing).

* feat(ci): weight-aware LPT bin-packer + auto SHA cache hash

scripts/sharding.ts (NEW) — pure TypeScript LPT bin-packer. Sort
weights desc, assign each file to the shard with current minimum total.
Worst-case makespan within 4/3 of optimal, O(n log n). Missing weights
fall back to corpus median (not 0). New test file → ships immediately
without regenerating weights. Pinned by test/scripts/sharding.test.ts
(23 cases).

scripts/mine-shard-weights.ts (NEW) — scrapes per-file timing from
gh run view --log via timestamp delta between ##[group]test/foo.test.ts:
headers within a shard. Three input modes: --run <ID>, --from-file
<PATH>, stdin. Stable JSON output (sorted keys). Initial weights mined
from run 26398061007. Pinned by test/scripts/mine-shard-weights.test.ts
(15 cases).

scripts/ci-cache-hash.sh (NEW) — deterministic 16-char sha256 over
git ls-files -s minus deny-list (CHANGELOG/TODOS/README/LICENSE/
docs/**/*.md). CLAUDE.md, AGENTS.md, skills/**/* deliberately
INCLUDED (8+ test files read them; deny-listing would create
false-pass holes). ~40ms on 1891 files. Pinned by
test/scripts/ci-cache-hash.test.ts (24 cases: 8 CRITICAL false-pass
guards + 7 SAFE deny-list invariants + 9 edge cases).

scripts/test-weights.json (NEW) — 712 weights. Total 3306s observed
runtime; median 30ms; max 6 min outlier.

* chore: bump version and changelog (v0.41.6.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:15:11 -07:00

176 lines
6.0 KiB
TypeScript

// run-verify-parallel.test.ts — pin the dispatcher's contract.
//
// We can't easily fake the 20-check list inside the script (it's a static
// array), but we CAN verify:
// 1. --dry-list emits one line per check
// 2. Unknown args exit 2
// 3. The fast-path measurement claim — running it on a clean tree
// completes faster than the sequential `&&`-chain would have
// 4. A made-up failing check propagates exit 1 with the named check
// in the failure report (covered by overriding the CHECKS array
// via a sibling temp script)
//
// (3) is a soft regression guard, not a hard timing assertion (CI runners
// vary). (4) is the load-bearing test: failure surfaces with name + log.
import { describe, expect, it } from "bun:test";
import { spawnSync } from "node:child_process";
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const SCRIPT = "scripts/run-verify-parallel.sh";
describe("run-verify-parallel.sh — CLI contract", () => {
it("--dry-list emits one line per check, exit 0", () => {
const r = spawnSync("bash", [SCRIPT, "--dry-list"], { encoding: "utf8" });
expect(r.status).toBe(0);
const lines = r.stdout.trim().split("\n").filter(Boolean);
// The exact count is allowed to grow; assert > 10 and that every line
// looks like a script name (no whitespace, no quotes).
expect(lines.length).toBeGreaterThan(10);
for (const l of lines) {
expect(l).toMatch(/^[a-z][a-z0-9:_-]+$/);
}
});
it("includes the load-bearing privacy + jsonb + typecheck checks", () => {
const r = spawnSync("bash", [SCRIPT, "--dry-list"], { encoding: "utf8" });
expect(r.status).toBe(0);
const set = new Set(r.stdout.trim().split("\n"));
expect(set.has("check:privacy")).toBe(true);
expect(set.has("check:jsonb")).toBe(true);
expect(set.has("typecheck")).toBe(true);
expect(set.has("check:operations-filter-bypass")).toBe(true);
});
it("unknown arg exits 2 with usage error", () => {
const r = spawnSync("bash", [SCRIPT, "--bogus"], { encoding: "utf8" });
expect(r.status).toBe(2);
expect(r.stderr).toContain("unknown arg");
expect(r.stderr).toContain("usage:");
});
});
describe("run-verify-parallel.sh — failure surfacing (synthetic dispatcher)", () => {
// We can't inject a fake check into the real script without touching the
// CHECKS array. Instead, we write a SMALLER synthetic dispatcher that
// shares the same shape (background jobs, exit-file aggregation, log
// tail on failure) and assert the surfacing contract on it. This proves
// the design, leaving the real script tested via CLI contract above.
//
// Each "check" is written as its own executable file so bash array
// expansion doesn't try to word-split shell strings (the natural
// failure mode of expressing checks as bash command strings is that
// spaces and quotes don't survive `"${CHECKS[@]}"`).
function writeSynth(checkBodies: { name: string; body: string }[]): string {
const dir = mkdtempSync(join(tmpdir(), "verify-synth-"));
const checkPaths: string[] = [];
for (const c of checkBodies) {
const p = `${dir}/${c.name}.sh`;
writeFileSync(p, `#!/usr/bin/env bash\n${c.body}\n`, { mode: 0o755 });
checkPaths.push(p);
}
const dispatcher = `${dir}/dispatch.sh`;
// CHECKS is an array of absolute paths to executable scripts.
const arrLit = checkPaths.map((p) => `"${p}"`).join(" ");
writeFileSync(
dispatcher,
`#!/usr/bin/env bash
set -uo pipefail
LOG_DIR=$(mktemp -d /tmp/verify-synth-log-XXXXXX)
trap 'rm -rf "$LOG_DIR"' EXIT
CHECKS=(${arrLit})
PIDS=()
NAMES=()
for c in "\${CHECKS[@]}"; do
base=$(basename "$c" .sh)
NAMES+=("$base")
(
"$c" > "$LOG_DIR/$base.log" 2>&1
echo $? > "$LOG_DIR/$base.exit"
) &
PIDS+=($!)
done
for p in "\${PIDS[@]}"; do wait "$p" 2>/dev/null || true; done
FAIL=0
FAILED_NAMES=""
for i in "\${!NAMES[@]}"; do
n="\${NAMES[$i]}"
rc=$(cat "$LOG_DIR/$n.exit")
if [ "$rc" != "0" ]; then
FAIL=$((FAIL+1))
FAILED_NAMES="$FAILED_NAMES $n"
echo "--- $n (rc=$rc) ---" >&2
tail -10 "$LOG_DIR/$n.log" >&2
fi
done
if [ "$FAIL" -gt 0 ]; then
echo "Failed:$FAILED_NAMES" >&2
exit 1
fi
exit 0
`,
{ mode: 0o755 },
);
return dispatcher;
}
function cleanup(dispatcher: string) {
rmSync(dispatcher.replace(/\/dispatch\.sh$/, ""), { recursive: true, force: true });
}
it("all-pass: exit 0, no failure block", () => {
const d = writeSynth([
{ name: "alpha", body: "exit 0" },
{ name: "beta", body: "exit 0" },
{ name: "gamma", body: "exit 0" },
]);
try {
const r = spawnSync("bash", [d], { encoding: "utf8" });
expect(r.status).toBe(0);
expect(r.stderr).not.toContain("Failed:");
} finally {
cleanup(d);
}
});
it("one-fails: exit 1, failure block names the check + shows log tail", () => {
const d = writeSynth([
{ name: "alpha", body: "exit 0" },
{
name: "beta",
body: 'echo "synthetic failure detail line" >&2\nexit 7',
},
{ name: "gamma", body: "exit 0" },
]);
try {
const r = spawnSync("bash", [d], { encoding: "utf8" });
expect(r.status).toBe(1);
expect(r.stderr).toContain("Failed: beta");
expect(r.stderr).toContain("synthetic failure detail line");
expect(r.stderr).toMatch(/--- beta \(rc=7\) ---/);
} finally {
cleanup(d);
}
});
it("two-fail: both names appear in the Failed: list", () => {
const d = writeSynth([
{ name: "alpha", body: "exit 0" },
{ name: "beta", body: "echo err-A >&2\nexit 1" },
{ name: "gamma", body: "echo err-B >&2\nexit 2" },
]);
try {
const r = spawnSync("bash", [d], { encoding: "utf8" });
expect(r.status).toBe(1);
expect(r.stderr).toContain("err-A");
expect(r.stderr).toContain("err-B");
expect(r.stderr).toMatch(/Failed:\s+beta\s+gamma/);
} finally {
cleanup(d);
}
});
});