Give a tests-only change one reviewer in the /ship review plan (GST-2547) (#4318)

* Give a tests-only change one reviewer in the /ship review plan (GST-2547)

A diff that changed only test files planned zero readers, so the author's own core read was its only review. R51 gives tests one reviewer: the light runtime-0 plan now dispatches one batched Testing reader for a tests diff, and one batched code reader when the only code is registered outputs. Docs-only diffs still plan none.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Repin the review planner after its no-loss replay (GST-2547)

Forced specialists on a tests diff keep their own reader, as on main; the batched Testing reader joins only when nothing else reads the diff. The deterministic no-loss replay over 186,624 planReview cases found 0 lighter plans and 2,392 heavier ones, each one added batched reader (replay GST-2547).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-12 12:42:41 +00:00
1 parent bc858ffc65
commit 990315df00
8 files changed
+100 -18

No files matched your search

+2 -2
View File
@@ -61,7 +61,7 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
--gated a,b specialists the hit-rate stats tag GATE_CANDIDATE
--force a,b|all specialists forced by prompt flags (--all-specialists is all): at standard and full depth they join the
batched specialist's checklists, where maintainability, performance and simplification are read only when
forced; light runtime-0 and scoped passes dispatch them one reader each; targeted is no specialist and
forced; light runtime-0 docs and scoped passes dispatch them one reader each; targeted is no specialist and
refuses (exit 2): the plan dispatches it itself
--reviewed-numbers <fp> the numeric-claim fingerprint the outside voice already reviewed
--depth-floor standard|full raise the depth to at least this (never lowers it); /ship passes its prompt flag on every run
@@ -104,7 +104,7 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
Mode (R65, the base policy's review key): review.panel all (default) keeps today's panel on every class. review.panel risk
(opt-in; its one-reader shape has not passed a no-loss replay) runs lean off risk class, and review.panel off everywhere: lean runs one reader: risk class gets the merged reader on review.risk_model
(the native adversarial brief plus the checklists, the Red Team and the targeted brief), non-risk code the single reader at the
lane's reasoning (cross-family when it publishes numbers), docs and tests the core read; one fix-check after fixes, never more
lane's reasoning (cross-family when it publishes numbers), docs the core read, tests one batched testing reader (R51); one fix-check after fixes, never more
than 2 rounds. review.platform on (any panel): a light diff with no numeric claims whose PR
carries capy-ai[bot]'s review at HEAD plans NEXT: platform and a planned SKIP for every reader.
Depth (R51, risk not size): full on gate class, a risk class (money, auth, secret, security, data, review.risk_paths) or any DEPTH_ERROR;
@@ -0,0 +1,4 @@
---
important: regression
---
- **A change that only edits tests now gets one reviewer in /ship.** A pull request whose diff touched only test files (for example, a test switched to a scripted clock) made `gstack-review-plan` print a plan with zero readers, so the only review was the author's own core checklist read, and /ship carried on as if the change had been reviewed. Review policy (R51) gives tests one reviewer and one round. Now a tests-only diff plans one batched reader on the Testing checklist (`DISPATCH: specialist batched effort=medium checklists=testing`), which leaves any docs in the diff out of what it reads, and a clean pass or below-floor fixes still end the loop after that one round. A change that only touches registered generated outputs gets one batched reader on the code checklists too. A docs-only diff still plans no reader, and the plans for code, gate-class and security changes are unchanged.
+7 -4
View File
@@ -239,7 +239,8 @@ Under the opt-in `review.panel risk` (and `review.platform on` under any panel):
|---|---|---|
| Risk class: gate class, an R51 class (money, auth, secret, security, data), `eval` (scorer, judge and grader paths) or `policy` (`review.risk_paths`) | The full-depth plan as printed: the batched specialist, Red Team, native adversarial, outside voice and the targeted reader | A fixed blocker re-runs the printed plan on the fixed head as round 2; below-floor fixes get one fix-check |
| Non-risk code | One native adversarial reader (`DISPATCH: adversarial-native single`) at the lane's reasoning, on `review.reader_model` (`lane` by default), carrying the code checklists and the Red Team checklist; it also carries the targeted brief when the diff has a targeted class | A fixed blocker or a gate-class edit gets one fix-check on the touched hunks; below-floor fixes ship as notes |
| Docs and tests | The core read; the outside voice only when the diff publishes numbers | One fix-check after a fixed blocker |
| Docs | The core read; the outside voice only when the diff publishes numbers | One fix-check after a fixed blocker |
| Tests, or code with no runtime lines (only registered outputs) | One batched reader on the class's checklists (Testing for tests, which leaves docs and registered outputs out of its diff); the outside voice only when the diff publishes numbers | One fix-check after a fixed blocker |
| Docs or tests with no numeric, results or prereg claims, under `review.platform on`, with capy-ai[bot]'s review on the PR at HEAD | No reader: `NEXT: platform` and a planned `SKIP:` for every reader, the core read included | none |
`review.panel all`, the default, keeps today's panel and loop on every class. `risk` is
@@ -315,7 +316,7 @@ in a data file still schedules the outside voice.
| Depth | When | Pass 1 dispatches |
|---|---|---|
| **light**, runtime 0 | docs, tests, eval write-ups, or only registered outputs | one reader: the core checklist; the outside voice instead reads a diff that publishes numbers |
| **light**, runtime 0 | docs, tests, eval write-ups, or only registered outputs | docs: the core checklist only (R81). Tests: one **batched** reader on the Testing checklist, which leaves docs and registered outputs out of its diff (R51: tests get one reviewer). Only registered outputs: one batched reader on the code checklists over the whole diff. The outside voice also reads a diff that publishes numbers |
| **light**, runtime above 0 | code with no risk class, at any size (R51: size never raises the depth) | core, one **batched** specialist (the batched checklists below plus the Red Team prompt in one context), the native adversarial pass (kept: backtest inconclusive, GSTA-147 D5) and the outside voice on numbers |
| **standard** | `--depth-floor standard`, or a light diff raised by a fixed blocker or a CRITICAL | one batched specialist at `medium` and the Red Team at 200 runtime lines (or on a CRITICAL or a raise); the batched reader leaves docs and registered outputs out of its diff; adversarial and outside voice read everything |
| **full** | gate class, a [risk class](#risk-classes), or any classifier error | one batched specialist on the whole diff, the Red Team always, no hit-rate gating |
@@ -446,8 +447,10 @@ the specialists, audits and watchers at the host default too
most 150 runtime lines, is a [scoped pass](#scoped-pass): a fix-check and the Red
Team on the range, plus the native adversarial pass (on the range once Step 11 has
run, otherwise on the whole branch), instead of the specialist army.
- A tests-only diff (runtime 0) no longer dispatches the Testing specialist: the
core checklist and the gate scan review it.
- A tests-only diff (runtime 0) dispatches one batched reader on the Testing
checklist, beside the core checklist and the gate scan (GST-2547). Before, it
dispatched nothing, so the only review was the author's own core read; R51 gives
tests one reviewer. Only a docs diff plans no reader on pass 1.
- Every depth dispatches one batched specialist instead of one reader per
checklist; Maintainability, Performance and Simplification run only when forced.
- Light no longer runs a separate Red Team: the batched specialist carries its
+11 -3
View File
@@ -599,7 +599,11 @@ export function planReview(i: PlanInput): ReviewPlan {
if (firstVoices) add('adversarial-native', 'adversarial', 'full', [], { reason: LIGHT_ADVERSARIAL_REASON });
} else {
for (const n of SPECIALISTS.filter(forced)) add('specialist', n, specialist);
skipped.push('specialists, red-team and adversarial-native (light depth, runtime 0)');
if (cls === 'docs' || dispatches.length) skipped.push('specialists, red-team and adversarial-native (light depth, runtime 0)');
else {
add('specialist', 'batched', specialist, cls === 'tests' ? docsAndOutputs : [], { checklists: batchChecklists(cls, i, skipped) });
skipped.push(`red-team and adversarial-native (light depth, runtime 0: one batched reader reads the ${cls} diff, R51)`);
}
}
if (h.fullPasses === 0 && i.numbers.publishes) add('outside-voice', 'outside-voice', 'host');
else if (numbersChanged) add('outside-voice', 'outside-voice (numbers changed)', 'host');
@@ -654,7 +658,9 @@ export function planReview(i: PlanInput): ReviewPlan {
* R65's lean dispatch (docs/review-policy.md#review-depth): pass 1 runs one native adversarial reader at the lane's reasoning, on
* review.reader_model (the lane's model by default: FOUN-32, ggstack#52). A risk-class diff under review.panel off gets the merged
* reader: the gate-class checklists, the Red Team checklist and the targeted brief for its classes; a non-risk code diff gets the
* code-class checklists and the Red Team. Docs and tests keep the core read. A docs or tests diff with no numeric, results or prereg
* code-class checklists and the Red Team. Docs keep the core read; a tests diff, or code with no runtime lines, gets one batched
* reader on its class's checklists unless a forced specialist already reads it (R51: tests get one reviewer, so no non-docs pass 1
* plans zero readers). A docs or tests diff with no numeric, results or prereg
* claims, under review.platform on, with the platform review at HEAD, runs no reader at all (mode platform).
*/
/** Risk class read from the paths and lines (gate class, an R51 or review.risk_paths class, a classifier error), or a floor above light (FOUN-64 item 7: never the recorded review_depth). */
@@ -690,7 +696,9 @@ function leanPlan(i: PlanInput, c: ClassResult, depth: DepthResult, next: NextSt
reader();
skipped.push(`specialists, red-team and outside-voice as their own readers (R65: the one ${risky ? 'risk-class' : 'light'} reader carries them)`);
} else if (next.step === 'full') {
for (const n of SPECIALISTS.filter(forcedBy(i))) add('specialist', n, i.tiers === false ? 'host' : ROLE_EFFORT.specialist);
const effort: Effort = i.tiers === false ? 'host' : ROLE_EFFORT.specialist;
for (const n of SPECIALISTS.filter(forcedBy(i))) add('specialist', n, effort);
if (depth.class !== 'docs' && !dispatches.length) add('specialist', 'batched', effort, depth.class === 'tests' ? docsAndOutputs : [], { checklists: batchChecklists(depth.class, i, skipped) });
if (i.numbers.publishes) add('outside-voice', 'outside-voice', 'host');
else skipped.push(`outside-voice (${depth.class} class, no numeric claims)`);
} else if (next.step === 'voices') reader();
+1 -1
View File
@@ -1 +1 @@
{"sha":"a21071a64f90bc38781fd7d6b9a65b0fe78a0466cfd1e22238bc2dc4d5f4872c","replay":"GST-1833"}
{"sha":"057c5f84b5227d6ad8c6010447875ac9f031e35a4e6ef1aaaf17726cdd797ae6","replay":"GST-2547"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-2547","justification":"GST-2547 (R51: a tests-only diff gets one reviewer): lib/review-depth.ts is main's GST-1833 bytes plus one rule in planReview's light, runtime-0 branch and in leanPlan's matching branch: a tests diff, or a code diff with no runtime lines (only registered outputs), that has no forced specialist reading it gets one batched reader on its class's checklists (Testing for tests, which leaves docs and outputs out of its diff; the code checklists over the whole diff for outputs-only), so only a docs diff plans zero readers on pass 1. Preregistered no-loss criterion (written before the replay ran): over the grid below, no case loses a dispatch main planned, lowers class, depth or step depth, or changes the next step; every heavier case is a light full step on a tests or outputs-only diff that adds exactly one batched reader. Deterministic no-loss replay, no paid run, old module from origin/main bc858ffc against the new one: planReview over 186,624 cases (18 diff fixtures including docs, docs with numbers, tests, tests plus docs, a tests file on an eval path, outputs-only code, light code, gate, declared security, auth, migrations, a harness pin, unconsumed data, forced specialists and targeted gate; 6 review shapes: none, panel all, risk and off, platform on with and without the review at HEAD; 3 depth floors; the GST-967 history grid; tiers on and off): 0 lighter, 184,232 identical, 2,392 heavier, all of them one added batched reader (Testing on 1,784 tests cases, testing and security on 608 outputs-only cases). A first version merged forced specialists into that reader; this replay caught it (588 lighter cases) and the forced specialists now keep their own dispatch, as on main. No reviewer brief or checklist changes; the GST-967 and GST-972 grids hash to main's plans with the added Testing reader read back out."}
+66 -8
View File
@@ -196,8 +196,19 @@ describe('GST-967 invariant: no gate-class owner exit before Step 11\'s voices;
* takes none). The brief bodies lib/review-brief.ts cuts from the ship sections hash to MAIN_SECTIONS: main's bodies plus
* GST-508's exclude-pathspec placeholders (replay 'GST-508+GST-464+GST-967': the merged bodies minus main's are the wave's minus fc292e22f),
* then GST-1821's last-line VERDICT request in the outside brief (replay 'GST-1821').
* GST-2547 (R51) adds one reader and drops none: a light runtime-0 full step on the tests diff dispatches one batched reader on
* the Testing checklist where main dispatched nothing; asMainBeforeR51 checks that reader and reads the plan back as main's.
*/
const R51_SKIP = 'red-team and adversarial-native (light depth, runtime 0: one batched reader reads the tests diff, R51)';
function asMainBeforeR51(p: ReturnType<typeof planReview>): { dispatches: typeof p.dispatches; skipped: string[]; added: boolean } {
if (!(p.class === 'tests' && p.next.step === 'full' && p.stepDepth === 'light')) return { dispatches: p.dispatches, skipped: p.skipped, added: false };
expect(p.dispatches[0]).toEqual({ role: 'specialist', name: 'batched', effort: ROLE_EFFORT.specialist, exclude: [], checklists: ['testing'] });
const at = p.skipped.indexOf('specialists except testing (tests class)');
expect(p.skipped.slice(at, at + 2)).toEqual(['specialists except testing (tests class)', R51_SKIP]);
return { dispatches: p.dispatches.slice(1), skipped: [...p.skipped.slice(0, at), 'specialists, red-team and adversarial-native (light depth, runtime 0)', ...p.skipped.slice(at + 2)], added: true };
}
const MAIN_PLANS = '9d13d6bae86bfc9207ce7e0cec8da5b33db2518c1653e8e009160a98b0f04c89';
const R51_CASES = 28;
const MAIN_SECTIONS = '7eb6b4c2dd31edbd7c69f02bd31f8884407187fac8ace4e796632d60957b6bbb';
describe('GST-967 dispatch-superset no-loss check', () => {
// Value: protects=the no-loss rule for review-depth changes: GST-967 drops, renames and re-efforts no reviewer main dispatches on any grid case, and changes no brief body; fails_when=a non-stop plan changes, a voices step appears off gate class or after Step 11, it dispatches anything but the three voices, or a brief body moves;
@@ -213,19 +224,22 @@ describe('GST-967 dispatch-superset no-loss check', () => {
};
const rows: unknown[] = [];
let voices = 0;
let r51 = 0;
for (const [name, f] of Object.entries(fixtures)) for (const fullPasses of [0, 1, 2]) for (const last of ['none', 'full', 'fix-check'] as const) for (let bits = 0; bits < 16; bits++) for (const adversarialRan of [false, true]) {
const history = { fullPasses, last, fixedBlocking: !!(bits & 1), fixedBelow: !!(bits & 2), blockingOpen: !!(bits & 4), touchedGate: !!(bits & 8) };
const p = planReview({ ...f, numbers: none, history, adversarialRan } as PlanInput);
let { step, reason } = p.next;
let dispatches = p.dispatches;
const back = asMainBeforeR51(p);
if (back.added) r51++;
let dispatches = back.dispatches;
if (step === 'voices') {
voices++;
expect([p.class, adversarialRan, dispatches.map(d => `${d.role}:${d.effort}`)]).toEqual(['gate', false, ['adversarial-native:full', 'outside-voice:host', 'targeted:full']]);
[step, reason, dispatches] = ['stop', reason.slice(reason.indexOf('--adversarial-ran (') + '--adversarial-ran ('.length, -1), []];
}
rows.push({ name, fullPasses, last, bits, adversarialRan, class: p.class, step, reason, dispatches, skipped: p.skipped });
rows.push({ name, fullPasses, last, bits, adversarialRan, class: p.class, step, reason, dispatches, skipped: back.skipped });
}
expect([rows.length, voices, sha(rows)]).toEqual([1440, 71, MAIN_PLANS]);
expect([rows.length, voices, r51, sha(rows)]).toEqual([1440, 71, R51_CASES, MAIN_PLANS]);
expect(sha(sectionTexts(ROOT))).toBe(MAIN_SECTIONS);
});
});
@@ -276,7 +290,7 @@ describe('GST-972 follow-up no-loss check', () => {
for (const [name, c] of Object.entries(cases)) for (const fullPasses of [0, 1, 2]) for (const last of ['none', 'full', 'fix-check'] as const) for (let bits = 0; bits < 16; bits++) for (const adversarialRan of [false, true]) {
const history = { fullPasses, last, fixedBlocking: !!(bits & 1), fixedBelow: !!(bits & 2), blockingOpen: !!(bits & 4), touchedGate: !!(bits & 8) };
const p = planReview({ paths: c[pick], scope: c[2], gateTagged: 0, diffLines: 40, consumed: [], numbers: none, history, adversarialRan });
rows.push({ name, fullPasses, last, bits, adversarialRan, class: p.class, depth: p.depth, step: p.next.step, dispatches: p.dispatches.map(d => ({ role: d.role, name: d.name, effort: d.effort, checklists: d.checklists ?? [] })) });
rows.push({ name, fullPasses, last, bits, adversarialRan, class: p.class, depth: p.depth, step: p.next.step, dispatches: (pick ? asMainBeforeR51(p).dispatches : p.dispatches).map(d => ({ role: d.role, name: d.name, effort: d.effort, checklists: d.checklists ?? [] })) });
}
return rows;
};
@@ -305,6 +319,50 @@ describe('GST-972 follow-up no-loss check', () => {
});
});
describe('GST-2547: a tests-only diff gets one reader (R51)', () => {
// Value: protects=R51's one reviewer on a tests diff and the rule that only a human-docs diff (R81) may plan zero readers on pass 1; fails_when=a tests diff, or code with no runtime lines, plans no dispatch, a tests diff plans more than one reader or a second round after a clean pass, or the docs, code, gate and security plans move;
// why_new=HARN-1 (harness-evals#425): a tests-only diff printed dispatches=0 and the only review was the author's core read; seam=planReview; no_claim=what the reader finds
const off: PlanInput['shape'] = { panel: 'off', readerModel: 'lane', platform: 'off', head: 'h', platformReview: null };
const tests = { paths: ['test/clock.test.ts'], scope: { SCOPE_TESTS: 'true' }, diffLines: 12 };
test.each([['panel all', undefined], ['review.panel off', off]] as const)('%s: one batched Testing reader on pass 1, then done after a clean pass or below-floor fixes', (_, shape) => {
const p = planReview(base({ ...tests, shape }));
expect([p.class, p.depth, roles(p), batch(p)]).toEqual(['tests', 'light', ['specialist:batched:medium'], ['testing']]);
const after = (h: Partial<PlanInput['history']>) => planReview(base({ ...tests, shape, history: { fullPasses: 1, last: 'full', ...h } })).next.step;
expect([after({}), after({ fixedBelow: true, rangeRuntimeLines: 0 })]).toEqual(['done', 'done']);
});
test('only a docs diff plans zero readers on pass 1, under every panel and depth floor', () => {
const fixtures: Record<string, Partial<PlanInput>> = {
tests,
'outputs-only code': { paths: ['src/gen.ts'], perPath: [{ path: 'src/gen.ts', lines: 30, binary: false }], registry: { sha: 'a', outputs: ['src/gen.ts'] } },
code: {}, gate: { gateTagged: 1 }, security: { declared: ['security'] },
};
for (const shape of [undefined, off, { ...off, panel: 'risk' as const }, { ...off, panel: 'all' as const }]) for (const floor of [undefined, 'standard', 'full'] as const) {
for (const [name, f] of Object.entries(fixtures)) expect([name, shape?.panel, floor, planReview(base({ ...f, shape, floor })).dispatches.length > 0]).toEqual([name, shape?.panel, floor, true]);
expect(planReview(base({ paths: ['README.md'], scope: {}, gateTagged: null, shape })).dispatches).toEqual([]);
}
});
test('a forced specialist on a tests diff stays its own reader and is the one reader, as on main', () => {
const p = planReview(base({ ...tests, force: ['performance'] }));
expect([roles(p), p.skipped[0]]).toEqual([['specialist:performance:medium'], 'specialists, red-team and adversarial-native (light depth, runtime 0)']);
});
test('code with no runtime lines (registered outputs only) gets one batched reader on the code checklists over the whole diff', () => {
const p = planReview(base({ paths: ['src/gen.ts'], perPath: [{ path: 'src/gen.ts', lines: 30, binary: false }], registry: { sha: 'a', outputs: ['src/gen.ts'] } }));
expect([p.class, p.runtimeLines, p.dispatches]).toEqual(['code', 0, [{ role: 'specialist', name: 'batched', effort: 'medium', exclude: [], checklists: ['testing', 'security'] }]]);
});
test('controls: the docs, code, gate and security plans are main\'s', () => {
const plan = (o: Partial<PlanInput>) => { const p = planReview(base(o)); return [p.class, p.depth, p.dispatches.map(d => `${d.role}:${d.name}:${d.effort}${d.checklists ? `[${d.checklists.join(',')}]` : ''}`)]; };
expect(plan({ paths: ['docs/howto.md'], scope: {}, gateTagged: null })).toEqual(['docs', 'light', []]);
expect(plan({ paths: ['docs/howto.md'], scope: {}, gateTagged: null, shape: off })).toEqual(['docs', 'light', []]);
expect(plan({})).toEqual(['code', 'light', ['specialist:batched:full[testing,security,red-team]', 'adversarial-native:adversarial:full']]);
expect(plan({ shape: off })).toEqual(['code', 'light', ['adversarial-native:single:full[testing,security,red-team]']]);
const panel = ['specialist:batched:medium[testing,security]', 'red-team:red-team:full', 'adversarial-native:adversarial:full', 'outside-voice:outside-voice:host'];
expect(plan({ gateTagged: 1, targeted: { classes: ['gate'] } })).toEqual(['gate', 'full', [...panel, 'targeted:targeted:full']]);
expect(plan({ gateTagged: 1, shape: off })).toEqual(['gate', 'full', ['adversarial-native:merged:full[testing,security,red-team]']]);
expect(plan({ declared: ['security'] })).toEqual(['code', 'full', panel]);
expect(plan({ declared: ['security'], shape: off })).toEqual(['code', 'full', ['adversarial-native:merged:full[testing,security,red-team]']]);
});
});
describe('scanNumbers', () => {
test('finds claims and fingerprints them', () => {
const a = scanNumbers({ 'docs/bench.md': ['Median dropped 48% to 1.2s', 'no numbers here'] });
@@ -328,10 +386,10 @@ describe('planReview', () => {
const p = planReview(base({ paths: ['docs/bench.md'], scope: {}, numbers: scanNumbers({ 'docs/bench.md': ['3x faster'] }) }));
expect(roles(p)).toEqual(['outside-voice:outside-voice:host']);
});
test('tests class: runtime 0 is light, core only (Behavior changes: no Testing specialist)', () => {
test('tests class: runtime 0 is light, one batched reader on the Testing checklist (R51, GST-2547)', () => {
const p = planReview(base({ paths: ['test/a.test.ts'], scope: { SCOPE_TESTS: 'true' } }));
expect(p.depth).toBe('light');
expect(roles(p)).toEqual([]);
expect([roles(p), batch(p)]).toEqual([['specialist:batched:medium'], ['testing']]);
});
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 }));
@@ -455,9 +513,9 @@ const mixed = (o: Partial<PlanInput> = {}) => base({ paths: Object.keys(o.perPat
const err = (code: DepthError['code'], result?: DepthError['result']): DepthError => ({ code, detail: 'd', input: 'i', repair: 'r', ...(result ? { result } : {}) });
describe('review depth: the matrix', () => {
test('runtime 0 (tests and docs only) is light: core only, nothing dispatched', () => {
test('runtime 0 (tests and docs only) is light: one batched Testing reader that leaves the docs out (R51, GST-2547)', () => {
const p = planReview(mixed({ perPath: per({ 'test/a.test.ts': 300, 'docs/a.md': 40 }), diffLines: 340 }));
expect([p.depth, p.runtimeLines, p.dispatches]).toEqual(['light', 0, []]);
expect([p.depth, p.runtimeLines, p.dispatches]).toEqual(['light', 0, [{ role: 'specialist', name: 'batched', effort: 'medium', exclude: [':(exclude)docs/a.md'], checklists: ['testing'] }]]);
});
test('runtime 1-50 is light plus one batched specialist: the fixed set, scope picks and the Red Team in one context', () => {
const p = planReview(mixed({ perPath: per({ 'src/app.ts': 48, 'test/a.test.ts': 190, 'docs/a.md': 9 }), diffLines: 247 }));
+8
View File
@@ -52,6 +52,14 @@ describe('gstack-review-plan', () => {
expect(plan.status).toBe(0);
expect(plan.stdout).toContain('REVIEW_PLAN: class=docs pass=1 next=full dispatches=0 numbers=no');
});
// Value: protects=R51's one reviewer on a tests-only diff at the CLI /ship runs; fails_when=it prints dispatches=0 or any reader but the batched Testing one;
// why_new=HARN-1 (harness-evals#425, GST-2547): a test getting a scripted clock planned zero readers; seam=gstack-review-plan on a real repo; no_claim=what the reader finds
test('a tests-only diff plans one batched Testing reader (R51)', () => {
const r = run(repo({ 'test/clock.test.ts': "import { test, expect, setSystemTime } from 'bun:test';\ntest('clock', () => { setSystemTime(new Date(0)); expect(Date.now()).toBe(0); setSystemTime(); });\n" }), []);
expect(r.status).toBe(0);
expect(r.stdout).toContain('REVIEW_PLAN: class=tests pass=1 next=full dispatches=1 numbers=no');
expect(r.stdout.split('\n').filter(l => l.startsWith('DISPATCH: '))).toEqual(['DISPATCH: specialist batched effort=medium checklists=testing']);
});
test('a docs diff that publishes numbers keeps the outside voice', () => {
const r = run(repo({ 'docs/bench.md': 'Median latency fell 42% to 180 ms.\n' }), []);
expect(r.stdout).toContain('DISPATCH: outside-voice outside-voice effort=host');