Merge remote-tracking branch 'origin/main' into gsta208/ship-receipt-session-kind

This commit is contained in:
garrytan committed 2026-11-02 01:22:33 +00:00
commit 0b997e046d
60 files changed
+1188 -226

No files matched your search

+150 -26
View File
@@ -843,7 +843,7 @@ describe('gstack-brain-sync per-file hold', () => {
}
function setup() {
init();
write(SHARED, '# shared v1\n');
write(SHARED, '{"note":"shared v1"}\n');
write(OTHER, '# other v1\n');
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('ok');
@@ -856,15 +856,15 @@ describe('gstack-brain-sync per-file hold', () => {
test('a dirty tracked file the remote did not touch rides through the merge', () => {
setup();
peerCommit(SHARED, '# shared from the peer\n');
peerCommit(SHARED, '{"note":"shared from the peer"}\n');
write(OTHER, '# other, local edit not queued\n', false);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('ok');
expect(remoteFiles()).toContain(CLEAN);
expect(remoteShow(SHARED).stdout).toBe('# shared from the peer\n');
expect(remoteShow(SHARED).stdout).toBe('{"note":"shared from the peer"}\n');
expect(fs.readFileSync(path.join(tmpHome, OTHER), 'utf-8')).toBe('# other, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared from the peer\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared from the peer"}\n');
});
/** The restore command the status names, run as printed. */
@@ -878,17 +878,17 @@ describe('gstack-brain-sync per-file hold', () => {
test('a dirty tracked file the remote also changed no longer leaves the push failed', () => {
setup();
peerCommit(SHARED, '# shared from the peer\n');
write(SHARED, '# shared, local edit not queued\n', false);
peerCommit(SHARED, '{"note":"shared from the peer"}\n');
write(SHARED, '{"note":"shared, local edit not queued"}\n', false);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
const s = statusJson();
expect(s.status).toBe('ok');
expect(remoteFiles()).toContain(CLEAN);
expect(remoteShow(SHARED).stdout).toBe('# shared from the peer\n');
expect(remoteShow(SHARED).stdout).toBe('{"note":"shared from the peer"}\n');
// The merged version stays in the working tree: no conflict markers,
// nothing staged, and nothing stale for a later sync to publish.
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared from the peer\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared from the peer"}\n');
expect(unmerged()).toBe('');
expect(staged()).toBe('');
expect(s.message).toContain(`local edits to ${SHARED} conflicted`);
@@ -900,23 +900,23 @@ describe('gstack-brain-sync per-file hold', () => {
write(OTHER, '# other v2\n');
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('ok');
expect(remoteShow(SHARED).stdout).toBe('# shared from the peer\n');
expect(git(['show', `${sha}:${SHARED}`]).stdout).toBe('# shared, local edit not queued\n');
expect(remoteShow(SHARED).stdout).toBe('{"note":"shared from the peer"}\n');
expect(git(['show', `${sha}:${SHARED}`]).stdout).toBe('{"note":"shared, local edit not queued"}\n');
// The printed command brings the local edit back.
expect(recover(s.message)).toBe(sha!);
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared, local edit not queued"}\n');
});
test('a local deletion of a file the remote changed keeps the remote file and stashes the deletion', () => {
setup();
peerCommit(SHARED, '# shared from the peer\n');
peerCommit(SHARED, '{"note":"shared from the peer"}\n');
fs.rmSync(path.join(tmpHome, SHARED));
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
const s = statusJson();
expect(s.status).toBe('ok');
expect(remoteShow(SHARED).stdout).toBe('# shared from the peer\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared from the peer\n');
expect(remoteShow(SHARED).stdout).toBe('{"note":"shared from the peer"}\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared from the peer"}\n');
expect(unmerged()).toBe('');
expect(s.message).toContain(`local edits to ${SHARED} conflicted`);
recover(s.message);
@@ -934,7 +934,7 @@ describe('gstack-brain-sync per-file hold', () => {
} finally {
fs.rmSync(peer, { recursive: true, force: true });
}
write(SHARED, '# shared, local edit not queued\n', false);
write(SHARED, '{"note":"shared, local edit not queued"}\n', false);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
const s = statusJson();
@@ -943,7 +943,7 @@ describe('gstack-brain-sync per-file hold', () => {
expect(fs.existsSync(path.join(tmpHome, SHARED))).toBe(false);
expect(unmerged()).toBe('');
recover(s.message);
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared, local edit not queued"}\n');
});
test('a conflicting file whose name looks like a pattern resets only itself', () => {
@@ -954,16 +954,16 @@ describe('gstack-brain-sync per-file hold', () => {
expect(git(['merge', '-q', '--ff-only', 'origin/main']).status).toBe(0);
peerCommit(GLOB, '# glob from the peer\n');
write(GLOB, '# glob, local edit not queued\n', false);
write(SHARED, '# shared, local edit not queued\n', false);
write(SHARED, '{"note":"shared, local edit not queued"}\n', false);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
const s = statusJson();
expect(s.status).toBe('ok');
expect(fs.readFileSync(path.join(tmpHome, GLOB), 'utf-8')).toBe('# glob from the peer\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared, local edit not queued"}\n');
recover(s.message);
expect(fs.readFileSync(path.join(tmpHome, GLOB), 'utf-8')).toBe('# glob, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared, local edit not queued"}\n');
});
test('a file that never checks out clean does not strand the other local edits', () => {
@@ -973,7 +973,7 @@ describe('gstack-brain-sync per-file hold', () => {
expect(drain().status).toBe(0);
fs.mkdirSync(path.join(tmpHome, '.git', 'info'), { recursive: true });
fs.writeFileSync(path.join(tmpHome, '.git', 'info', 'attributes'), '*.md text eol=lf\n');
peerCommit(SHARED, '# shared from the peer\n');
peerCommit(SHARED, '{"note":"shared from the peer"}\n');
write(OTHER, '# other, local edit not queued\n', false);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
@@ -987,28 +987,28 @@ describe('gstack-brain-sync per-file hold', () => {
setup();
write(OTHER, '# other, older stashed work\n', false);
expect(git(['stash', 'push', '-q', '-m', 'older work']).status).toBe(0);
peerCommit(SHARED, '# shared from the peer\n');
write(SHARED, '# shared, local edit not queued\n', false);
peerCommit(SHARED, '{"note":"shared from the peer"}\n');
write(SHARED, '{"note":"shared, local edit not queued"}\n', false);
write(CLEAN, '# a plan\n');
const shim = gitShim('case " $* " in *" stash push "*) exit 0;; esac');
expect(drain(shim.env).status).toBe(0);
expect(statusJson().status).toBe('push_failed');
expect(git(['stash', 'list']).stdout).toContain('older work');
expect(fs.readFileSync(path.join(tmpHome, OTHER), 'utf-8')).toBe('# other v1\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared, local edit not queued\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared, local edit not queued"}\n');
});
test('a conflicting merge is aborted and git\'s reason reaches the status', () => {
setup();
peerCommit(SHARED, '# shared from the peer\n');
write(SHARED, '# shared, local commit\n');
peerCommit(SHARED, '{"note":"shared from the peer"}\n');
write(SHARED, '{"note":"shared, local commit"}\n');
expect(drain().status).toBe(0);
const s = statusJson();
expect(s.status).toBe('push_failed');
expect(s.message).toContain(`Merge conflict in ${SHARED}`);
expect(mergeHead()).toBe(false);
expect(unmerged()).toBe('');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('# shared, local commit\n');
expect(fs.readFileSync(path.join(tmpHome, SHARED), 'utf-8')).toBe('{"note":"shared, local commit"}\n');
// The next drain is not blocked by a half-finished merge.
write(OTHER, '# other v2\n');
expect(drain().status).toBe(0);
@@ -1270,6 +1270,130 @@ describe('gstack-brain-sync per-file hold', () => {
expect(remoteShow(SNAP).stdout).toContain('"ccc"');
});
});
describe('a synced JSON file that does not parse is never committed (#3763)', () => {
const PROFILE = 'developer-profile.json';
const MARKED = '{\n "sessions": [],\n<<<<<<< HEAD\n "v": "local"\n=======\n "v": "peer"\n>>>>>>> origin/main\n}\n';
const parses = (text: string) => { try { JSON.parse(text); return true; } catch { return false; } };
test('a queued profile holding conflict markers is held back; the rest syncs; a repaired profile syncs next', () => {
init();
write(PROFILE, MARKED);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
expect(remoteFiles()).toContain(CLEAN);
expect(remoteFiles()).not.toContain(PROFILE);
expect(staged()).toBe('');
expect(spoolText()).toContain(PROFILE);
const s = statusJson();
expect(s.status).toBe('held');
expect(s.held.map((h: { path: string; rule: string }) => [h.path, h.rule])).toEqual([[PROFILE, 'invalid-json']]);
expect(s.held[0].fixes[0]).toContain(`${PROFILE} is not valid JSON`);
expect(s.held[0].fixes).toHaveLength(1);
expect(s.message).toContain('not valid JSON');
expect(s.message).not.toContain('secret scan');
write(PROFILE, '{"sessions":[],"v":"repaired"}\n', false);
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('ok');
expect(remoteShow(PROFILE).stdout).toBe('{"sessions":[],"v":"repaired"}\n');
});
test('a marker-free syntax error is held too', () => {
init();
write(PROFILE, '{"sessions": [],}\n');
expect(drain().status).toBe(0);
expect(remoteFiles()).not.toContain(PROFILE);
expect(statusJson().held.map((h: { rule: string }) => h.rule)).toEqual(['invalid-json']);
expect(spoolText()).toContain(PROFILE);
});
test('an unparseable decisions.active.json holds its coupled decisions.jsonl until repaired', () => {
init();
writeDecisions(['a'], ['a']);
expect(drain().status).toBe(0);
write(LOG, [decide('a'), decide('bb')].join('\n') + '\n', false);
write(SNAP, '[{"id":"a"},\n<<<<<<< HEAD\n{"id":"bb"}\n=======\n{"id":"c"}\n>>>>>>> origin/main\n]\n', false);
run(['gstack-brain-enqueue', LOG]);
expect(drain().status).toBe(0);
const s = statusJson();
expect(s.status).toBe('held');
expect(s.held.map((h: { path: string; rule: string; dependents: string[] }) => [h.path, h.rule, h.dependents])).toEqual([[SNAP, 'invalid-json', [LOG]]]);
expect(remoteShow(LOG).stdout).toBe(decide('a') + '\n');
expect(spoolText()).toContain(LOG);
writeDecisions(['a', 'bb'], ['a', 'bb']);
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('ok');
expect(remoteShow(LOG).stdout).toBe([decide('a'), decide('bb')].join('\n') + '\n');
});
test('two machines editing the profile never put conflict markers on the remote', () => {
init();
write(PROFILE, '{"sessions":[],"v":"base"}\n');
expect(drain().status).toBe(0);
// The retry merge commits with the ambient identity; a CI runner has none.
expect(git(['config', 'user.name', 'fixture']).status).toBe(0);
expect(git(['config', 'user.email', 'fixture@example.com']).status).toBe(0);
const peer = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-sync-peer-'));
try {
expect(git(['clone', '-q', bareRemote, peer]).status).toBe(0);
fs.writeFileSync(path.join(peer, PROFILE), '{"sessions":[],"v":"peer"}\n');
expect(git(['add', '-f', '--', PROFILE], peer).status).toBe(0);
expect(git(['-c', 'user.name=peer', '-c', 'user.email=peer@example.com', 'commit', '-qm', 'peer profile'], peer).status).toBe(0);
expect(git(['push', '-q', 'origin', 'HEAD'], peer).status).toBe(0);
} finally {
fs.rmSync(peer, { recursive: true, force: true });
}
write(PROFILE, '{"sessions":[],"v":"local"}\n');
expect(drain().status).toBe(0);
// Every later profile write queues the file again.
run(['gstack-brain-enqueue', PROFILE]);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
expect(parses(remoteShow(PROFILE).stdout)).toBe(true);
expect(remoteShow(PROFILE).stdout).not.toContain('<<<<<<<');
expect(remoteShow(PROFILE).stdout).toBe('{"sessions":[],"v":"peer"}\n');
});
test('an unfinished merge with a held profile commits nothing until the profile is repaired', () => {
init();
write(PROFILE, '{"sessions":[],"v":"base"}\n');
expect(drain().status).toBe(0);
expect(git(['config', 'user.name', 'fixture']).status).toBe(0);
expect(git(['config', 'user.email', 'fixture@example.com']).status).toBe(0);
const peer = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-sync-peer-'));
try {
expect(git(['clone', '-q', bareRemote, peer]).status).toBe(0);
fs.writeFileSync(path.join(peer, PROFILE), '{"sessions":[],"v":"peer"}\n');
expect(git(['add', '-f', '--', PROFILE], peer).status).toBe(0);
expect(git(['-c', 'user.name=peer', '-c', 'user.email=peer@example.com', 'commit', '-qm', 'peer profile'], peer).status).toBe(0);
expect(git(['push', '-q', 'origin', 'HEAD'], peer).status).toBe(0);
} finally {
fs.rmSync(peer, { recursive: true, force: true });
}
fs.writeFileSync(path.join(tmpHome, PROFILE), '{"sessions":[],"v":"local"}\n');
expect(git(['commit', '-qam', 'local profile']).status).toBe(0);
expect(git(['fetch', '-q', 'origin']).status).toBe(0);
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
expect(git(['merge', '--no-edit', `origin/${branch}`]).status).not.toBe(0);
run(['gstack-brain-enqueue', PROFILE]);
write(CLEAN, '# a plan\n');
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('blocked');
expect(statusJson().message).toContain('unfinished');
expect(statusJson().message).not.toContain('--skip-file <path>');
expect(remoteShow(PROFILE).stdout).toBe('{"sessions":[],"v":"peer"}\n');
expect(remoteFiles()).not.toContain(CLEAN);
expect(git(['rev-parse', '-q', '--verify', 'MERGE_HEAD']).status).toBe(0);
write(PROFILE, '{"sessions":[],"v":"merged"}\n', false);
expect(drain().status).toBe(0);
expect(statusJson().status).toBe('ok');
expect(remoteShow(PROFILE).stdout).toBe('{"sessions":[],"v":"merged"}\n');
expect(remoteFiles()).toContain(CLEAN);
});
});
});
// ---------------------------------------------------------------
+2 -2
View File
@@ -210,8 +210,8 @@ not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
raises it). Pass 1 reads the diff, later passes the range; the cap is two rounds,
fix-checks included. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+2 -2
View File
@@ -210,8 +210,8 @@ not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
raises it). Pass 1 reads the diff, later passes the range; the cap is two rounds,
fix-checks included. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+2 -2
View File
@@ -208,8 +208,8 @@ not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
raises it). Pass 1 reads the diff, later passes the range; the cap is two rounds,
fix-checks included. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+2 -2
View File
@@ -210,8 +210,8 @@ not a commit ID. Use `git diff <old-tree> <new-tree>` to compare these snapshots
**Review policy** (docs/review-policy.md): only a finding that would fail a test,
break production, or lose data or secrets blocks a pass; the rest is a PR-body
note. Step 9's plan sets both reviews' depth (`--depth-floor standard|full` only
raises it). Pass 1 reads the diff, later passes the range; the cap is two full
passes and a fix-check. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
raises it). Pass 1 reads the diff, later passes the range; the cap is two rounds,
fix-checks included. `NO FINDINGS` is complete. `ship: review_scope` (Step 3)
is the knob; the receipt's `review_loop` records the outcome.
### Ship control flow
+18
View File
@@ -628,6 +628,24 @@ describe('gstack-developer-profile --check-mismatch', () => {
// Error handling
// -----------------------------------------------------------------------
describe('gstack-developer-profile --profile on an unparseable profile (#3763)', () => {
test('exits non-zero, prints nothing to stdout, and names the file and the conflict markers', () => {
fs.writeFileSync(path.join(tmpHome, 'developer-profile.json'), '{\n<<<<<<< HEAD\n "sessions": []\n=======\n "sessions": [1]\n>>>>>>> origin/main\n}\n');
const r = runDev('--profile');
expect(r.status).not.toBe(0);
expect(r.stdout).toBe('');
expect(r.stderr).toContain('developer-profile.json is not valid JSON');
expect(r.stderr).toContain('git conflict markers');
});
test('a valid profile still prints as before', () => {
fs.writeFileSync(path.join(tmpHome, 'developer-profile.json'), '{"sessions":[]}\n');
const r = runDev('--profile');
expect(r.status).toBe(0);
expect(r.stdout).toBe('{"sessions":[]}\n');
});
});
describe('gstack-developer-profile errors', () => {
test('unknown subcommand exits non-zero', () => {
const r = runDev('--not-a-real-subcommand');
+19
View File
@@ -136,6 +136,25 @@ describe('gstack-doctor', () => {
expect(r.out).not.toContain('.brain-sync-status.json');
});
test('artifacts sync: a synced JSON file with conflict markers warns by name (#3763)', () => {
const f = makeFixture();
syncOn(f, { status: 'ok', message: 'pushed 3 file(s)', drainable: 0, last_drain_at: iso(60), last_push_at: iso(60) });
write(path.join(f.state, 'developer-profile.json'), '{\n<<<<<<< HEAD\n "v": 1\n=======\n "v": 2\n>>>>>>> origin/main\n}\n');
const r = doctor(f);
expect(r.row('artifacts sync').state).toBe('warn');
expect(r.row('artifacts sync').detail).toBe('developer-profile.json holds git conflict markers and does not parse');
write(path.join(f.state, 'developer-profile.json'), '{"v": 2}\n');
expect(doctor(f).row('artifacts sync').state).toBe('ok');
});
test('artifacts sync: a JSON hold is not attributed to the secret scanner (#3763)', () => {
const f = makeFixture();
syncOn(f, { status: 'held', message: 'held', held_count: 1, held: [{ path: 'developer-profile.json', rule: 'invalid-json', dependents: [], fixes: ['repair', 'skip'] }], drainable: 0, last_drain_at: iso(60), last_push_at: iso(60) });
const r = doctor(f);
expect(r.row('artifacts sync').state).toBe('warn');
expect(r.row('artifacts sync').detail).toBe('status held (1 synced file(s) are not valid JSON; 1 held in all)');
});
test('artifacts sync: held, then ok', () => {
const f = makeFixture();
syncOn(f, { status: 'held', message: 'held 2 file(s): sk-ant-secret', held_count: 2, held: [{ path: 'projects/p/notes.md', rule: 'anthropic_key', dependents: [], fixes: ['edit', 'skip'] }, { path: 'projects/p/b.md', rule: 'anthropic_key', dependents: [], fixes: ['edit', 'skip'] }], drainable: 0, last_drain_at: iso(60), last_push_at: iso(60) });
+1 -1
View File
@@ -816,7 +816,7 @@ describe('ARTIFACTS_SYNC attention lines (sync status table)', () => {
const held = runStart([], env);
expect(held).toMatch(/^ARTIFACTS_SYNC: mode=full \| last_push=\S+ \| queue=\d+$/m);
expect(attention(held)).toEqual([
`ARTIFACTS_SYNC: attention: status=held held=1. The secret scan is holding back 1 file(s); everything else still syncs. See which files and how to fix them: ${path.join(ROOT, 'bin', 'gstack-brain-sync')} --status`,
`ARTIFACTS_SYNC: attention: status=held held=1. Sync is holding back 1 file(s) that the secret scan flagged or that are not valid JSON; everything else still syncs. See which files and how to fix them: ${path.join(ROOT, 'bin', 'gstack-brain-sync')} --status`,
]);
expect(held).not.toContain('ghp_');
expect(remoteFiles()).toContain('projects/p/ceo-plans/clean.md');
+41
View File
@@ -0,0 +1,41 @@
/**
* #3659: gstack-jsonl-merge read its inputs as UTF-8 but wrote them with
* print(), so a redirected stdout on a legacy code page (cp1250 on the
* reporter's Windows) raised UnicodeEncodeError on a Cyrillic line, and any
* Windows Python rewrote LF as CRLF. The driver must write UTF-8 with LF
* whatever the platform and code page. PYTHONIOENCODING=cp1252 reproduces the
* legacy code page on every OS.
*
* Windows-safe by construction: the driver launches through
* lib/script-invocation.ts (Git Bash on win32), files live under os.tmpdir().
*/
import { afterEach, beforeEach, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { runScriptSync } from '../lib/script-invocation';
const ROOT = path.resolve(import.meta.dir, '..');
let tmp = '';
beforeEach(() => { tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-jsonl-enc-'))); });
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
test('a line the stdout code page cannot encode merges as UTF-8 with LF endings (#3659)', () => {
const base = '{"ts":"2026-01-01T00:00:00Z","note":"base"}\n';
const ours = base + '{"ts":"2026-01-02T00:00:00Z","note":"ours"}\n';
const theirsLine = '{"ts":"2026-01-03T00:00:00Z","note":"привет"}';
const p = (n: string) => path.join(tmp, n);
fs.writeFileSync(p('base'), base);
fs.writeFileSync(p('ours'), ours);
fs.writeFileSync(p('theirs'), base + theirsLine + '\n');
const posix = (f: string) => f.replace(/\\/g, '/');
const r = runScriptSync(path.join(ROOT, 'bin', 'gstack-jsonl-merge'), [posix(p('base')), posix(p('ours')), posix(p('theirs'))], {
env: { ...process.env, PYTHONIOENCODING: 'cp1252', PYTHONUTF8: '0' }, encoding: 'utf8', timeout: 60_000,
});
expect(r.error).toBeUndefined();
expect(r.status, r.stderr).toBe(0);
const bytes = fs.readFileSync(p('ours'));
expect(bytes.includes(0x0d)).toBe(false);
expect(bytes.toString('utf8')).toBe(ours + theirsLine + '\n');
});
+35
View File
@@ -201,6 +201,41 @@ describe('literals (pure)', () => {
{ name: 'MAX', old: '3', new: '4', file: 'f.ts' }, { name: 'timeout', old: '5000', new: '9000', file: 'f.ts' },
]);
});
// Value: protects=a sibling file that shares the module's stem (`postgres-unit-arms.txt` beside `postgres-unit-arms.ts`) is not the
// module, so a test that only names it is not a consumer, while an extensionless import, a JS/TS import extension, the module's own
// file name and a path ending at the stem (then a quote, space, bracket, sentence end or `.d.ts`) still are, and neither `<file>.snap` nor a JS twin of a
// shell module is; fails_when=`/<stem>` again accepts any following `.<word>`, the file name accepts a further extension,
// a non-JS module takes JS/TS extensions, or stops accepting a following space, bracket or sentence end
// (GST-820: 17 false files in gbrain #8372 on ARM_SHARDS 12 -> 18); why_new=no test had a same-stem sibling; seam=none;
// no_claim=name-gated hits (a file naming ARM_SHARDS still counts any 12)
test('consumersOf: a sibling file sharing the module stem is not the module; real imports of it still are (GST-820)', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-stem-'));
const nl = String.fromCharCode(10);
const files: Record<string, string> = {
'test/a.test.ts': ['// listed in test/postgres-unit-arms.txt', 'expect(n).toBe(12);', ''].join(nl),
'test/json.test.ts': ["const arms = read('fixtures/postgres-unit-arms.json');", 'expect(arms.length).toBe(12);', ''].join(nl),
'test/longer.test.ts': ["run('scripts/postgres-unit-arms-v2.ts');", 'expect(n).toBe(12);', ''].join(nl),
'test/import.test.ts': ["import { shards } from '../scripts/postgres-unit-arms';", 'expect(shards()).toBe(12);', ''].join(nl),
'test/import-js.test.ts': ["import { shards } from '../scripts/postgres-unit-arms.js';", 'expect(shards()).toBe(12);', ''].join(nl),
'test/own.test.ts': ["const out = run('scripts/postgres-unit-arms.ts');", 'expect(out).toBe(12);', ''].join(nl),
'test/line-end.test.ts': ['// covers scripts/postgres-unit-arms', 'expect(n).toBe(12);', ''].join(nl),
'test/prose.test.ts': ['// covers scripts/postgres-unit-arms and more', 'expect(n).toBe(12);', ''].join(nl),
'test/paren.test.ts': ['// (scripts/postgres-unit-arms)', 'expect(n).toBe(12);', ''].join(nl),
'test/sentence.test.ts': ['// see scripts/postgres-unit-arms.', 'expect(n).toBe(12);', ''].join(nl),
'test/dts.test.ts': ["import type { Arms } from '../scripts/postgres-unit-arms.d.ts';", 'expect(n).toBe(12);', ''].join(nl),
'test/sibling-test.test.ts': ["run('scripts/postgres-unit-arms.test.ts');", 'expect(n).toBe(12);', ''].join(nl),
'test/snap.test.ts': ["read('scripts/__snapshots__/postgres-unit-arms.ts.snap');", 'expect(n).toBe(12);', ''].join(nl),
'test/sh-twin.test.ts': ["import { arms } from '../lib/arms-tool.ts';", 'expect(n).toBe(12);', ''].join(nl),
'test/sh-own.test.ts': ["run('scripts/arms-tool.sh');", 'expect(n).toBe(12);', ''].join(nl),
};
for (const [rel, text] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), text); }
expect(consumersOf(root, Object.keys(files), { name: 'ARM_SHARDS', old: '12', new: '18', file: 'scripts/postgres-unit-arms.ts' })).toEqual([
'test/import.test.ts:2', 'test/import-js.test.ts:2', 'test/own.test.ts:2', 'test/line-end.test.ts:2',
'test/prose.test.ts:2', 'test/paren.test.ts:2', 'test/sentence.test.ts:2', 'test/dts.test.ts:2',
]);
expect(consumersOf(root, ['test/sh-twin.test.ts', 'test/sh-own.test.ts'], { name: 'ARMS', old: '12', new: '18', file: 'scripts/arms-tool.sh' })).toEqual(['test/sh-own.test.ts:2']);
fs.rmSync(root, { recursive: true, force: true });
});
test('consumersOf: a module stem is a path fragment, never a bare word, and a one-character old value needs the constant name (GSTA-114 flood: install.sh 2 -> 3 flagged 575 consumers)', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-'));
const nl = String.fromCharCode(10);
+46 -19
View File
@@ -44,8 +44,23 @@ describe('nextStep: the transition table', () => {
test('a fixed blocker earns full pass 2', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBlocking: true }).step).toBe('full'));
test('below-floor fixes earn one fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true }).step).toBe('fix-check'));
test('a fix touching gate code earns a full pass instead of a fix-check', () => expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true, touchedGate: true }).step).toBe('full'));
test('at the full-pass cap a fixed blocker gets a fix-check', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true }).step).toBe('fix-check'));
test('at the cap a gate-touching fix still gets a fix-check, never a third full pass', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true, touchedGate: true }).step).toBe('fix-check'));
// R51: two rounds is the cap, a fix-check included. A blocker open or fixed in round 2 stops for the owner (a grant may add
// one scoped fix-check); below-floor fixes in round 2 finish with notes; a light pass whose fixes change no runtime lines is done.
test('a blocker fixed in round 2 stops for the owner, never a third round', () => {
expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true })).toMatchObject({ step: 'stop', reason: expect.stringContaining('scoped fix-check') });
expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBlocking: true, touchedGate: true }).step).toBe('stop');
});
test('below-floor fixes in round 2 finish with notes, a fix-check included', () => {
expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', fixedBelow: true })).toMatchObject({ step: 'done', reason: expect.stringContaining('notes') });
expect(nextStep({ fullPasses: 1, last: 'fix-check', fixedBelow: true })).toMatchObject({ step: 'done', reason: expect.stringContaining('notes') });
expect(nextStep({ fullPasses: 1, last: 'fix-check', fixedBelow: true, fixedBlocking: true }).step).toBe('stop');
});
test('a light pass 1 whose below-floor fixes change no runtime lines is done; a runtime fix or a deeper diff keeps the fix-check', () => {
expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true, rangeRuntimeLines: 0 }, 'light').step).toBe('done');
expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true, rangeRuntimeLines: 3 }, 'light').step).toBe('fix-check');
expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true, rangeRuntimeLines: 0 }, 'full').step).toBe('fix-check');
expect(nextStep({ fullPasses: 1, last: 'full', fixedBelow: true }, 'light').step).toBe('fix-check');
});
test('a blocker open at the cap stops', () => expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', blockingOpen: true }).step).toBe('stop'));
test('a blocker open with no fix stops', () => expect(nextStep({ fullPasses: 1, last: 'full', blockingOpen: true }).step).toBe('stop'));
test('a fix-check that finds a blocker stops, never a third full pass', () => {
@@ -83,8 +98,12 @@ describe('planReview', () => {
expect(p.depth).toBe('light');
expect(roles(p)).toEqual([]);
});
test('code class pass 1: specialists at medium, the native adversarial at full, the outside voice at its own', () => {
const p = planReview(base({}));
test('non-risky code pass 1 is light at any size (R51): one batched reader carrying the Red Team prompt and the native adversarial', () => {
const p = planReview(base({ diffLines: 900 }));
expect([p.depth, roles(p)]).toEqual(['light', ['specialist:batched:full', 'adversarial-native:adversarial:full']]);
});
test('code class pass 1 at standard: specialists at medium, the native adversarial at full, the outside voice at its own', () => {
const p = planReview(base({ floor: 'standard' }));
expect(roles(p)).toEqual(['specialist:batched:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(batch(p)).toEqual(['testing', 'security']);
expect(p.skipped).toContain('maintainability, performance, simplification (lean ensemble: read only when forced)');
@@ -162,10 +181,10 @@ describe('GSTA-131 pass-1 review repairs', () => {
expect(scanNumbers({ 'test/fixtures/bench.json': ['{ "ratio": 0.74 }'] }).publishes).toBe(true);
});
test('--all-specialists forces every specialist; a code diff past 200 lines or with a CRITICAL adds the Red Team', () => {
const all = planReview(base({ force: 'all' }));
const all = planReview(base({ floor: 'standard', force: 'all' }));
expect(all.dispatches.filter(d => d.role === 'specialist').length).toBe(1);
expect([...batch(all)!].sort()).toEqual(['api-contract', 'data-migration', 'design', 'maintainability', 'performance', 'security', 'simplification', 'state-machines', 'testing']);
const has = (o: Partial<PlanInput>) => roles(planReview(base(o))).some(r => r.startsWith('red-team'));
const has = (o: Partial<PlanInput>) => roles(planReview(base({ floor: 'standard', ...o }))).some(r => r.startsWith('red-team'));
expect(has({ diffLines: 120 })).toBe(false);
expect(has({ diffLines: 201 })).toBe(true);
expect(has({ diffLines: 120, criticalFound: true })).toBe(true);
@@ -175,20 +194,20 @@ describe('GSTA-131 pass-1 review repairs', () => {
describe('GSTA-131 coverage: Red Team and specialist arms', () => {
const roles = (p: ReturnType<typeof planReview>) => p.dispatches.map(d => `${d.role}:${d.name}`);
test('Red Team runs on code past 200 lines or after a CRITICAL, and on every gate diff', () => {
expect(roles(planReview(base({ diffLines: 120 })))).not.toContain('red-team:red-team');
expect(roles(planReview(base({ diffLines: 201 })))).toContain('red-team:red-team');
expect(roles(planReview(base({ diffLines: 120, criticalFound: true })))).toContain('red-team:red-team');
expect(roles(planReview(base({ diffLines: 120, floor: 'standard' })))).not.toContain('red-team:red-team');
expect(roles(planReview(base({ diffLines: 201, floor: 'standard' })))).toContain('red-team:red-team');
expect(roles(planReview(base({ diffLines: 120, floor: 'standard', criticalFound: true })))).toContain('red-team:red-team');
expect(roles(planReview(base({ diffLines: 20, gateTagged: 1 })))).toContain('red-team:red-team');
});
test('scope arms select their specialist, state machines default on when scope is unknown, and --force all reaches docs class', () => {
const picked = (scope: Record<string, string>) => batch(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false', ...scope } })));
const picked = (scope: Record<string, string>) => batch(planReview(base({ floor: 'standard', scope: { SCOPE_STATE_MACHINE: 'false', ...scope } })));
expect(picked({ SCOPE_MIGRATIONS: 'true' })).toContain('data-migration');
expect(picked({ SCOPE_API: 'true' })).toContain('api-contract');
expect(picked({ SCOPE_FRONTEND: 'true' })).toEqual(['testing', 'security', 'design']);
expect(batch(planReview(base({ scope: {} })))).toContain('state-machines');
expect(batch(planReview(base({ scope: { SCOPE_STATE_MACHINE: 'false' } })))).not.toContain('state-machines');
expect(batch(planReview(base({ force: ['maintainability'] })))).toEqual(['testing', 'security', 'maintainability']);
expect(batch(planReview(base({ gated: ['design'], scope: { SCOPE_FRONTEND: 'true', SCOPE_STATE_MACHINE: 'false' } })))).toEqual(['testing', 'security']);
expect(batch(planReview(base({ floor: 'standard', force: ['maintainability'] })))).toEqual(['testing', 'security', 'maintainability']);
expect(batch(planReview(base({ floor: 'standard', gated: ['design'], scope: { SCOPE_FRONTEND: 'true', SCOPE_STATE_MACHINE: 'false' } })))).toEqual(['testing', 'security']);
const docsAll = planReview(base({ paths: ['docs/a.md'], force: 'all' }));
expect(docsAll.class).toBe('docs');
expect(docsAll.dispatches.filter(d => d.role === 'specialist').length).toBeGreaterThan(5);
@@ -215,8 +234,12 @@ describe('review depth: the matrix', () => {
expect(p.skipped.join('; ')).toContain('red-team (light depth');
expect(planReview(mixed({ perPath: per({ 'src/app.ts': 48 }), adversarialRan: true, history: { fullPasses: 1, last: 'full', touchedGate: true, fixedBelow: true } })).dispatches.some(d => d.role === 'adversarial-native')).toBe(false);
});
test('runtime 51 is standard: thresholds read runtime lines, reviewers read their own hunks', () => {
test('runtime 51 non-risky code stays light (R51: size never raises the depth)', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 400, 'README.md': 3 }), diffLines: 454 }));
expect([p.depth, p.runtimeLines, roles(p)]).toEqual(['light', 51, ['specialist:batched:full', 'adversarial-native:adversarial:full']]);
});
test('standard (floor or raise): thresholds read runtime lines, reviewers read their own hunks', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 400, 'README.md': 3 }), diffLines: 454, floor: 'standard' }));
expect([p.depth, p.runtimeLines]).toEqual(['standard', 51]);
expect(roles(p)).toEqual(['specialist:batched:medium', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host']);
expect(batch(p)).toEqual(['testing', 'security']);
@@ -274,14 +297,16 @@ describe('review depth: the matrix', () => {
expect(depthFor({ cls: 'code', runtimeLines: 10, errors: [], floor: 'full' })).toBe('full');
expect(depthFor({ cls: 'code', runtimeLines: 10, errors: [], floor: 'standard' })).toBe('standard');
expect(depthFor({ cls: 'gate', runtimeLines: 10, errors: [], floor: 'standard' })).toBe('full');
expect(depthFor({ cls: 'code', runtimeLines: 80, errors: [] })).toBe('standard');
expect(depthFor({ cls: 'code', runtimeLines: 80, errors: [] })).toBe('light');
expect(depthFor({ cls: 'code', runtimeLines: 3, errors: [], risk: ['data'] })).toBe('full');
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 10 }), floor: 'full' }));
expect(p.depth).toBe('full');
expect(p.depthReasons).toContain('floor:full');
});
test('depthOfCommitInput gives the same depth as the plan for the same input', () => {
const i = { paths: ['src/app.ts', 'test/a.test.ts'], perPath: per({ 'src/app.ts': 51, 'test/a.test.ts': 9 }), gateTagged: 0 };
expect(depthOfCommitInput(i)).toMatchObject({ depth: 'standard', runtimeLines: 51, rules: DEPTH_RULES, errors: [] });
expect(depthOfCommitInput(i)).toMatchObject({ depth: 'light', runtimeLines: 51, rules: DEPTH_RULES, errors: [], risk: [] });
expect(depthOfCommitInput({ ...i, added: { 'src/app.ts': ['const spend_cap_usd = 5;'] } })).toMatchObject({ depth: 'full', risk: ['money'] });
expect(depthOfCommitInput({ ...i, gateTagged: null }).depth).toBe('full');
});
});
@@ -300,6 +325,7 @@ describe('review depth: diff commands and the rules hash', () => {
try {
const src = fs.readFileSync(path.join(ROOT, 'lib/review-depth.ts'), 'utf8');
fs.symlinkSync(path.join(ROOT, 'lib/ship-policy.ts'), path.join(dir, 'ship-policy.ts'));
fs.symlinkSync(path.join(ROOT, 'lib/review-risk.ts'), path.join(dir, 'review-risk.ts'));
const hashOf = (text: string) => {
fs.writeFileSync(path.join(dir, 'review-depth.ts'), text);
fs.writeFileSync(path.join(dir, 'print.ts'), "import { DEPTH_RULES } from './review-depth';\nconsole.log(DEPTH_RULES);\n");
@@ -369,7 +395,7 @@ describe('D6: the scoped second pass after a blocking fix (GSTA-152 PR B)', () =
expect(r(p)).toEqual(['fix-check:fix-check', 'red-team:red-team', 'adversarial-native:adversarial']);
});
test('the scoped pass counts toward the cap, and a blocker it leaves stops for the owner', () => {
expect(nextStep(fixed(10, { fullPasses: FULL_PASS_CAP }), 'standard').step).toBe('fix-check');
expect(nextStep(fixed(10, { fullPasses: FULL_PASS_CAP }), 'standard').step).toBe('stop');
expect(nextStep({ fullPasses: FULL_PASS_CAP, last: 'full', blockingOpen: true, rangeRuntimeLines: 10 }, 'light').step).toBe('stop');
const at = (m: number) => new Date(Date.UTC(2026, 9, 28, 10, m)).toISOString();
const rows = [
@@ -397,6 +423,7 @@ describe('D6: the scoped second pass after a blocking fix (GSTA-152 PR B)', () =
const src = fs.readFileSync(path.join(ROOT, 'lib/review-depth.ts'), 'utf8');
expect(src).toContain('SCOPED_RANGE_LINES = 150');
fs.symlinkSync(path.join(ROOT, 'lib/ship-policy.ts'), path.join(dir, 'ship-policy.ts'));
fs.symlinkSync(path.join(ROOT, 'lib/review-risk.ts'), path.join(dir, 'review-risk.ts'));
fs.writeFileSync(path.join(dir, 'review-depth.ts'), src.replace('SCOPED_RANGE_LINES = 150', 'SCOPED_RANGE_LINES = 151'));
fs.writeFileSync(path.join(dir, 'print.ts'), "import { DEPTH_RULES } from './review-depth';\nconsole.log(DEPTH_RULES);\n");
const res = spawnSync(process.execPath, ['--no-env-file', '--config=/dev/null', path.join(dir, 'print.ts')], { encoding: 'utf8', timeout: 30_000 });
@@ -491,11 +518,11 @@ describe('lean ensemble: one batched specialist at standard and full depth (GSTA
expect(specialists(full).map(d => [d.name, d.effort, d.checklists])).toEqual([['batched', 'medium', ['testing', 'security', 'state-machines']]]);
});
test('a standard code plan reads one batched reader on its own hunks, and a forced checklist joins it', () => {
const p = planReview(base({ diffLines: 400, scope: { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'false' } }));
const p = planReview(base({ diffLines: 400, floor: 'standard', scope: { SCOPE_BACKEND: 'true', SCOPE_STATE_MACHINE: 'false' } }));
expect(specialists(p).map(d => [d.name, d.checklists])).toEqual([['batched', ['testing', 'security']]]);
expect(roles(p)).toContain('red-team:red-team:full');
expect(batch(planReview(base({ force: ['performance', 'simplification'] })))).toEqual(['testing', 'security', 'performance', 'simplification']);
expect(specialists(planReview(base({ tiers: false })))[0]!.effort).toBe('host');
expect(batch(planReview(base({ floor: 'standard', force: ['performance', 'simplification'] })))).toEqual(['testing', 'security', 'performance', 'simplification']);
expect(specialists(planReview(base({ floor: 'standard', tiers: false })))[0]!.effort).toBe('host');
});
});
+3 -1
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",
'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;
const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
@@ -77,7 +78,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: '4542785a1e217d8dd054985978b42c055b9fd79082579cb7664f187f7066e48e', replay: 'GST-257' },
'lib/review-depth.ts': { sha: 'd649569a163c03476c6e0c5ea295f5d02088f96aab85fbc7d558f2ac2b27006a', replay: 'R51' },
'lib/review-risk.ts': { sha: 'dbef7475d467e17340b21a2bacb52a16a7ccb1ee66291900ed2d36dfb5cfb7f2', replay: 'R51' },
};
describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => {
+1 -1
View File
@@ -522,7 +522,7 @@ describe('Capy pass program (fake host, real bins)', () => {
fs.mkdirSync(files, { recursive: true });
const fc: Record<string, string> = {};
for (const [k, v] of [['fix_list', '1. src/a.ts: guard v80\n'], ['hunks', 'diff --git a/src/a.ts b/src/a.ts\n+export const v81 = 81;\n'], ['tests', 'bun test: 3 pass, 0 fail\n'], ['callers', 'none\n']]) { fc[k] = path.join(files, k + '.txt'); fs.writeFileSync(fc[k]!, v!); }
const planFlags = ['--pass', '2', '--last', 'full', '--fixed-blocking'];
const planFlags = ['--pass', '1', '--last', 'full', '--fixed-below'];
const missing = await runPass(host(dir, env), { base: 'main', out: path.join(dir, '.state/run/fc0'), pass: 'x19fc0', planFlags });
expect(missing).toContain('STOP: gstack-review-dispatch pass exited');
expect(missing).toContain('the plan dispatches a fix-check: pass needs --fix-list, --hunks and --tests');
+9 -9
View File
@@ -35,18 +35,18 @@ describe('gstack-review-plan', () => {
});
test('--json carries the class, the next step, every dispatch with its effort and the audit effort; --no-tiers renders specialists and audits at host effort', () => {
const r = repo({ 'src/a.ts': Array.from({ length: 80 }, (_, i) => `export const v${i} = ${i};`).join('\n') + '\n' });
const plan = JSON.parse(run(r, ['--json']).stdout);
const plan = JSON.parse(run(r, ['--json', '--depth-floor', 'standard']).stdout);
expect(plan).toMatchObject({ class: 'code', next: { step: 'full' }, pass: 0 });
expect(plan.dispatches.filter((d: any) => d.role === 'specialist').map((d: any) => d.effort)).toEqual(expect.arrayContaining(['medium']));
expect(plan.dispatches.filter((d: any) => d.role === 'specialist').every((d: any) => d.effort === 'medium')).toBe(true);
expect(plan.dispatches.find((d: any) => d.role === 'adversarial-native').effort).toBe('full');
expect(plan.audit_effort).toBe('light');
expect(run(r, []).stdout).toContain('DISPATCH: specialist batched effort=medium checklists=testing,security,state-machines');
const host = JSON.parse(run(r, ['--json', '--no-tiers']).stdout);
expect(run(r, ['--depth-floor', 'standard']).stdout).toContain('DISPATCH: specialist batched effort=medium checklists=testing,security,state-machines');
const host = JSON.parse(run(r, ['--json', '--no-tiers', '--depth-floor', 'standard']).stdout);
expect(host.audit_effort).toBe('host');
expect(host.dispatches.filter((d: any) => d.role === 'specialist').every((d: any) => d.effort === 'host')).toBe(true);
expect(host.dispatches.find((d: any) => d.role === 'adversarial-native').effort).toBe('full');
expect(run(r, ['--no-tiers']).stdout).toContain('DISPATCH: specialist batched effort=host checklists=testing,security,state-machines');
expect(run(r, ['--no-tiers', '--depth-floor', 'standard']).stdout).toContain('DISPATCH: specialist batched effort=host checklists=testing,security,state-machines');
});
test('a below-floor fix after pass 1 plans one fix-check', () => {
const r = run(repo({ 'src/a.ts': 'export const a = 1;\n' }), ['--pass', '1', '--last', 'full', '--fixed-below']);
@@ -132,7 +132,7 @@ describe('gstack-review-plan: review depth', () => {
const r = repo({ 'src/a.ts': code(10), 'test/a.test.ts': code(200) });
const out = run(r, []).stdout;
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
expect(out).toMatch(/^REVIEW_DEPTH: light runtime_lines=10 rules=[0-9a-f]{16} reasons=runtime=10<=50 codes=none$/m);
expect(out).toMatch(/^REVIEW_DEPTH: light runtime_lines=10 rules=[0-9a-f]{16} reasons=runtime=10 codes=none$/m);
expect(out).toContain(`GATE_SCAN: tagged=0 head=${head}`);
expect(out).toContain('DISPATCH: specialist batched effort=full checklists=testing,security,state-machines,red-team');
expect(out).toContain('DISPATCH: adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)');
@@ -149,7 +149,7 @@ describe('gstack-review-plan: review depth', () => {
});
test('--json carries depth, runtime_lines, rules, errors, gate_scan and every dispatch\'s exclude array', () => {
const r = repo({ 'src/a.ts': code(80), 'README.md': '# y\n' });
const plan = JSON.parse(run(r, ['--json']).stdout);
const plan = JSON.parse(run(r, ['--json', '--depth-floor', 'standard']).stdout);
expect(plan).toMatchObject({ depth: 'standard', step_depth: 'standard', runtime_lines: 80, errors: [], codes: [], gate_scan: { tagged: 0 } });
expect(plan.rules).toMatch(/^[0-9a-f]{16}$/);
expect(plan.dispatches.find((d: any) => d.name === 'batched').exclude).toEqual([':(exclude)README.md']);
@@ -157,7 +157,7 @@ describe('gstack-review-plan: review depth', () => {
});
test('--depth-floor full raises a light diff, never lowers a full one, and an invalid floor is a usage error', () => {
const r = repo({ 'src/a.ts': code(5) });
expect(run(r, ['--depth-floor', 'full']).stdout).toMatch(/^REVIEW_DEPTH: full runtime_lines=5 .*reasons=runtime=5<=50,floor:full /m);
expect(run(r, ['--depth-floor', 'full']).stdout).toMatch(/^REVIEW_DEPTH: full runtime_lines=5 .*reasons=runtime=5,floor:full /m);
const gate = repo({ 'src/a.ts': code(5), '.github/workflows/ci.yml': 'on: push\n' });
expect(run(gate, ['--depth-floor', 'standard']).stdout).toMatch(/^REVIEW_DEPTH: full /m);
expect(run(r, ['--depth-floor', 'light']).status).toBe(2);
@@ -190,7 +190,7 @@ describe('gstack-review-plan: the scoped second pass (GSTA-152 D6)', () => {
const out = run(r, [...pass2, '--range-base', rangeBase]).stdout;
expect(out).toMatch(/^REVIEW_PLAN: class=code pass=2 next=scoped dispatches=3 /m);
expect(out).toContain(`FIX_RANGE: runtime_lines=24 base=${rangeBase}`);
expect(out).toMatch(/^NEXT: scoped \(a blocking finding was fixed on a standard diff with 24 runtime lines in the fix range/m);
expect(out).toMatch(/^NEXT: scoped \(a blocking finding was fixed on a light diff with 24 runtime lines in the fix range/m);
expect(out).toContain('DISPATCH: fix-check fix-check effort=full');
expect(out).toContain('DISPATCH: red-team red-team effort=full');
expect(out).toContain('DISPATCH: adversarial-native adversarial effort=full');
@@ -221,7 +221,7 @@ describe('gstack-review-plan: the scoped second pass (GSTA-152 D6)', () => {
fs.writeFileSync(path.join(r.dir, 'src', 'b.ts'), code(2, 'b'));
git('add', '.'); git('commit', '-qm', 'fix: revert the workflow edit');
const out = run(r, [...pass2, '--range-base', rangeBase]).stdout;
expect(out).toMatch(/^REVIEW_DEPTH: standard /m);
expect(out).toMatch(/^REVIEW_DEPTH: light .* step=standard$/m);
expect(out).toMatch(/^NEXT: full \(a blocking finding was fixed and a fix touched a gate-class path/m);
expect(JSON.parse(run(r, [...pass2, '--range-base', rangeBase, '--json']).stdout)).toMatchObject({ next: { step: 'full' }, fix_range: { touched_gate: true } });
}, 30_000);
+123
View File
@@ -0,0 +1,123 @@
/**
* R51: review scales to risk, not size (docs/review-policy.md#review-depth, #risk-classes). The risk classes
* (lib/review-risk.ts), the round cap, and the routing table: every gate,
* security, user-data and money fixture still plans the full panel, and non-risky code is light at any size.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { classifyDiff, nextStep, planReview, scanNumbers, type PlanInput } from '../lib/review-depth';
import { matchRisk, RISK_RULES } from '../lib/review-risk';
import { targetedClasses } from '../lib/review-targeted';
import { parseReviewLoop, receiptSummaryLine } from '../lib/ship-receipt';
const ROOT = path.join(import.meta.dir, '..');
const risk = (codePaths: string[], added: Record<string, string[]> = {}, scopeAuth = false) => [...matchRisk({ codePaths, added, removed: {}, scopeAuth }).keys()];
describe('risk classes', () => {
// Value: protects=security and user-data code reaches the full panel; fails_when=a trust-boundary or data-loss signal is missed,
// or an unrelated path or line raises a class; seam=matchRisk
test('security and data: paths and lines, each with a negative', () => {
expect(risk(['src/core/trust/owner-actions.ts'])).toEqual(['security']);
expect(risk(['lib/redact-patterns.ts'])).toEqual(['security']);
expect(risk(['src/run.ts'], { 'src/run.ts': ['const child = spawn(cmd, { shell: true });'] })).toEqual(['security']);
expect(risk(['src/view.tsx'], { 'src/view.tsx': ['<div dangerouslySetInnerHTML={{ __html: body }} />'] })).toEqual(['security']);
expect(risk(['bin/x.sh'], { 'bin/x.sh': ['eval "$CMD"'] })).toEqual(['security']);
expect(risk(['src/run.ts'], { 'src/run.ts': ['const m = re.exec(line);', 'const s = trustee.name;'] })).toEqual([]);
expect(risk(['src/core/persistence/git-door.ts'])).toEqual(['data']);
expect(risk(['src/db/users.ts'], { 'src/db/users.ts': ['await sql`DELETE FROM users WHERE id = ${id}`;'] })).toEqual(['data']);
expect(risk(['app/models/user.rb'], { 'app/models/user.rb': ['User.where(stale: true).destroy_all'] })).toEqual(['data']);
expect(risk(['src/list.ts'], { 'src/list.ts': ['// delete from the list when done', 'arr.splice(i, 1);'] })).toEqual([]);
for (const line of ['db.exec("delete from users");', 'await sql`delete from sessions where id = 1`;', 'DELETE FROM users;', 'drop database app;', 'DROP SCHEMA s;',
'await sql`DELETE FROM ${table} WHERE id = ?`;', 'DELETE FROM users u WHERE u.stale', 'DELETE FROM sessions USING users WHERE x', 'DELETE FROM users RETURNING id', 'const q = "DELETE FROM " + table;'])
expect(risk(['src/db.ts'], { 'src/db.ts': [line] })).toEqual(['data']);
for (const line of ["const r = spawn('sh', ['-c', cmd], { timeout: 1 });", "Bun.spawn(['bash', '-lc', x]);", "fs.rmSync(t, { recursive: true, force: true });", 'rm -rf "$DIR"'])
expect(risk(['src/run.ts'], { 'src/run.ts': [line] })).toEqual(['security']);
expect(risk(['scripts/test-pr-profile.ts', 'lib/storyboard.ts'])).toEqual([]);
expect(RISK_RULES).toMatch(/^[0-9a-f]{16}$/);
});
// Value: protects=the targeted pass keeps its exact classes now that its matchers live in lib/review-risk.ts; fails_when=the move
// changes a money, auth or secret verdict; seam=targetedClasses over matchRisk
test('money, auth and secret come from the same matchers the targeted pass reads', () => {
const lines = { 'src/run.ts': ['const spend_cap_usd = 5;', 'const k = process.env.OPENAI_API_KEY;'] };
expect(risk(['src/run.ts', 'src/auth/session.ts'], lines, true)).toEqual(['money', 'auth', 'secret']);
expect(targetedClasses({ paths: ['src/run.ts', 'src/auth/session.ts'], added: lines, removed: {}, scope: { SCOPE_AUTH: 'true' }, gateTagged: 0 }).classes).toEqual(['money', 'auth', 'secret']);
});
});
describe('UI text and SVGs stay code (R51: the docs paths are deferred to an allowlist classifier)', () => {
// Value: protects=a copy edit, a Vue binding or a namespaced SVG script never plans as docs; fails_when=any of them leaves the code
// class or its lines skip the risk scan; seam=classifyDiff
test.each([
['a JSX copy edit', 'src/App.tsx', ['<h1>Old copy</h1>'], ['<h1>New copy</h1>']],
['a Vue binding', 'src/App.vue', ['<div :title="user.name">Profile</div>'], ['<div :title="leak(document.cookie)">Profile</div>']],
['a locale value', 'locales/en.yml', ['save: "Save"'], ['save: "<img src=x onerror=alert(1)>"']],
['a namespaced SVG script', 'docs/chart.svg', [], ['<svg xmlns:s="http://www.w3.org/2000/svg"><s:script>window.x=42</s:script></svg>']],
])('%s is code', (_n, p, removed, added) => {
expect(classifyDiff({ paths: [p], gateTagged: 0, added: { [p]: added }, removed: { [p]: removed }, perPath: [{ path: p, lines: removed.length + added.length, binary: false }] }).class).toBe('code');
});
});
describe('round 2 never ships an unread gate edit', () => {
// Value: protects=a gate-class edit after round 1 always gets the owner's eyes; fails_when=a round-2 or fix-check gate edit
// returns done; seam=nextStep
test('a gate edit in round 2 or in the fix-check stops for the owner; a plain below-floor fix there finishes', () => {
expect(nextStep({ fullPasses: 2, last: 'full', fixedBelow: true, touchedGate: true })).toMatchObject({ step: 'stop', reason: expect.stringContaining('gate-class') });
expect(nextStep({ fullPasses: 1, last: 'fix-check', touchedGate: true })).toMatchObject({ step: 'stop', reason: expect.stringContaining('gate-class') });
expect(nextStep({ fullPasses: 2, last: 'full', fixedBelow: true }).step).toBe('done');
});
});
const none = scanNumbers({});
const plan = (o: Partial<PlanInput>) => planReview({ paths: ['src/app.ts'], scope: { SCOPE_STATE_MACHINE: 'false' }, gateTagged: 0, diffLines: 10, numbers: none, history: { fullPasses: 0, last: 'none' }, ...o });
const roles = (p: ReturnType<typeof planReview>) => [...new Set(p.dispatches.map(d => d.role))];
const FULL_PANEL = ['specialist', 'red-team', 'adversarial-native', 'outside-voice'];
const captured = (name: string): Partial<PlanInput> => {
const x = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/fixtures/review-depth', name + '.json'), 'utf8'));
return { paths: x.paths, deleted: x.deleted, stampOnly: x.stamp_only ?? [], scope: x.scope, gateTagged: x.gate_tagged, diffLines: x.diff_lines, numbers: x.numbers, perPath: x.per_path };
};
const one = (p: string, lines: string[] = ['export const a = 1;']) => ({ paths: [p], added: { [p]: lines }, removed: {}, perPath: [{ path: p, lines: lines.length, binary: false }] });
describe('routing: every gate, security, user-data and money fixture plans the full panel', () => {
// Value: protects=R51 never lowers review on risky code; fails_when=a gate or risk fixture plans less than outside voice, Red Team,
// batched specialist and the native adversarial pass, or loses the targeted reader it had; seam=planReview
test.each([
['captured gate fixture', captured('code'), []],
['gate path', one('.github/workflows/ci.yml', ['on: push']), []],
['gate tag', { ...one('src/a.ts'), gateTagged: 2 }, ['gate']],
['deleted test', { ...one('src/a.ts'), deleted: ['test/a.test.ts'] }, []],
['auth scope', { ...one('src/auth/session.ts'), scope: { SCOPE_AUTH: 'true' } }, ['auth']],
['migration scope', { ...one('db/migrate/001.sql', ['ALTER TABLE users ADD COLUMN email text;']), scope: { SCOPE_MIGRATIONS: 'true' } }, []],
['secret path', one('lib/credentials.ts'), ['secret']],
['secret line', one('src/client.ts', ['const k = process.env.OPENAI_API_KEY;']), ['secret']],
['money path', one('src/billing/invoice.ts'), ['money']],
['money line', one('src/charge.ts', ['export const spend_cap_usd = 5;']), ['money']],
['security path', one('lib/redact-patterns.ts'), []],
['security line', one('src/run.ts', ['const run = new Function(body);']), []],
['user-data path', one('src/core/persistence/store.ts'), []],
['user-data line', one('src/db/purge.ts', ['await db.users.deleteMany({ where: { stale: true } });']), []],
] as const)('%s', (_n, o, targeted) => {
const p = plan({ ...(o as Partial<PlanInput>), targeted: { classes: [...targeted] } });
expect(p.depth).toBe('full');
expect(FULL_PANEL.filter(r => !roles(p).includes(r as never))).toEqual([]);
expect(roles(p).includes('targeted')).toBe(targeted.length > 0);
});
test('non-risky code is light at any size: one batched reader with the Red Team prompt and the native adversarial', () => {
const p = plan({ ...one('src/util.ts', Array.from({ length: 900 }, (_, n) => `export const v${n} = ${n};`)), diffLines: 900 });
expect([p.depth, roles(p)]).toEqual(['light', ['specialist', 'adversarial-native']]);
const docs = plan({ paths: ['docs/evals/run.md'], perPath: [{ path: 'docs/evals/run.md', lines: 400, binary: false }], numbers: scanNumbers({ 'docs/evals/run.md': ['recall 33/41'] }) });
expect([docs.depth, roles(docs)]).toEqual(['light', ['outside-voice']]);
});
});
describe('the receipt records the tier and the rounds', () => {
// Value: protects=a receipt carries review_loop.rounds and the summary line prints depth and rounds; fails_when=the parser drops
// rounds or a legacy loop without it gains one; seam=parseReviewLoop and receiptSummaryLine
test('rounds parse and print; a legacy loop prints no rounds', () => {
expect(parseReviewLoop({ passes: 1, blocking_found: 0, deferred: 0, fix_checks: 1, rounds: 2 })).toMatchObject({ rounds: 2 });
const r = { pr: 7, head: 'a'.repeat(40), base: 'main', review_depth: { depth: 'light' }, review_loop: { passes: 1, blocking_found: 0, deferred: 0, fix_checks: 1, rounds: 2 } } as never;
expect(receiptSummaryLine(r)).toContain('depth=light passes=1 blocking_found=0 deferred=0 fix_checks=1 rounds=2');
const legacy = { pr: 7, head: 'a'.repeat(40), base: 'main', review_loop: { passes: 2, blocking_found: 1, deferred: 0 } } as never;
expect(receiptSummaryLine(legacy)).not.toContain('rounds=');
});
});
+1 -1
View File
@@ -30,7 +30,7 @@ describe('late defers from Step 11', () => {
{ skill: 'adversarial-review', via: 'ship', timestamp: '2026-10-26T10:25:00Z', findings: [{ fingerprint: 'd:1:w', blocking: true, action: 'deferred' }] },
];
const s = summarizeReviewLoop(rows, '2026-10-26T09:30:00Z');
expect(s).toEqual({ passes: 2, blocking_found: 3, deferred: 2, noted: 0, fixed: 1, scope: 'frozen' });
expect(s).toEqual({ passes: 2, blocking_found: 3, deferred: 2, noted: 0, fixed: 1, scope: 'frozen', rounds: 2 });
expect(reviewLoopLine(s)).toBe('REVIEW_LOOP: passes=2 blocking_found=3 deferred=2 noted=0 fixed=1 scope=frozen');
expect(LATE_DEFER_SKILL).toBe('review-late-defer');
// A `review` row logged without a start token would be the newest review row and read UNVERIFIED; the late-defer row is not a diff review at all.
+60
View File
@@ -0,0 +1,60 @@
/**
* #3541: `gstack-review-scope summary` reads the review log through
* gstack-review-read, a bash script. Spawned by path it never starts on
* Windows (no shebangs), so summary always refused with an empty
* "gstack-review-read failed:" and /ship could not write its review-loop file.
*
* Windows-safe by construction: both bins launch through
* lib/script-invocation.ts (Git Bash on win32), state lives under os.tmpdir().
*/
import { afterEach, beforeEach, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { runScriptSync } from '../lib/script-invocation';
const ROOT = path.resolve(import.meta.dir, '..');
const SLUG = 'review-scope-launch';
let tmp = '', repo = '', state = '', env: NodeJS.ProcessEnv = {};
beforeEach(() => {
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-rs-launch-')));
repo = path.join(tmp, 'repo');
state = path.join(tmp, 'state');
fs.mkdirSync(repo);
fs.mkdirSync(path.join(state, 'projects', SLUG), { recursive: true });
const gitconfig = path.join(tmp, 'gitconfig');
fs.writeFileSync(gitconfig, '');
env = {
...process.env, HOME: tmp, USERPROFILE: tmp, GSTACK_STATE_ROOT: state, GSTACK_HOME: state, GSTACK_PROJECT_SLUG: SLUG,
GIT_CONFIG_GLOBAL: gitconfig, GIT_CONFIG_NOSYSTEM: '1', GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined,
};
const git = (...args: string[]) => {
const r = spawnSync('git', args, { cwd: repo, env, encoding: 'utf8', timeout: 30_000 });
expect(r.status, r.stderr).toBe(0);
};
git('init', '-q', '-b', 'feat');
fs.writeFileSync(path.join(repo, 'a.txt'), 'a\n');
git('add', 'a.txt');
git('-c', 'user.email=fixture@example.test', '-c', 'user.name=Fixture', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture');
});
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
test('summary reads the review log through gstack-review-read (#3541)', () => {
const rows = [
{ skill: 'review', via: 'ship', timestamp: '2026-10-26T10:00:00Z', review_scope: 'frozen', findings: [{ fingerprint: 'a:1:x', blocking: true, action: 'fixed' }] },
{ skill: 'review', via: 'ship', timestamp: '2026-10-26T10:30:00Z', review_scope: 'frozen', findings: [] },
];
fs.writeFileSync(path.join(state, 'projects', SLUG, 'feat-reviews.jsonl'), rows.map(r => JSON.stringify(r)).join('\n') + '\n');
const out = path.join(tmp, 'review-loop.json');
const r = runScriptSync(path.join(ROOT, 'bin', 'gstack-review-scope'), ['summary', '--since', '2026-10-26T09:00:00Z', '--out', out, '--json'], {
cwd: repo, env, encoding: 'utf8', timeout: 120_000,
});
expect(r.error).toBeUndefined();
expect(r.status, r.stderr).toBe(0);
const summary = JSON.parse(fs.readFileSync(out, 'utf8'));
expect(summary).toMatchObject({ passes: 2, blocking_found: 1, fixed: 1, scope: 'frozen' });
}, 150_000);
+3 -3
View File
@@ -208,7 +208,7 @@ describe('loop summary', () => {
{ skill: 'review', timestamp: '2026-10-26T10:40:00Z', findings: [{ fingerprint: 'e:1:v', blocking: true }] },
];
const s = summarizeReviewLoop(rows, '2026-10-26T09:30:00Z');
expect(s).toEqual({ passes: 2, blocking_found: 2, deferred: 1, noted: 1, fixed: 1, scope: 'frozen' });
expect(s).toEqual({ passes: 2, blocking_found: 2, deferred: 1, noted: 1, fixed: 1, scope: 'frozen', rounds: 2 });
expect(reviewLoopLine(s)).toBe('REVIEW_LOOP: passes=2 blocking_found=2 deferred=1 noted=1 fixed=1 scope=frozen');
});
@@ -223,7 +223,7 @@ describe('loop summary', () => {
{ skill: 'review', via: 'ship', review_binding: { state: 'changed', started_at: '2026-10-26T09:00:00.000Z' }, findings: [{ fingerprint: 'old:1:q', action: 'noted' }] },
{ skill: 'review', via: 'ship', findings: [{ fingerprint: 'undated:1:q', action: 'noted' }] },
];
expect(summarizeReviewLoop(rows, '2026-10-26T09:30:00Z')).toEqual({ passes: 3, blocking_found: 1, deferred: 0, noted: 2, fixed: 1, scope: 'frozen' });
expect(summarizeReviewLoop(rows, '2026-10-26T09:30:00Z')).toEqual({ passes: 3, blocking_found: 1, deferred: 0, noted: 2, fixed: 1, scope: 'frozen', rounds: 3 });
});
});
@@ -421,7 +421,7 @@ describe('gstack-review-scope (bin)', () => {
{ skill: 'review', timestamp: '2026-10-26T10:40:00Z', findings: [{ fingerprint: 'e:1:v', blocking: true, action: 'fixed' }] },
];
fs.writeFileSync(path.join(logDir, `${branch}-reviews.jsonl`), rows.map(r => JSON.stringify(r)).join('\n') + '\n{not json\n');
const expected = { passes: 2, blocking_found: 2, deferred: 1, noted: 1, fixed: 1, scope: 'frozen' };
const expected = { passes: 2, blocking_found: 2, deferred: 1, noted: 1, fixed: 1, scope: 'frozen', rounds: 2 };
const out = path.join(repo.root, 'review-loop.json');
const r = runBin(repo, 'gstack-review-scope', ['summary', '--since', '2026-10-26T09:30:00Z', '--out', out, '--json']);
expect(r.status).toBe(EXIT.ok);
+26 -8
View File
@@ -110,8 +110,8 @@ describe('planReview dispatches the targeted reader', () => {
test('at light, standard and full depth when a class hit, never when none did', () => {
expect(plan({ targeted: { classes: ['money'] } })).toMatchObject({ depth: 'light' });
expect(roles(plan({ targeted: { classes: ['money'] } }))).toContain('targeted full');
expect(plan({ diffLines: 80, perPath: [{ path: 'src/a.ts', lines: 80, binary: false }], targeted: { classes: ['secret'] } }).depth).toBe('standard');
expect(roles(plan({ diffLines: 80, perPath: [{ path: 'src/a.ts', lines: 80, binary: false }], targeted: { classes: ['secret'] } }))).toContain('targeted full');
expect(plan({ diffLines: 80, perPath: [{ path: 'src/a.ts', lines: 80, binary: false }], targeted: { classes: ['secret'] }, floor: 'standard' }).depth).toBe('standard');
expect(roles(plan({ diffLines: 80, perPath: [{ path: 'src/a.ts', lines: 80, binary: false }], targeted: { classes: ['secret'] }, floor: 'standard' }))).toContain('targeted full');
expect(plan({ gateTagged: 1, targeted: { classes: ['gate'] } }).depth).toBe('full');
expect(roles(plan({ gateTagged: 1, targeted: { classes: ['gate'] } }))).toContain('targeted full');
expect(roles(plan({ targeted: { classes: [] } }))).not.toContain('targeted full');
@@ -183,6 +183,18 @@ function planRepo(f: Fixture) {
const run = (args: string[]) => spawnSync(PLAN, ['--base', 'main', ...(args.includes('--gate-tagged') ? [] : ['--gate-tagged', '0']), ...args], { cwd: dir, encoding: 'utf8', timeout: 60_000, env: { ...process.env, GSTACK_STATE_ROOT: state } });
return { dir, run, write };
}
/**
* R51 (risk, not size): the depth each fixture plans now. Every risk or gate fixture plans full and keeps every role main
* dispatched, plus the full panel; a non-risky fixture keeps main's reviewers except the roles listed (size no longer raises it).
*/
const FULL_PANEL = ['specialist', 'red-team', 'adversarial-native', 'outside-voice'];
const R51: Record<string, { depth: string; panel: boolean; dropped?: string[] }> = {
'none-light': { depth: 'light', panel: false }, 'none-standard': { depth: 'light', panel: false, dropped: ['outside-voice'] }, 'none-docs': { depth: 'light', panel: false },
'none-tests': { depth: 'light', panel: false }, 'none-costume': { depth: 'light', panel: false }, 'none-gate-path': { depth: 'full', panel: true },
'secret-light': { depth: 'full', panel: true }, 'secret-path': { depth: 'full', panel: true }, 'secret-removed': { depth: 'full', panel: true }, 'gate-tagged': { depth: 'full', panel: true },
'money-light': { depth: 'full', panel: true }, 'money-path': { depth: 'full', panel: true }, 'money-removed': { depth: 'full', panel: true }, 'money-standard': { depth: 'full', panel: true },
'auth-path': { depth: 'full', panel: true }, 'money-fix-check': { depth: 'full', panel: false }, 'money-pass2': { depth: 'full', panel: false }, 'gate-repeat': { depth: 'full', panel: false },
};
const field = (out: string, prefix: string, key: string) => new RegExp(`^${prefix}.* ${key}=(\\S+)`, 'm').exec(out)?.[1];
describe('gstack-review-plan prints TARGETED and dispatches the targeted reader', () => {
@@ -190,17 +202,23 @@ describe('gstack-review-plan prints TARGETED and dispatches the targeted reader'
// standard and full depth and on fix-check, scoped and repeat steps, is still dispatched unchanged, and the only addition is the
// targeted reader on a class diff; fails_when=the change drops, renames or re-efforts any reviewer, or adds the reader on a diff
// with no class; seam=bin/gstack-review-plan end to end; no_claim=that the reader finds the defect (the paid replay's question)
test('the dispatch-superset no-loss check: on every fixture the branch dispatches main\'s reviewers plus only the targeted reader', () => {
// R51 changes the table: risk and gate fixtures plan full (every role main dispatched, plus the full panel), and the one
// non-risky standard fixture drops only the outside voice; fails_when=a risk fixture loses a role main dispatched or misses
// the full panel, or a non-risky fixture loses a role the R51 table does not list
test('the dispatch-superset no-loss check: risk and gate fixtures keep every role main dispatched plus the full panel; non-risky ones drop only what R51 lists', () => {
expect(Object.keys(MAIN).sort()).toEqual(Object.keys(FIXTURES).sort());
expect(new Set(Object.values(MAIN).map(m => m.depth))).toEqual(new Set(['light', 'standard', 'full']));
expect(Object.keys(R51).sort()).toEqual(Object.keys(FIXTURES).sort());
const role = (d: string) => d.split(' ')[0]!;
for (const [name, f] of Object.entries(FIXTURES)) {
const r = planRepo(f).run(f.args ?? []);
expect(r.status, `${name}: ${r.stderr}`).toBe(0);
const got = r.stdout.split('\n').filter(l => l.startsWith('DISPATCH: ')).map(l => l.slice('DISPATCH: '.length));
const want = MAIN[name]!;
expect(r.stdout, name).toContain(`REVIEW_DEPTH: ${want.depth} `);
expect(got.filter(d => !want.main.includes(d)), name).toEqual(want.classes === 'none' ? [] : ['targeted targeted effort=full']);
expect(want.main.filter(d => !got.includes(d)), name).toEqual([]);
const want = MAIN[name]!, now = R51[name]!;
expect(r.stdout, name).toContain(`REVIEW_DEPTH: ${now.depth} `);
const roles = new Set(got.map(role));
expect([...new Set(want.main.map(role))].filter(x => !roles.has(x)), name).toEqual(now.dropped ?? []);
if (now.panel) expect(FULL_PANEL.filter(x => !roles.has(x)), name).toEqual([]);
expect(roles.has('targeted'), name).toBe(want.classes !== 'none');
expect(field(r.stdout, 'TARGETED:', 'classes'), name).toBe(want.classes);
}
}, 180_000);
+5 -1
View File
@@ -18,7 +18,7 @@ import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const SPAWN_BIN_BY_PATH = /\b(?:spawn|spawnSync|execFile|execFileSync)\(\s*(?:path\.)?join\(\s*(?:BIN_DIR|binDir|BIN|bin|binRoot|BIN_ROOT)\b/;
const SPAWN_BIN_BY_PATH = /\b(?:spawn|spawnSync|execFile|execFileSync)\(\s*(?:path\.)?join\(\s*(?:(?:BIN_DIR|binDir|BIN|bin|binRoot|BIN_ROOT)\b|(?:path\.)?dirname\(|import\.meta\.dir\b)/;
const SPAWN_BARE_BASH = /\b(?:spawn|spawnSync|execFile|execFileSync|execSync)\(\s*['"]bash['"]|Bun\.spawn(?:Sync)?\(\s*\[\s*['"]bash['"]|\[\s*['"]bash['"]\s*,\s*['"]-l?c['"]/;
const COMMENT = /^\s*(?:\/\/|\*|\/\*)/;
/** `const X = path.resolve(…, 'bin', 'gstack-…')`: a bin path held in a constant, then spawned by name (#3190's gstack-config). */
@@ -34,6 +34,8 @@ const KNOWN_SITES: Record<string, string> = {
'bin/gstack-issue-links': 'outside this wave: the gstack-issue-guard spawn moves to runScriptSync',
// Found when the bin-constant rule was added (fix wave PR 5, item 10b).
'bin/gstack-brain-cache': 'outside this wave: the gstack-config salience read moves to runScriptSync',
// Found when the scanner learned the import.meta.url bin path (#3541).
'bin/gstack-ship-receipt': 'the gstack-session-kind spawn moves to runScriptSync in the #3541 ship-receipt follow-up',
'lib/autoplan-admission.ts': 'returns before the spawn on win32; the bin/gstack-lease touch moves to runScriptSync with that bin\'s owner',
};
@@ -88,6 +90,8 @@ describe('#3197: shebang bins and bash go through lib/script-invocation.ts', ()
test('the scanner catches both shapes', () => {
expect(SPAWN_BIN_BY_PATH.test("spawnSync(path.join(BIN_DIR, 'gstack-wtree'), [], { timeout: 1 })")).toBe(true);
expect(SPAWN_BIN_BY_PATH.test('spawnSync(join(BIN_DIR, "gstack-slug"), { timeout: 1 })')).toBe(true);
expect(SPAWN_BIN_BY_PATH.test("spawnSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-review-read'), [], { timeout: 1 })")).toBe(true);
expect(SPAWN_BIN_BY_PATH.test("spawnSync(join(import.meta.dir, 'gstack-slug'), [], { timeout: 1 })")).toBe(true);
expect(SPAWN_BARE_BASH.test("spawnSync('bash', ['-c', cmd], { timeout: 1 })")).toBe(true);
expect(SPAWN_BARE_BASH.test('const argv = ["bash", "-c", command];')).toBe(true);
expect(SPAWN_BARE_BASH.test("spawnSync(process.execPath, [script], { timeout: 1 })")).toBe(false);
+1 -1
View File
@@ -86,7 +86,7 @@ posixTest('a scripted actor that fixes the diff, defers the pre-existing defect
await act(f);
const verdict = await shipFrozenScopeFailures(f, { exitReason: 'success' });
expect(verdict.failures).toEqual([]);
expect(verdict.evidence.loop).toEqual({ passes: 2, blocking_found: 2, deferred: 1, noted: 0, fixed: 1, scope: 'frozen' });
expect(verdict.evidence.loop).toEqual({ passes: 2, blocking_found: 2, deferred: 1, noted: 0, fixed: 1, rounds: 2, scope: 'frozen' });
const rows = verdict.evidence.rows as Array<Record<string, any>>;
expect(rows.map(r => r.pass)).toEqual([1, 2]);
expect(rows[0]!.findings.map((x: any) => [x.fingerprint, x.action])).toEqual([[FROZEN_SCOPE_FIX_IDENTITY, 'auto-fixed'], [FROZEN_SCOPE_DEFER_IDENTITY, 'deferred']]);
+42
View File
@@ -6,6 +6,7 @@
import { afterEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { laneConcurrency } from '../lib/ship-bin-common';
import { lineOf, PREFLIGHT, shipFixture, type ShipFx } from './helpers/ship-bin-fixture';
setDefaultTimeout(60_000);
@@ -107,6 +108,47 @@ describe('gstack-ship-preflight', () => {
expect(lineOf(zero.stdout, 'LANE')).toMatch(/^LANE: tests ZERO-RUN exit=0 tests_ran=0/);
});
// Value: protects=Step 5 can run its lanes one at a time, and each lane keeps its own tests_ran (GST-851: GSTA-2 saw six lanes start within
// 60 ms on a 4-core machine on gstack-evals#115); fails_when=--lanes serial lanes overlap, a lane loses its count, or a bad --lanes value runs;
// why_new=runLanes started every lane at once with no serial mode; seam=runLanes; no_claim=how long a lane takes
test('--lanes serial runs one lane at a time and keeps each lane\'s count; a bad --lanes value is usage', () => {
const f = fx();
const log = path.join(f.root, 'lane-order.log');
const lane = (n: string) => `${n}=echo start ${n} >> '${log}'; sleep 1; echo end ${n} >> '${log}'; printf " 2 pass\\n 0 fail\\nRan 2 tests across 1 file. [5.00ms]\\n"`;
const r = f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--lanes', 'serial', '--lane', lane('a'), '--lane', lane('b')]);
expect(r.status).toBe(0);
const order = fs.readFileSync(log, 'utf8').trim().split('\n');
expect(order.length).toBe(4);
expect(order.map(l => l.split(' ')[0])).toEqual(['start', 'end', 'start', 'end']);
expect(order[1]!.split(' ')[1]).toBe(order[0]!.split(' ')[1]);
expect(r.stdout.split('\n').filter(l => l.startsWith('LANE: ')).map(l => l.replace(/ log=\S+$/, ''))).toEqual(['LANE: a pass exit=0 tests_ran=2', 'LANE: b pass exit=0 tests_ran=2']);
expect(lineOf(r.stdout, 'LANES')).toBe('LANES: serial (--lanes)');
expect(f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--lanes', 'fast', '--lane', LANE]).status).toBe(2);
expect(f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--lane', LANE, '--lanes']).status).toBe(2);
});
// Value: protects=parallel lanes report in the order given even when a later lane finishes first, and an unflagged run names the core count
// behind its default (GST-851); fails_when=results come back in finish order or the LANES line drops its reason; why_new=runLanes became a
// worker pool; seam=runLanes, LANES line; no_claim=which default any given machine picks
test('--lanes parallel keeps lane order when a later lane finishes first; unflagged LANES names the core count', () => {
const f = fx();
const lane = (n: string, wait: number) => `${n}=sleep ${wait}; printf " 1 pass\\n 0 fail\\nRan 1 tests across 1 file. [5.00ms]\\n"`;
const r = f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--lanes', 'parallel', '--lane', lane('slow', 1), '--lane', lane('fast', 0)]);
expect(r.status).toBe(0);
expect(lineOf(r.stdout, 'LANES')).toBe('LANES: parallel (--lanes)');
expect(r.stdout.split('\n').filter(l => l.startsWith('LANE: ')).map(l => l.split(' ')[1])).toEqual(['slow', 'fast']);
expect(lineOf(f.run(PREFLIGHT, ['--tests-only', '--base', 'main', '--lane', LANE]).stdout, 'LANES')).toMatch(/^LANES: (serial|parallel) \(\d+ cores\)$/);
});
// Value: protects=a small machine runs lanes one at a time without a flag, a large one keeps running them together, and the flag overrides
// either way (GST-851); fails_when=a 4-core default runs lanes at once or the flag is ignored; why_new=no default existed; seam=laneConcurrency;
// no_claim=the core count of any CI runner
test('lane concurrency: serial on 4 cores or fewer, parallel above, --lanes overrides', () => {
expect([laneConcurrency(undefined, 2), laneConcurrency(undefined, 4), laneConcurrency(undefined, 8)]).toEqual([1, 1, Infinity]);
expect([laneConcurrency('serial', 64), laneConcurrency('parallel', 2)]).toEqual([1, Infinity]);
expect(laneConcurrency('fast', 8)).toBe('--lanes fast is not serial or parallel');
});
// Value: protects=Step 2's candidates and Step 1.4's carry check are reported for the agent to judge, and an open PR's carry verdict is printed; fails_when=a new bin/ artifact goes unmentioned or the carry check is skipped when a PR is open;
// why_new=moved from Steps 1.4 and 2; seam=gh, gstack-ship-receipt carry; no_claim=whether a candidate is a distributable
test('a new artifact path is a Step 2 candidate; an open PR runs the carry dry-run and prints its verdict', () => {
+10 -8
View File
@@ -57,25 +57,26 @@ describe('write records the review depth at the checked-out head', () => {
describe('the queue refuses a receipt below the depth its head needs', () => {
// Value: protects=a receipt reviewed lighter than its head needs never READYs or seats, and the refusal says what to do;
// fails_when=the queue trusts the recorded depth, or prints no required depth, reasons and inputs; seam=bindShippedReceipt
test('light on a standard head, a missing depth from a current writer and an unbound gate scan refuse; a legacy receipt is accepted', () => {
// R51: size never raises the depth, so the head that needs more than light is a risk-class one (a money path needs full).
test('standard on a full (money) head, a missing depth from a current writer and an unbound gate scan refuse; a legacy receipt is accepted', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const head = commit(repo, { 'lib/big.js': lines(80) });
const head = commit(repo, { 'lib/billing.js': lines(80) });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'standard', runtime_lines: 80 });
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'full', runtime_lines: 80, risk: ['money'] });
const base = repo.git('rev-parse', 'origin/main').slice(0, 12);
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
const good = bind(fs.readFileSync(body, 'utf8'));
expect(good.ok).toBe(true);
expect(good.ok && good.depthLine).toContain('REVIEW_DEPTH: receipt=standard required=standard runtime_lines=80 class=code');
expect(good.ok && good.depthLine).toContain('REVIEW_DEPTH: receipt=full required=full runtime_lines=80 class=code');
const low = bind(rewrite(body, r => { r.review_depth.depth = 'light'; }));
const low = bind(rewrite(body, r => { r.review_depth.depth = 'standard'; }));
expect(low.ok).toBe(false);
if (low.ok) return;
expect(low.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
expect(low.depthLine).toContain('receipt=light required=standard runtime_lines=80');
expect(low.depthLine).toContain('receipt=standard required=full runtime_lines=80');
expect(low.depthLine).toContain(`inputs=base@${base},registry@${base},gate_scan@${head.slice(0, 12)}`);
expect(low.message).toContain('runtime=80>50');
expect(low.message).toContain('risk:money');
expect(low.message).toContain(`the receipt is unchanged; run /ship on ${head.slice(0, 12)}`);
const unbound = bind(rewrite(body, r => { r.review_depth.depth = 'standard'; r.review_depth.gate_scan.head = 'a'.repeat(40); }));
@@ -218,7 +219,8 @@ describe('the queue and a scoped second pass (GSTA-152 D6)', () => {
expect(receiptIn(body).review_loop).toMatchObject({ passes: 2, dispatches: loop.dispatches });
const bound = bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '7', [head]);
expect(bound.ok).toBe(true);
expect(bound.ok && bound.depthLine).toContain('REVIEW_DEPTH: receipt=standard required=standard');
expect(bound.ok && bound.depthLine).toContain('REVIEW_DEPTH: receipt=light required=light');
expect(bound.ok && bound.depthLine).toMatch(/ rounds=2$/);
}, 60_000);
});
@@ -0,0 +1,4 @@
{
"bytes": 66,
"why": "GST-851: Step 5 prose says lanes run one at a time on 4 or fewer cores, names --lanes serial|parallel and the LANES line"
}
+65
View File
@@ -0,0 +1,65 @@
/**
* #3628: a host that keeps the Stop hook's stdin open after writing its payload
* held the hook for as long as the pipe stayed open (fs.readFileSync(0) waits
* for EOF), well past the hook's 2 s budget and toward the host's 15 s RPC
* window. The hook must return within its budget and still repair the timeline.
*
* Windows-safe by construction: the hook runs through Bun argv
* (process.execPath), state lives under os.tmpdir().
*/
import { afterEach, beforeEach, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
const ROOT = path.resolve(import.meta.dir, '..');
const HOOK_TS = path.join(ROOT, 'hosts', 'claude', 'hooks', 'timeline-stop-hook.ts');
const SLUG = 'stop-hook-stdin';
const BUDGET_MS = 2_000;
const STARTUP_ALLOWANCE_MS = process.platform === 'win32' ? 3_000 : 1_500;
let home = '', project = '', timeline = '';
beforeEach(() => {
home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-stop-stdin-home-')));
project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-stop-stdin-proj-')));
fs.mkdirSync(path.join(home, 'projects', SLUG), { recursive: true });
timeline = path.join(home, 'projects', SLUG, 'timeline.jsonl');
fs.writeFileSync(timeline, JSON.stringify({ skill: 'review', event: 'started', session: '1-1' }) + '\n');
});
afterEach(() => {
fs.rmSync(home, { recursive: true, force: true });
fs.rmSync(project, { recursive: true, force: true });
});
async function runHookHoldingStdin(payload: string | null): Promise<{ code: number | null; ms: number }> {
const started = performance.now();
const child = Bun.spawn([process.execPath, ...bunIsolationArgs(), HOOK_TS], {
stdin: 'pipe', stdout: 'ignore', stderr: 'ignore',
env: { ...process.env, GSTACK_HOME: home, GSTACK_STATE_ROOT: home, GSTACK_PROJECT_SLUG: SLUG },
});
if (payload !== null) { child.stdin.write(payload); child.stdin.flush(); }
const kill = setTimeout(() => child.kill(), 10_000);
const code = await child.exited;
clearTimeout(kill);
const ms = performance.now() - started;
try { child.stdin.end(); } catch { /* already closed */ }
return { code, ms };
}
const repairs = () => fs.readFileSync(timeline, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)).filter(e => e.source === 'stop-hook');
test('a complete payload on a pipe that never closes: returns within budget and repairs (#3628)', async () => {
const r = await runHookHoldingStdin(JSON.stringify({ hook_event_name: 'Stop', cwd: project }));
expect(r.code).toBe(0);
expect(r.ms).toBeLessThan(BUDGET_MS + STARTUP_ALLOWANCE_MS);
expect(repairs()).toHaveLength(1);
}, 20_000);
test('an open pipe with no data: returns within budget (#3628)', async () => {
const r = await runHookHoldingStdin(null);
expect(r.code).toBe(0);
expect(r.ms).toBeLessThan(BUDGET_MS + STARTUP_ALLOWANCE_MS);
}, 20_000);