mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 18:28:15 +01:00
v1.94.26.0 gstack-evidence: sum sharded bun lanes; docs: dispatch-carry warning and record-ci recheck order (GSTA-258 L6 follow-up) (#3934)
* feat(pregate): a guard that already passed at this tree carries from CI or the ledger (GST-1186)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(ci-carry): jobs-mapped lanes, aliases, narrow-lane counts, --simulate proof, policy-base check on carried (GST-1189, GST-1190)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ci-carry): a file whose every test skipped on CI is not carried; tree.json records skipped_files (GST-1190)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(ci-carry): path-skipped jobs count only under an unchanged gate; find --policy-from head --simulate (GST-1190)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ship: name the pre-push guards in Step 17 and the early push (moved from #3923 per GSTA-258)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci-carry: per-file lane carry for runner-skipped files; review fixes (gate inputs, passed ignores CI rows, simulate consent, pull_request-only skips, guard not-owed)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ship: Step 17 names the pre-push guards without a code this branch cannot print
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: gstack's bun run test lane runs its runner-skipped files with per_file
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci-carry: a per_file remainder must run tests here and the carried runs must still stand (l6c-p2)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci-carry: read record-ci from its own last record line; the remainder's count must come from this run's row (l6c-p2 red-team)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(ci-tree-reuse): type the skipped_files fixture for the test typecheck ratchet
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* Revert "pregate: gstack's bun run test lane runs its runner-skipped files with per_file"
This reverts commit fafbae5b9b.
* ci-carry: ship the skipped_files reader inert; gstack's writer and per_file move to the adoption PR; Step 17 keeps its refs/heads marker
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: guard carry, per_file and the too-old message in pregate and troubleshooting (docs audit l6cdocs2)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* gstack-evidence record-ci: recheck the working tree after the post-remainder run recheck (#3929 second fix-check)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* gstack-evidence: sum bun --shard processes in a lane's tests_ran; docs: dispatch-carry warning and record-ci recheck order (GBEV-3, GSTA-258 L6 follow-up)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* gstack-evidence: keep shard and count lines that leave the output tail; sum only shards 1..N in order; soften the dispatch-carry mechanism claim (l6f-p1 fixes)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci-carry: a pull_request run whose base moved names the one-command dispatch fix (TRUT-7)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: the tree_mismatch refusal names its fix command (l6fdocs3)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci-carry: a pull_request run that recorded its merge tree names the base-moved fix (the real tree_mismatch path; #3934 fix-check)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
b2ba100781
commit
7caca3322e
10 files changed
+108
-25
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.94.26.0] - 2026-11-03
|
||||
|
||||
- A lane whose command runs several `bun test --shard=i/N` processes now records the sum of the shards' counts, not the last shard's (GBEV-3, gbrain-evals#317: 3336 recorded against 6,095 run across 2 shards). The sum needs shards 1/N to N/N, in order, at the end of the log; any other set records the last summary as before, so a command that runs a suite twice, or reruns one shard, still reports its last run. The shard lines and count lines that scroll out of the 64 KiB output tail are kept for this, so a long shard run still sums (a real 2-shard, 2000-test bun run recorded 1000 before and 2000 after).
|
||||
- docs/pregate.md#dispatch-carry warns that a `workflow_dispatch` run reports under the same job names on the head commit, so without strict branch protection a green dispatch run can hide a red merge-commit check. It recommends strict protection or distinct job names for dispatch runs, and notes that gstack's own merge trains test the merged tree.
|
||||
- When a pull_request run tested a merge whose base moved after the run, `CI_CARRY: none (… tree_mismatch …)` now says so and prints the one-command fix: `gh workflow run <workflow> --ref <branch>, then gstack-ci-carry wait` (TRUT-7).
|
||||
- docs/pregate.md#skipped-files names the order of record-ci's checks after a skipped-file remainder, including the final `tree_changed` recheck.
|
||||
|
||||
### Tests
|
||||
- `test/ship-ci-carry.test.ts`: a pull_request run whose merge commit has another tree prints the base-moved reason and `gh workflow run free.yml --ref feat, then gstack-ci-carry wait`. It fails without the change.
|
||||
- `test/evidence.test.ts`: two `--shard=i/2` processes in one log record 6095, two shards with more than 64 KiB of per-test lines record 4000, and an incomplete, repeated or rerun shard set records the last summary. The sum cases fail without the change (3336 and 2000).
|
||||
|
||||
## [1.94.25.0] - 2026-11-03
|
||||
|
||||
- A lane label with a space or a colon, such as `pnpm lint` or `db:test`, no longer goes MISSING right after it carries (TRUT-7, truther#1170). `gstack-evidence` writes a label to the ledger with unsafe characters turned into `_`, but `check`, which `gstack-ship-release finish` and `accept` call, looked it up under the raw label, so /ship stopped with "not FRESH at the pushed head". `run`, `record-ci` and `check` now share one label spelling.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.94.25.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.94.26.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+34
-11
@@ -37,7 +37,9 @@
|
||||
* the tail is the stdout tail then the stderr tail, so a line's position says
|
||||
* nothing about when it was printed across streams, and bun's own counts (on
|
||||
* stderr) would otherwise outrank a 10k-test run whichever of the two ran
|
||||
* first. Any summary when it did not speak follows the last-summary-wins rule;
|
||||
* first. Any summary when it did not speak follows the last-summary-wins rule,
|
||||
* except that a complete set of `bun test --shard=i/N` processes ending the log
|
||||
* is summed (GBEV-3);
|
||||
* anything else is "unknown".
|
||||
* Three states, never merged:
|
||||
* a count, 0 (ZERO-RUN, graded like STALE by `check`) and unknown (non-blocking).
|
||||
@@ -146,18 +148,30 @@ const TAIL_MAX_BYTES = 64 * 1024;
|
||||
* Ring of the last TAIL_MAX_BYTES of each child stream, kept per stream: the
|
||||
* pumps interleave by arrival, so a stderr summary read while stdout still has
|
||||
* pipe-buffered bytes behind it would otherwise be pushed out of a shared ring.
|
||||
* Lines leaving the ring that bun's shard sum reads (a "--shard=i/N: running"
|
||||
* line, " N pass", " N fail", "Ran N tests across") are kept in order ahead of
|
||||
* the tail, so every shard of a long sharded run still counts (GBEV-3).
|
||||
*/
|
||||
class OutputTail {
|
||||
private readonly decoders = { stdout: new TextDecoder("utf-8"), stderr: new TextDecoder("utf-8") };
|
||||
private readonly text = { stdout: "", stderr: "" };
|
||||
private readonly kept = { stdout: [] as string[], stderr: [] as string[] };
|
||||
private trim(origin: "stdout" | "stderr", text: string): string {
|
||||
const cut = text.lastIndexOf("\n", text.length - TAIL_MAX_BYTES);
|
||||
if (cut < 0) return text.slice(-TAIL_MAX_BYTES);
|
||||
const lines = text.slice(0, cut).split("\n").filter((l) => /^(?:--shard=\d+\/\d+: running |\s*\d+ (?:pass|fail)\s*$|Ran \d+ tests? across )/.test(strip(l)));
|
||||
this.kept[origin] = [...this.kept[origin], ...lines].slice(-4096);
|
||||
return text.slice(cut + 1);
|
||||
}
|
||||
push(chunk: Uint8Array, origin: "stdout" | "stderr"): void {
|
||||
this.text[origin] += this.decoders[origin].decode(chunk, { stream: true });
|
||||
if (this.text[origin].length > TAIL_MAX_BYTES * 2) this.text[origin] = this.text[origin].slice(-TAIL_MAX_BYTES);
|
||||
if (this.text[origin].length > TAIL_MAX_BYTES * 2) this.text[origin] = this.trim(origin, this.text[origin]);
|
||||
}
|
||||
end(): string {
|
||||
const tails = (["stdout", "stderr"] as const).map((origin) => {
|
||||
const text = this.text[origin] + this.decoders[origin].decode();
|
||||
return text.length > TAIL_MAX_BYTES ? text.slice(-TAIL_MAX_BYTES) : text;
|
||||
const tail = text.length > TAIL_MAX_BYTES ? this.trim(origin, text) : text;
|
||||
return [...this.kept[origin], tail].join("\n");
|
||||
});
|
||||
return tails.join("\n");
|
||||
}
|
||||
@@ -172,7 +186,10 @@ const count = (text: string, word: string): number => Number(new RegExp(`(\\d+)
|
||||
|
||||
/**
|
||||
* Bun: the last "Ran N tests across M files" block; executed = its " N pass" +
|
||||
* " N fail" lines. Bun always prints that counts block (" 0 pass" / " 0 fail"
|
||||
* " N fail" lines. When that block ends a `bun test --shard=i/N` process (bun's
|
||||
* "--shard=i/N: running" line since the previous block), the blocks of shards
|
||||
* 1/N to N/N, in that order, that end the log are summed (a sharded runner prints
|
||||
* every shard, GBEV-3); any other shard set counts the last block alone. Bun always prints that counts block (" 0 pass" / " 0 fail"
|
||||
* even for an all-skip or empty run), so a terminal line with no " N pass"
|
||||
* above it is a line gstack's quiet shard runner forwarded (it forwards the
|
||||
* terminal line without the " N pass" line), not a bun summary:
|
||||
@@ -189,7 +206,9 @@ async function bunTestsRan(lines: string[]): Promise<{ at: number; ran: number }
|
||||
} catch {
|
||||
warn("shard-engine classifier not importable; using the built-in bun summary parser");
|
||||
}
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const blocks: { at: number; ran: number; shard?: string }[] = [];
|
||||
let since = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!isTerminal(lines[i])) continue;
|
||||
let pass: number | null = null;
|
||||
let fail = 0;
|
||||
@@ -200,13 +219,17 @@ async function bunTestsRan(lines: string[]): Promise<{ at: number; ran: number }
|
||||
if (p) pass = Number(p[1]);
|
||||
if (f) fail = Number(f[1]);
|
||||
}
|
||||
if (pass === null) {
|
||||
if (/^Ran 0 tests? across/.test(lines[i])) return { at: i, ran: 0 };
|
||||
continue;
|
||||
}
|
||||
return { at: i, ran: pass + fail };
|
||||
const shard = lines.slice(since, i).map((l) => /^--shard=(\d+\/\d+): running /.exec(l)?.[1]).findLast(Boolean);
|
||||
since = i + 1;
|
||||
if (pass !== null) blocks.push({ at: i, ran: pass + fail, shard });
|
||||
else if (/^Ran 0 tests? across/.test(lines[i])) blocks.push({ at: i, ran: 0 });
|
||||
}
|
||||
return null;
|
||||
const last = blocks.at(-1);
|
||||
if (!last) return null;
|
||||
const total = Number(last.shard?.split("/")[1] ?? 1);
|
||||
const set = blocks.slice(-total);
|
||||
const complete = total > 1 && set.length === total && set.every((b, k) => b.shard === `${k + 1}/${total}`);
|
||||
return { at: last.at, ran: complete ? set.reduce((n, b) => n + b.ran, 0) : last.ran };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+22
-5
@@ -765,10 +765,12 @@ once:
|
||||
such as `"per_file": "bun test --timeout=30000 {files}"`, the same form as the
|
||||
tests stage's `runner`). That run is recorded under `<label>.skipped`, the
|
||||
`LANE:` line adds `skipped_here=<n> file(s) tests_ran_here=<n>`, and the CI row
|
||||
is written only after it passes with a test count above zero and every carried
|
||||
run still stands (`run_changed` otherwise). A failure there is
|
||||
`skipped_failed` and a run of zero tests is `skipped_zero_run`; either way the
|
||||
whole lane runs here. An entry with no `per_file` refuses as
|
||||
is written only after four checks pass. In order, they are the remainder's
|
||||
exit (`skipped_failed`), its test count above zero (`skipped_zero_run`), every
|
||||
carried run again (`run_changed`), and the working tree once more
|
||||
(`tree_changed`, for an edit made during those checks). An edit made while the
|
||||
remainder itself runs leaves its ledger row without a tree, so it shows as
|
||||
`skipped_zero_run`. Any of these refusals runs the whole lane here. An entry with no `per_file` refuses as
|
||||
`inventory_skipped`, naming the files, and the lane runs here whole. A partly
|
||||
skipped file ran tests and stays covered. gstack's own Free Tests writer does
|
||||
not emit the list yet: it starts in the same follow-up pull request that
|
||||
@@ -1014,12 +1016,27 @@ a branch that does not contain the base tip it tested another tree and
|
||||
`CI_CARRY` says `none (no_verified_run: … tree_mismatch)`: dispatch Free Tests
|
||||
on the branch
|
||||
(`gh workflow run free-tests.yml --ref <branch>`), which tests HEAD's own tree,
|
||||
and wait again. The preflight stage (generated-file freshness, secrets, strays
|
||||
and wait again. When the pull_request run tested that merge commit, the
|
||||
refusal names the command for the run's branch (`fix: gh workflow run
|
||||
free-tests.yml --ref <branch>, then gstack-ci-carry wait`). The preflight stage (generated-file freshness, secrets, strays
|
||||
and the other mechanical checks) and the sealed guard are unchanged and always
|
||||
run here. The macOS and Windows lane
|
||||
obligations still clear with `gstack-pregate clear --remote <lane> --run-url
|
||||
<url>` from their CI runs.
|
||||
|
||||
<a id="dispatch-carry"></a>**A dispatched run can hide a red merge commit.** A
|
||||
`workflow_dispatch` run reports its checks on the head commit under the same
|
||||
job names as the `pull_request` run. So a green dispatch run of HEAD's own tree
|
||||
can stand in for a required check whose `pull_request` run failed on the merge
|
||||
commit, and without strict branch
|
||||
protection ("Require branches to be up to date before merging") the pull
|
||||
request can merge with that failure hidden (GBEV-3). Turn on strict protection
|
||||
for the base branch, or give dispatch runs distinct job names (for example a
|
||||
job `name:` that includes `github.event_name`), so a dispatch run never stands
|
||||
in for the required `pull_request` check. gstack's own merge trains test the
|
||||
merged tree before they merge, so gstack is covered; a repo that merges
|
||||
without a train is not.
|
||||
|
||||
**/ship lanes: `ship_lanes_from`.** A lane carries only when the base branch's
|
||||
`.gstack/pregate.json` maps its exact command (the evidence ledger binds the
|
||||
command's sha256; a label alone never carries) to the profiles whose runs carry
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ function newestVerified(src: RunSource, name: string, profile: RunProfile, head:
|
||||
for (const run of listed) {
|
||||
const v = verifyRun(src.deps, { repo: src.repo, runRef: String(run.id), profileName: name, profile, vetoHeads: [head], now: src.now, unchanged: src.unchanged });
|
||||
if (v.ok) return v;
|
||||
refusals.push(`run ${run.id}: ${v.reason}`);
|
||||
refusals.push(`run ${run.id}: ${v.reason}${v.fix ? ` (the pull request's base moved after the run; fix: ${v.fix})` : ''}`);
|
||||
}
|
||||
return no('no_verified_run', `${name}: ${refusals.join('; ')}`);
|
||||
}
|
||||
|
||||
+11
-5
@@ -51,7 +51,7 @@ export interface RemoteGateRuns { profiles: string[]; inventory: string; skipped
|
||||
export interface ApiRun {
|
||||
id: number; run_attempt: number; status: string; conclusion: string | null; event: string; head_sha: string;
|
||||
path?: string; created_at?: string; run_started_at?: string; updated_at?: string;
|
||||
repository?: { full_name: string } | null; head_repository?: { full_name: string } | null;
|
||||
repository?: { full_name: string } | null; head_repository?: { full_name: string } | null; head_branch?: string | null;
|
||||
}
|
||||
export interface ApiJob { name: string; conclusion: string | null; completed_at?: string | null }
|
||||
export interface TreeRecord { run_id?: unknown; run_attempt?: unknown; event?: unknown; sha?: unknown; head_sha?: unknown; tree?: unknown; reused?: unknown; reuse?: unknown; os?: unknown; files?: unknown; tests?: unknown; skipped_files?: unknown }
|
||||
@@ -77,7 +77,8 @@ export type RunRefusal = 'run_ref_invalid' | 'other_repository' | 'fork_run' | '
|
||||
| 'artifact_reused' | 'tree_mismatch' | 'newer_run_vetoes' | 'run_changed' | 'run_stale' | 'os_mismatch' | 'api_error';
|
||||
/** `skipped`: the files whose every test skipped on the runner (tree.json `skipped_files`, GST-1190); undefined when an older tree.json does not say. */
|
||||
export interface RunEvidence { run_id: number; run_attempt: number; head_sha: string; tree: string; profile: string; event: string; finished_at: string; files: string[]; tests: number | null; skipped?: string[]; skipped_jobs?: string[] }
|
||||
export type RunVerdict = { ok: true; evidence: RunEvidence } | { ok: false; reason: RunRefusal; detail: string };
|
||||
/** `fix`: the one command that clears a refusal the caller can repair, such as a pull_request run that tested a merge with an older base. */
|
||||
export type RunVerdict = { ok: true; evidence: RunEvidence } | { ok: false; reason: RunRefusal; detail: string; fix?: string };
|
||||
|
||||
const SHA = /^[0-9a-f]{40}$/;
|
||||
const RUN_EVENTS = ['push', 'workflow_dispatch', 'pull_request'];
|
||||
@@ -145,7 +146,7 @@ export function runProfileErrors(profiles: unknown, gateRuns: unknown): Array<{
|
||||
return errors;
|
||||
}
|
||||
|
||||
const fail = (reason: RunRefusal, detail: string): RunVerdict => ({ ok: false, reason, detail });
|
||||
const fail = (reason: RunRefusal, detail: string, fix?: string): RunVerdict => ({ ok: false, reason, detail, ...(fix ? { fix } : {}) });
|
||||
/** When a run's newest attempt started. A run vetoes the candidate when it started at or after the second the candidate was created: a red re-run of an older run counts, and re-running the candidate does not hide a failure in between. */
|
||||
const started = (r: ApiRun): string => r.run_started_at ?? r.created_at ?? '';
|
||||
export const normalPath = (f: string): string => f.replace(/^(\.\/)+/, '');
|
||||
@@ -209,12 +210,17 @@ export function verifyRun(deps: RunDeps, o: { repo: string; runRef: string; prof
|
||||
if (wrong.length) return fail('artifact_mismatch', `${at}'s ${p.artifact} records ${wrong.map(([k, got, want]) => `${k} ${String(got)} (expected ${want})`).join(', ')}`);
|
||||
const headTree = deps.commitTree(run.head_sha);
|
||||
if (!headTree) return fail('api_error', `git cannot read the tree of ${run.head_sha}`);
|
||||
if (rec.tree !== headTree) return fail('tree_mismatch', `${at} recorded tree ${String(rec.tree)}, but its head ${run.head_sha.slice(0, 12)} has tree ${headTree}`);
|
||||
const baseMoved = `gh workflow run ${p.workflow} --ref ${run.head_branch || '<branch>'}, then gstack-ci-carry wait`;
|
||||
if (rec.tree !== headTree) {
|
||||
const testedMerge = pr && typeof rec.sha === 'string' && SHA.test(rec.sha) && deps.commitTree(rec.sha) === rec.tree;
|
||||
if (testedMerge) return fail('tree_mismatch', `${at} tested merge commit ${(rec.sha as string).slice(0, 12)} (tree ${String(rec.tree)}), not its head ${run.head_sha.slice(0, 12)}'s tree ${headTree}: the pull request was behind its base (the base moved after the run, or the branch lacks its tip)`, baseMoved);
|
||||
return fail('tree_mismatch', `${at} recorded tree ${String(rec.tree)}, but its head ${run.head_sha.slice(0, 12)} has tree ${headTree}`);
|
||||
}
|
||||
if (pr) {
|
||||
if (typeof rec.sha !== 'string' || !SHA.test(rec.sha)) return fail('artifact_mismatch', `${at}'s ${p.artifact} names no merge commit (sha) for its pull_request run`);
|
||||
const mergeTree = deps.commitTree(rec.sha);
|
||||
if (!mergeTree) return fail('api_error', `git cannot read the tree of merge commit ${rec.sha}`);
|
||||
if (mergeTree !== headTree) return fail('tree_mismatch', `${at} tested merge commit ${rec.sha.slice(0, 12)} (tree ${mergeTree}), not its head ${run.head_sha.slice(0, 12)}'s tree ${headTree}: the pull request was behind its base`);
|
||||
if (mergeTree !== headTree) return fail('tree_mismatch', `${at} tested merge commit ${rec.sha.slice(0, 12)} (tree ${mergeTree}), not its head ${run.head_sha.slice(0, 12)}'s tree ${headTree}: the pull request was behind its base (the base moved after the run, or the branch lacks its tip)`, baseMoved);
|
||||
}
|
||||
const oses = Array.isArray(rec.os) ? rec.os : [rec.os];
|
||||
if (p.os && !oses.includes(p.os)) return fail('os_mismatch', `${at} ran on ${oses.filter(Boolean).map(String).join(', ') || 'an unrecorded OS'}; ${o.profileName} requires ${p.os}`);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.94.25",
|
||||
"version": "1.94.26",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -495,6 +495,24 @@ describe('gstack-evidence tests_ran — zero-run green is a third state, never m
|
||||
expect(chk.stdout).not.toContain('STALE');
|
||||
});
|
||||
|
||||
test('bun --shard processes in one log record their sum; an incomplete or repeated shard set falls back to the last summary', () => {
|
||||
// GBEV-3 (gbrain-evals#317): scripts/test-shards.ts prints each `bun test --shard=i/N` process's whole output, then its own
|
||||
// total. The lane recorded 3336 (the last shard) against 6095 run across 2 shards.
|
||||
const shard = (i: number, pass: number, skip: number) => `===== shard ${i}/2 (exit 0) =====\nbun test v1\n--shard=${i}/2: running 80/160 test files\n\n ${pass} pass\n ${skip} skip\n 0 fail\nRan ${pass + skip} tests across 80 files. [10.00ms]\n`;
|
||||
const log = (name: string, text: string) => { const p = path.join(gstackHome, name); fs.writeFileSync(p, text); return `cat '${p}' >&2`; };
|
||||
run(['run', '--label', 'tests', '--', log('shards.out', shard(1, 2759, 20) + shard(2, 3336, 18) + '\n2 shards: 6095 pass, 38 skip, 0 fail in 400s\n')]);
|
||||
expect(records().pop().tests_ran).toBe(6095);
|
||||
run(['run', '--label', 'tests', '--', log('one.out', shard(2, 3336, 18))]);
|
||||
expect(records().pop().tests_ran).toBe(3336);
|
||||
run(['run', '--label', 'tests', '--', log('again.out', shard(2, 9, 0) + shard(2, 3336, 18))]);
|
||||
expect(records().pop().tests_ran).toBe(3336);
|
||||
run(['run', '--label', 'tests', '--', log('rerun.out', shard(1, 2759, 20) + shard(2, 3336, 18) + shard(1, 4, 0))]);
|
||||
expect(records().pop().tests_ran).toBe(4);
|
||||
const big = (i: number) => `bun test v1\n--shard=${i}/2: running 20/40 test files\n` + Array.from({ length: 2000 }, (_, k) => `(pass) case ${k} padded test name for size [0.01ms]`).join('\n') + '\n\n 2000 pass\n 0 fail\nRan 2000 tests across 20 files. [10.00ms]\n';
|
||||
run(['run', '--label', 'tests', '--', log('big.out', big(1) + big(2))]);
|
||||
expect(records().pop().tests_ran).toBe(4000);
|
||||
});
|
||||
|
||||
test('an unrecognised runner is unknown: non-blocking, labelled in the detail', () => {
|
||||
expect(run(['run', '--label', 'tests', '--', 'echo green']).status).toBe(0);
|
||||
expect(records().pop().tests_ran).toBe('unknown');
|
||||
|
||||
@@ -311,6 +311,14 @@ describe('GST-1189: a lane carries by named CI jobs instead of an inventory', ()
|
||||
expect(find(f).stdout).toContain('CI_CARRY: none (no_verified_run: ci/free: run 7: tree_mismatch)');
|
||||
});
|
||||
|
||||
test('a pull_request run whose base moved names the one-command fix (TRUT-7)', () => {
|
||||
const f = gbrain();
|
||||
const mergeTree = f.git('rev-parse', 'origin/main^{tree}').stdout.trim();
|
||||
const merge = f.git('commit-tree', mergeTree, '-p', 'HEAD', '-p', 'origin/main', '-m', 'Merge origin/main into feat').stdout.trim();
|
||||
world(f, { jobs: ALL, run: { event: 'pull_request', head_branch: 'feat' }, tree: { ...NO_FILES, tree: mergeTree, sha: merge } });
|
||||
expect(find(f).stdout).toContain("CI_CARRY: none (no_verified_run: ci/free: run 7: tree_mismatch (the pull request's base moved after the run; fix: gh workflow run free.yml --ref feat, then gstack-ci-carry wait))");
|
||||
});
|
||||
|
||||
test('an entry declares exactly one of inventory or jobs', () => {
|
||||
const both = shipFixture({ ...BASE, '.gstack/pregate.json': pregateJson({ jobs: ['verify'], inventory: 'printf x' }) }); fixtures.push(both);
|
||||
world(both, { jobs: ALL, tree: NO_FILES });
|
||||
|
||||
Reference in new issue
Block a user