mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 17:58:13 +01:00
fix: a granted fix-check after a fix-check plans in the loop (GST-1681) (#4096)
* fix: a granted fix-check after a fix-check plans in the loop (GST-1681) In the loop, a new review_loop_grant naming the last fix-check's commit (--range-base, the stop it answers) plans one more scoped fix-check, the next link of the fix-check chain finish counts (GEVA-363, gstack-evals#254). Replay: GST-1681 (repins lib/review-depth.ts) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes In the loop, a grant recorded before a later logged fix-check already bought a link of the fix-check chain, so gstack-review-plan refuses it (exit 3): a post-review link's grant names the commit its fix-check read, and a stale --range-base names an earlier link. A CLI regression covers both, and a grant recorded after the newest fix-check still plans. Review-Finding: p1/red-team#1 reader=red-team Review-Finding: p1/specialist-batched#1 reader=testing Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
0c41ced4fd
commit
e325b6d160
10 files changed
+155
-6
No files matched your search
@@ -81,7 +81,9 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
|
||||
before HEAD; needs --range-base): the stop after a round-2 full pass with a fixed blocking finding or a
|
||||
gate-class edit plans that fix-check instead; a fix-check never earns another, except that with
|
||||
--post-review --last fix-check a new grant at a stop head after --range-base (the last fix-check's
|
||||
commit) plans the next link of the fix-check chain; any other row is refused (exit 3)
|
||||
commit) plans the next link of the fix-check chain, and in the loop after a fix-check a new grant
|
||||
naming that fix-check's commit (--range-base) plans the next link; a grant recorded before a later
|
||||
logged fix-check already bought a link; any other row is refused (exit 3)
|
||||
--fix-checks <n> fix-checks already run in this invocation: the owner's grant adds one only after the first, in the lean
|
||||
loop and after a round-2 fix-check in the full loop (GEVA-223)
|
||||
--post-review plan the Step 9 that /ship Step 16 stage 2.1 inserted for edits after the review bound (finish's
|
||||
@@ -150,6 +152,7 @@ function main(): number {
|
||||
const root = git(['rev-parse', '--show-toplevel'])?.trim();
|
||||
if (!root) { process.stderr.write('gstack-review-plan: run it inside a git checkout\n'); return EXIT.fail; }
|
||||
process.chdir(root);
|
||||
let inLoopLink = false;
|
||||
if (grant) {
|
||||
const loc = locate({ cwd: root, env: process.env });
|
||||
const row = ('file' in loc ? readRecord(loc).rows : []).find(r => r.id === grant && r.outcome === 'approved' && `${r.ask} ${r.scope}`.includes('review_loop_grant'));
|
||||
@@ -157,7 +160,11 @@ function main(): number {
|
||||
const ancestor = (a: string, d: string) => git(['merge-base', '--is-ancestor', a, d]) !== null;
|
||||
const chained = has('--post-review') && last === 'fix-check';
|
||||
const stopHead = row && passHead ? `${row.ask} ${row.scope}`.match(/\b[0-9a-f]{7,40}\b/g)?.map(s => git(['rev-parse', '--verify', '--quiet', `${s}^{commit}`])?.trim()).find(s => s && ancestor(passHead, s) && ancestor(s, 'HEAD') && !(chained && s === passHead)) : undefined;
|
||||
inLoopLink = !chained && last === 'fix-check' && stopHead === passHead;
|
||||
if (!stopHead) { process.stderr.write(`gstack-review-plan: --grant ${grant}: no approved row ${grant} naming review_loop_grant and this invocation's stop head (a commit ${chained ? `after --range-base ${rangeBase}, the last fix-check's commit, whose own grant does not carry over` : `at or after --range-base ${rangeBase}`}, at or before HEAD) in this branch's approval record (${'file' in loc ? 'gstack-approvals list' : loc.message}); record the owner's grant with gstack-approvals add --ask '... (review_loop_grant <stop head sha>)' first\n`); return EXIT.refused; }
|
||||
const logged = inLoopLink ? runScriptSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-review-read'), [], { encoding: 'utf8', timeout: 60_000, maxBuffer: 64 * 1024 * 1024 }) : null;
|
||||
if (logged && logged.status !== 0) { process.stderr.write(`gstack-review-plan: --grant ${grant}: gstack-review-read exited ${logged.status}, so whether a fix-check already spent this grant is unknown; no plan is safer than a fix-check the grant may not buy\n`); return EXIT.fail; }
|
||||
if (logged && row && (logged.stdout ?? '').split('\n---CONFIG---')[0]!.split('\n').some(l => { try { const v = JSON.parse(l); return v?.skill === 'review-fix-check' && Date.parse(v.timestamp) > Date.parse(row.recorded_at); } catch { return false; } })) { process.stderr.write(`gstack-review-plan: --grant ${grant}: a fix-check was logged after this grant was recorded, so the grant already bought a link of the fix-check chain; the stop at --range-base ${rangeBase} needs a new grant recorded after the last fix-check (gstack-approvals add --ask '... (review_loop_grant ${passHead!.slice(0, 12)})')\n`); return EXIT.refused; }
|
||||
}
|
||||
const collected = collectLive(root, base);
|
||||
if (collected.errors.length) {
|
||||
@@ -236,7 +243,7 @@ function main(): number {
|
||||
const plan = planReview({
|
||||
...collected, ...exclusionInput, ...riskPaths, declared: declared as RiskClass[], shape: { panel: review.panel, readerModel: review.reader_model, platform: review.platform, head, platformReview: platform?.ok ? platform.review : null }, scope, gateTagged, targeted: { classes: targeted.classes, ran }, numbers: scanNumbers(kept(added)), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host',
|
||||
criticalFound: has('--critical-found'), adversarialRan: has('--adversarial-ran'), reviewedNumbers: flag('--reviewed-numbers'),
|
||||
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: (has('--fixed-below') || (postReview && pass > 0)) && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate || (postReview && pass > 0), pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines, granted: grant, fixChecks: has('--fix-checks') ? fixChecks : undefined, postReview },
|
||||
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: (has('--fixed-below') || (postReview && pass > 0)) && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate || (postReview && pass > 0), pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines, granted: grant, fixChecks: has('--fix-checks') ? fixChecks : undefined, postReview, chained: inLoopLink },
|
||||
});
|
||||
if (postReview && pass > 0) plan.next.reason = `edits after the review bound (Step 16 stage 2.1) plan as a fix to a gate-class path, never a note: ${plan.next.reason}`;
|
||||
const ranArg = flag('--targeted-ran');
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **A Captain-granted fix round after a fix-check is planned, not hand-run.** After round 2's fix-check and one granted fix-check, an owner who granted one more round at the last fix-check's stop head got `NEXT: stop` from `gstack-review-plan`, because in the loop a fix-check never earned another. The lane ran the fix-check by hand on a fix-range token, and finish printed `FIX_CHECK: not counted (fix-range pass; only a later full pass can clear review)` (gstack-evals#254). A new recorded grant naming the last fix-check's commit (`--range-base`) now plans one more scoped fix-check, the next link of the fix-check chain finish already counts; the previous link's grant (one recorded before a later logged fix-check) is refused and a grant at any other commit still stops, so one grant buys one link. (GST-1681)
|
||||
@@ -118,7 +118,10 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
`gstack-review-plan --post-review --last fix-check --range-base <that commit>
|
||||
--grant <id>`, plans one more fix-check, the next link of the
|
||||
[fix-check chain](ship-policy.md#fix-check-chain) (GST-1676). The last link's
|
||||
own grant is refused there, so each link still needs the owner's word.
|
||||
own grant is refused there, so each link still needs the owner's word. In the
|
||||
loop, a new grant naming the last fix-check's commit (`--range-base`) plans
|
||||
the next link the same way; a grant recorded before a later logged fix-check
|
||||
already bought a link and is refused (GST-1681).
|
||||
|
||||
<a id="transitions"></a>
|
||||
### Transitions
|
||||
@@ -144,6 +147,7 @@ incomplete coverage, which blocks until the reviewer is rerun.
|
||||
| a round-2 full pass that stops for a fixed blocking finding or a gate-class edit, with the owner's recorded grant (`--grant <approval id>`) | one scoped fix-check (round 3), after Step 11 on gate class |
|
||||
| a round-2 fix-check that stops the same way, with the owner's recorded grant and `--fix-checks 1` | one scoped fix-check (round 3), after Step 11 on gate class |
|
||||
| `--post-review` after a fix-check (edits after the review bound), with a new recorded grant at a stop head after the last fix-check's commit | one scoped fix-check, the next link of the fix-check chain, after Step 11 on gate class |
|
||||
| a fix-check that stops in the loop, with a new recorded grant naming its own commit (`--range-base`, the stop it answers), after any number of fix-checks | one scoped fix-check, the next link of the fix-check chain, after Step 11 on gate class |
|
||||
|
||||
A **fix-check** is one reviewer reading the touched hunks, the fix list, the
|
||||
test results and the callers of every changed symbol, at full effort. It is
|
||||
|
||||
+7
-1
@@ -1825,7 +1825,13 @@ the lean panel). At the cap it stops for the owner, whose recorded grant
|
||||
fix-check, pass `--range-base` at that fix-check's commit: a new grant at a
|
||||
stop head after it plans the next link of the chain below (GST-1676), and the
|
||||
last link's own grant is refused. So `finish` never asks for a step the
|
||||
planner refuses.
|
||||
planner refuses. In the loop, a fix-check that stops is answered the same way:
|
||||
a new grant naming that fix-check's commit (`--range-base`, the stop it
|
||||
answers) plans one more fix-check, the next link of the chain, however many
|
||||
fix-checks ran before (GST-1681, GEVA-363's third round). The previous link's
|
||||
grant is refused: it names an earlier stop head, or it was recorded before a
|
||||
later logged fix-check (a post-review link's grant names the commit its
|
||||
fix-check read). A grant at any other commit plans nothing: one grant, one link.
|
||||
|
||||
<a id="fix-check-chain"></a>**Chained fix-checks (GST-1627).** A second granted
|
||||
fix-check after the first one (a merge resolution, say) logs its row with
|
||||
|
||||
+5
-1
@@ -386,6 +386,8 @@ export interface History {
|
||||
fixChecks?: number;
|
||||
/** The Step 9 /ship Step 16 stage 2.1 inserted for edits after the review bound (gstack-review-plan --post-review). */
|
||||
postReview?: boolean;
|
||||
/** In the loop, the CLI bound `granted` to the last fix-check's own stop head, its commit (--range-base): a new grant, never the previous link's. */
|
||||
chained?: boolean;
|
||||
}
|
||||
export interface NextStep { step: Step; reason: string }
|
||||
|
||||
@@ -401,7 +403,8 @@ export interface NextStep { step: Step; reason: string }
|
||||
* and the owner's recorded grant (`granted`) turns the stop after a round-2 full pass with a fixed finding, or after a round-2
|
||||
* fix-check when `fixChecks` says it was the only one (GEVA-223), into its one fix-check; a fix-check never earns another, so
|
||||
* the grant cannot repeat. Edits after the review bound (`postReview`) that follow a fix-check are a new stop, and a new grant
|
||||
* there adds one fix-check, the next link of the chain finish counts (GST-1676, docs/ship-policy.md#fix-check-chain).
|
||||
* there adds one fix-check, the next link of the chain finish counts (GST-1676, docs/ship-policy.md#fix-check-chain). In the
|
||||
* loop, a grant the CLI bound to the last fix-check's own stop head (`chained`) adds one more the same way (GST-1681).
|
||||
*/
|
||||
export function nextStep(h: History, depth?: Depth): NextStep {
|
||||
const n = h.lean ? leanStep(h, depth) : loopStep(h, depth);
|
||||
@@ -409,6 +412,7 @@ export function nextStep(h: History, depth?: Depth): NextStep {
|
||||
if (h.gate && !h.adversarialRan) return { step: 'voices', reason: `gate class: Step 11's ${h.lean ? 'one reader' : 'voices (adversarial, outside, targeted)'} run${h.lean ? 's' : ''} before the owner's exit, then ask again with --adversarial-ran (${n.reason})` };
|
||||
const afterFixCheck = h.last === 'fix-check' && h.fullPasses === FULL_PASS_CAP - 1 && h.fixChecks === 1;
|
||||
if (h.postReview && h.last === 'fix-check' && h.granted && !h.blockingOpen) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check for the edits after the last fix-check, a new link of the fix-check chain (docs/ship-policy.md#fix-check-chain); a later post-review edit needs a grant of its own` };
|
||||
if (h.chained && h.last === 'fix-check' && h.granted && !h.blockingOpen) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check at the last fix-check's stop, a new link of the fix-check chain (docs/ship-policy.md#fix-check-chain); another stop needs a grant of its own` };
|
||||
if (!h.lean && h.granted && !h.blockingOpen && (afterFixCheck || (h.last === 'full' && h.fullPasses === FULL_PASS_CAP))) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check after round ${FULL_PASS_CAP} (review_loop_exception), ${afterFixCheck ? 'never a second' : 'never a fourth round'}` };
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"sha":"34685744162ba50d743abbf7c6e2e1ee57bf5ae0e6fd82347bca28f50a155b3f","replay":"GST-1676"}
|
||||
{"sha":"eac38277f567d80e6ec5613e050c05c2b2b837b405f14df25329814b9f3c34f9","replay":"GST-1681"}
|
||||
@@ -0,0 +1 @@
|
||||
{"replay":"GST-1681","justification":"GST-1681 (granted fix-check after a fix-check, in the loop): lib/review-depth.ts is main's GST-1676 bytes plus one History field, chained, and one transition in nextStep: with chained set, last fix-check, a recorded grant and no open blocker, the stop becomes one fix-check (the next link of finish's fix-check chain, docs/ship-policy.md#fix-check-chain). Only bin/gstack-review-plan sets chained, outside --post-review, when the grant names exactly --range-base (the last fix-check's commit, the stop it answers). Deterministic no-loss replay, no paid run: the GST-967 grid (MAIN_PLANS) and the GST-972 grid (MAIN_972_PLANS) in test/review-depth.test.ts hash unchanged and the GST-967 transition-table invariant passes, since none of them sets chained; so no plan main builds drops, renames or re-efforts a reader, no stop main prints without a grant changes, and no brief body moves (MAIN_SECTIONS unchanged)."}
|
||||
@@ -125,6 +125,20 @@ describe('GST-967: the round cap limits fix rounds, not the gate-class voices; a
|
||||
for (const o of [{ granted: undefined }, { blockingOpen: true }, { postReview: false }, { postReview: undefined }]) expect([o, nextStep({ ...h, ...o }).step]).toEqual([o, 'stop']);
|
||||
expect(nextStep({ ...h, last: 'full', fullPasses: 1, fixedBelow: true }).step).toBe('full');
|
||||
});
|
||||
// Value: protects=in the loop, a grant the CLI bound to the last fix-check's own stop head (`chained`) plans one more fix-check
|
||||
// after any number of fix-checks (GEVA-363's third round), after Step 11 on gate class; without that binding a fix-check still
|
||||
// never earns another; fails_when=the planner prints stop for a new grant (GST-1681) or plans one for an unbound grant;
|
||||
// seam=nextStep; no_claim=the CLI's binding (test/review-plan-cli.test.ts)
|
||||
test('a new grant bound to the last fix-check\'s stop plans the next chained fix-check in the loop (GST-1681)', () => {
|
||||
const h = { fullPasses: 1, last: 'fix-check', fixedBlocking: true, granted: 'a-5214be46db1e', fixChecks: 2, chained: true } as const;
|
||||
expect(nextStep(h)).toMatchObject({ step: 'fix-check', reason: expect.stringContaining('a-5214be46db1e') });
|
||||
expect(nextStep({ ...h, lean: true }).step).toBe('fix-check');
|
||||
expect(nextStep({ ...h, fullPasses: FULL_PASS_CAP }).step).toBe('fix-check');
|
||||
expect(nextStep({ ...h, gate: true }).step).toBe('voices');
|
||||
expect(nextStep({ ...h, gate: true, adversarialRan: true }).step).toBe('fix-check');
|
||||
for (const o of [{ chained: false }, { chained: undefined }, { granted: undefined }, { blockingOpen: true }]) expect([o, nextStep({ ...h, ...o }).step]).toEqual([o, 'stop']);
|
||||
expect(nextStep({ ...h, fixedBlocking: false }).step).toBe('done');
|
||||
});
|
||||
test('docs/review-policy.md\'s transitions table and rule 3 say both', () => {
|
||||
const doc = fs.readFileSync(path.join(ROOT, 'docs', 'review-policy.md'), 'utf8');
|
||||
const table = doc.slice(doc.indexOf('<a id="transitions"></a>'), doc.indexOf('A **fix-check** is one reviewer'));
|
||||
|
||||
@@ -156,6 +156,86 @@ describe('gstack-review-plan', () => {
|
||||
expect([reused.status, reused.stdout]).toEqual([3, '']);
|
||||
expect(reused.stderr).toContain(`--grant ${g1}: no approved row ${g1} naming review_loop_grant and this invocation's stop head (a commit after --range-base ${link1}, the last fix-check's commit`);
|
||||
}, 30_000);
|
||||
// Value: protects=a Captain's grant for a third fix round past a fix-check is planned, not hand-rendered: GEVA-363 (gstack-evals#254)
|
||||
// ran pass 1 full, fc1 (round 2), fc2 under a grant at fc1's stop, then the owner granted a third round at fc2's stop head (its
|
||||
// commit, the new --range-base); the in-loop Step 9 plans that one fix-check; the previous link's grant, a grant at another
|
||||
// commit, no grant or an open blocker do not; GBRA-336's --post-review --last fix-check --fix-checks 2 --grant invocation plans
|
||||
// with a grant after --range-base (GST-1676); fails_when=the planner prints stop despite the new grant (GEVA-363: the lane
|
||||
// hand-ran fc3 on a fix-range token finish could not count) or lets fc2's grant buy fc3; seam=bin/gstack-review-plan, nextStep;
|
||||
// no_claim=finish's chain count (test/ship-finish-ruled-notes.test.ts)
|
||||
test('GST-1681: in the loop, a new grant at the last fix-check\'s stop head plans one more fix-check, never for the previous grant', () => {
|
||||
const r = repo({ 'src/a.ts': 'export const a = 1;\n' });
|
||||
const env: NodeJS.ProcessEnv = { ...process.env, GSTACK_STATE_ROOT: r.state, GSTACK_APPROVALS_MIRROR: '0' };
|
||||
delete env.GSTACK_APPROVALS_DIR;
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
|
||||
const commit = (n: number) => { fs.appendFileSync(path.join(r.dir, 'src/a.ts'), `export const v${n} = ${n};\n`); git('commit', '-qam', `c${n}`); return git('rev-parse', 'HEAD'); };
|
||||
const add = (stop: string, n: string) => {
|
||||
const a = spawnSync(path.join(path.dirname(BIN), 'gstack-approvals'), ['add', '--ask', `Owner grant: scoped fix-check ${n} (review_loop_grant ${stop.slice(0, 12)})`, '--words', 'do it', '--by', 'GStack Minder', '--scope', 'this /ship invocation', '--json'], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
|
||||
expect(a.status, a.stderr).toBe(0);
|
||||
return JSON.parse(a.stdout).added[0] as string;
|
||||
};
|
||||
const fc1 = commit(1);
|
||||
const g1 = add(fc1, 'fc2');
|
||||
const fc2 = commit(2);
|
||||
const g2 = add(fc2, 'fc3');
|
||||
const later = commit(3);
|
||||
const g3 = add(later, 'at the fix');
|
||||
const plan = (a: string[]) => spawnSync(BIN, ['--base', 'main', '--gate-tagged', '0', '--pass', '1', '--last', 'fix-check', '--fix-checks', '2', '--range-base', fc2, ...a], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
|
||||
const third = plan(['--fixed-blocking', '--grant', g2]);
|
||||
expect(third.status, third.stderr).toBe(0);
|
||||
expect(third.stdout).toContain(`NEXT: fix-check (the owner's grant ${g2} adds one scoped fix-check at the last fix-check's stop, a new link of the fix-check chain`);
|
||||
expect(third.stdout).toContain('DISPATCH: fix-check fix-check effort=full');
|
||||
expect(plan(['--fixed-blocking']).stdout).toContain('NEXT: stop');
|
||||
expect(plan(['--fixed-blocking', '--grant', g2, '--blocking-open']).stdout).toContain('NEXT: stop');
|
||||
expect(plan(['--fixed-blocking', '--grant', g3]).stdout).toContain('NEXT: stop');
|
||||
const reused = plan(['--fixed-blocking', '--grant', g1]);
|
||||
expect([reused.status, reused.stdout]).toEqual([3, '']);
|
||||
expect(reused.stderr).toContain(`--grant ${g1}: no approved row ${g1} naming review_loop_grant and this invocation's stop head (a commit at or after --range-base ${fc2}`);
|
||||
const gbra336 = plan(['--post-review', '--grant', g3]);
|
||||
expect(gbra336.status, gbra336.stderr).toBe(0);
|
||||
expect(gbra336.stdout).toContain(`NEXT: fix-check (edits after the review bound (Step 16 stage 2.1) plan as a fix to a gate-class path, never a note: the owner's grant ${g3} adds one scoped fix-check for the edits after the last fix-check`);
|
||||
expect(plan(['--post-review', '--grant', g2]).status).toBe(3);
|
||||
}, 30_000);
|
||||
// Value: protects=one grant buys one link in the loop too: a grant recorded before a later logged fix-check already bought that
|
||||
// fix-check (a post-review link's grant names the commit its fix-check read; a stale --range-base names an earlier link), so the
|
||||
// in-loop replan refuses it, while a grant recorded after the newest fix-check plans; fails_when=a spent grant plans another
|
||||
// paid fix-check that finish cannot count (the GST-1681 /ship review: red-team#1, specialist#1); seam=bin/gstack-review-plan;
|
||||
// no_claim=finish's chain count (test/ship-finish-ruled-notes.test.ts)
|
||||
test('GST-1681: in the loop, a grant recorded before a later logged fix-check is spent and refused', () => {
|
||||
const r = repo({ 'src/a.ts': 'export const a = 1;\n' });
|
||||
const env: NodeJS.ProcessEnv = { ...process.env, GSTACK_STATE_ROOT: r.state, GSTACK_APPROVALS_MIRROR: '0' };
|
||||
delete env.GSTACK_APPROVALS_DIR;
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
|
||||
const commit = (n: number) => { fs.appendFileSync(path.join(r.dir, 'src/a.ts'), `export const v${n} = ${n};\n`); git('commit', '-qam', `c${n}`); return git('rev-parse', 'HEAD'); };
|
||||
const add = (stop: string, n: string) => {
|
||||
const a = spawnSync(path.join(path.dirname(BIN), 'gstack-approvals'), ['add', '--ask', `Owner grant: scoped fix-check ${n} (review_loop_grant ${stop.slice(0, 12)})`, '--words', 'do it', '--by', 'GStack Minder', '--scope', 'this /ship invocation', '--json'], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
|
||||
expect(a.status, a.stderr).toBe(0);
|
||||
return JSON.parse(a.stdout).added[0] as string;
|
||||
};
|
||||
const logFixCheck = () => {
|
||||
const l = spawnSync(path.join(path.dirname(BIN), 'gstack-review-log'), [JSON.stringify({ skill: 'review-fix-check', timestamp: new Date().toISOString(), status: 'clean', via: 'ship', completed: true, converged: true })], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
|
||||
expect(l.status, l.stderr).toBe(0);
|
||||
};
|
||||
const fc1 = commit(1);
|
||||
logFixCheck();
|
||||
const g1 = add(fc1, 'fc2');
|
||||
commit(2);
|
||||
logFixCheck();
|
||||
const c = commit(3);
|
||||
const gc = add(c, 'post-review link at c');
|
||||
logFixCheck();
|
||||
commit(4);
|
||||
const plan = (rangeBase: string, grant: string) => spawnSync(BIN, ['--base', 'main', '--gate-tagged', '0', '--pass', '1', '--last', 'fix-check', '--fix-checks', '3', '--fixed-blocking', '--range-base', rangeBase, '--grant', grant], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
|
||||
for (const [rangeBase, grant] of [[c, gc], [fc1, g1]] as const) {
|
||||
const spent = plan(rangeBase, grant);
|
||||
expect([spent.status, spent.stdout]).toEqual([3, '']);
|
||||
expect(spent.stderr).toContain(`--grant ${grant}: a fix-check was logged after this grant was recorded, so the grant already bought a link of the fix-check chain`);
|
||||
}
|
||||
const fresh = add(c, 'fc5');
|
||||
const next = plan(c, fresh);
|
||||
expect(next.status, next.stderr).toBe(0);
|
||||
expect(next.stdout).toContain(`NEXT: fix-check (the owner's grant ${fresh} adds one scoped fix-check at the last fix-check's stop`);
|
||||
}, 30_000);
|
||||
test('--fix-check-prompt renders the fix-check reviewer prompt from the parent\'s files and refuses a missing one', () => {
|
||||
const r = repo({ 'docs/howto.md': '# x\n' });
|
||||
const w = (n: string, t: string) => { const f = path.join(r.state, n); fs.writeFileSync(f, t); return f; };
|
||||
|
||||
@@ -350,6 +350,38 @@ describe('finish counts a post-review fix-check chain whose grants name each lin
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GST-1681 (GEVA-363, gstack-evals#254): pass 1, then fc1 (round 2) at the stop the voices read, then the owner's grant at that
|
||||
* stop head buys fc2, and a second grant at fc2's commit (its stop head) buys a third round, fc3. Each in-loop grant names the
|
||||
* stop it answers, the link's own range_base.
|
||||
*/
|
||||
describe('finish counts a Captain-granted third fix round as a chained fix-check (GST-1681)', () => {
|
||||
// Value: protects=the GEVA-363 shape counts once fc3 is a planned fix-check: fc2 and fc3 each carry their own grant, the chain stops
|
||||
// at fc1's tree (the reviewed tree), and the ungranted in-cap fc1 row is not a link; fails_when=finish refuses fc3 (GEVA-363
|
||||
// printed FIX_CHECK: not counted) or asks fc1 for a grant; seam=finish's chain walk and per-link grant assignment
|
||||
test('GEVA-363: fc1 in the cap, then fc2 and fc3 under their own grants, and finish counts fc2..fc3', () => {
|
||||
const f = shipFixture({ 'src/x.ts': 'export const x = 1;\n' }, { 'src/x.ts': 'export const x = 2;\n', 'test/x.test.ts': TEST_V1 });
|
||||
ships.push(f);
|
||||
const p1 = headOf(f);
|
||||
f.write({ 'src/x.ts': 'export const x = 3;\n' }); f.commit('round 1 fixes');
|
||||
const fc1 = headOf(f);
|
||||
logLink(f, p1);
|
||||
const reviewed = f.run(path.join(BIN, 'gstack-wtree'), []).stdout.trim();
|
||||
const reviewedBase = f.git('merge-base', 'origin/main', 'HEAD').stdout.trim();
|
||||
const g2 = grantLink(f, fc1, 'fc2');
|
||||
f.write({ 'src/x.ts': 'export const x = 4;\n' }); f.commit('fix the voices\' CRITICAL');
|
||||
const fc2 = headOf(f);
|
||||
logLink(f, fc1);
|
||||
const g3 = grantLink(f, fc2, 'fc3');
|
||||
f.write({ 'test/x.test.ts': TEST_V2 }); f.commit('fix fc2\'s CRITICAL');
|
||||
const fc3 = headOf(f);
|
||||
logLink(f, fc2);
|
||||
const r = finishAfterAudit(f, reviewed, reviewedBase);
|
||||
expect(lineOf(r.stdout, 'FIX_CHECK'), r.stdout).toBe(`FIX_CHECK: reviewed src/x.ts,test/x.test.ts (fix-check chain of 2 from ${fc1.slice(0, 12)}: at ${fc2.slice(0, 12)} grant ${g2}; at ${fc3.slice(0, 12)} grant ${g3})`);
|
||||
expect([r.status, lineOf(r.stdout, 'ROUTE')], r.stdout).toEqual([0, expect.stringMatching(/ behavior=0 .* fix_checked=2$/)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ruledNotes reads only approved owner rulings (GST-1537)', () => {
|
||||
const HEAD = '0123456789abcdef0123456789abcdef01234567';
|
||||
const row = (finding: string, o: Partial<{ ask: string; scope: string; by: string; outcome: string }> = {}) => {
|
||||
|
||||
Reference in new issue
Block a user