fix(gate): part 1 verifier findings — stop-rule wording, guard tests, unreadable round
- plan-gate: the bare gate's stale-verdict reason no longer says "run the next round" once the stop rule fired; it says there is no next round, as acceptOpen already does. - trekplan Phase 9: "REVISE after the last round" (Fix now with --extra-round) is qualified "and the stop rule did not fire", pinned in review-loop-docs.test.mjs next to the [STP] pins. - plan-gate-stop-rule tests: a planted stop-rule acceptance claiming an open security blocker stays STOP; a planted acceptance without under: "stop-rule" stays STOP; the "a blocker -> refused" test now asserts the blocker refusal; --accept-open under the stop rule covers an open MAJOR and the gate passes with a WARN. - review-rounds prepare: when the stop rule is judged (P >= 2) and either round's JSON block cannot be read, exit 2 (not measured) with "stop rule: a round has no readable JSON block" instead of skipping the rule. The [RND] differing-archive and [CAP] fixtures now use readable archives; one new [STP] test covers an unreadable archive and an unreadable review.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
08e8aa1c66
commit
07a3c06c57
6 changed files with 153 additions and 13 deletions
|
|
@ -177,6 +177,70 @@ test('gate: a fired stop rule with an open blocker stops with PLAN_GATE_STOP_RUL
|
|||
assert.equal(g.stop_rule.fired, true);
|
||||
});
|
||||
|
||||
const T3_FROM = 'Why: the brief asks for a parser CLI.';
|
||||
const T3_TO = 'Why: the brief asks for a parser CLI with clear errors.';
|
||||
|
||||
test('gate: a stale verdict after a fired stop rule never says "run the next round"; without the stop rule it does', () => {
|
||||
const { r1, r2 } = sameClassTwice();
|
||||
assert.ok(r2.text.includes(T3_FROM));
|
||||
const g = gatePlan(r2.text.replace(T3_FROM, T3_TO));
|
||||
assert.equal(g.code, 1);
|
||||
const stale = g.reasons.find(x => x.code === 'PLAN_GATE_VERDICT_STALE');
|
||||
assert.ok(stale, JSON.stringify(g.reasons));
|
||||
assert.doesNotMatch(stale.message, /run the next round/);
|
||||
assert.match(stale.message, /stop rule fired/);
|
||||
assert.match(stale.message, /no next round/);
|
||||
// Control: the stop rule did not fire after round 1.
|
||||
assert.equal(r1.verdict.stop_rule.fired, false);
|
||||
const c = gatePlan(r1.text.replace(T3_FROM, T3_TO));
|
||||
const cs = c.reasons.find(x => x.code === 'PLAN_GATE_VERDICT_STALE');
|
||||
assert.ok(cs, JSON.stringify(c.reasons));
|
||||
assert.match(cs.message, /run the next round/);
|
||||
});
|
||||
|
||||
/** The verdict block of `text` replaced by `fn(verdict)`. */
|
||||
const putVerdict = (text, fn) => text.replace(/```json\n[\s\S]*?\n```/, () => '```json\n' + JSON.stringify(fn(extractVerdict(text).verdict), null, 2) + '\n```');
|
||||
|
||||
test('gate guard: a planted stop-rule acceptance that claims an open SECURITY blocker leaves the gate at STOP', () => {
|
||||
const t1 = buildPlan();
|
||||
const r1 = rec(t1, dd({ pc: [f('blocker', at(t1, R1_CHANGES), B1, 'SECURITY_INJECTION')] }));
|
||||
const id = r1.verdict.findings[0].id;
|
||||
const t2 = editA(r1.text);
|
||||
const r2 = rec(t2, dd({
|
||||
pc: [f('blocker', at(t2, 'its error path'), B2, 'SECURITY_INJECTION')],
|
||||
pcPrev: [{ id, resolved: true }],
|
||||
}), { base: baseOf(r1.text) });
|
||||
assert.equal(r2.verdict.stop_rule?.fired, true);
|
||||
const planted = putVerdict(r2.text, v => ({
|
||||
...v,
|
||||
accepted_open: { round: v.round, plan_sha256: v.plan_sha256, accepted_at: NOW, under: 'stop-rule', findings: v.findings.map(x => ({ id: x.id ?? null, text: x.text ?? '' })) },
|
||||
}));
|
||||
const ex = extractVerdict(planted);
|
||||
assert.equal(ex.error, null);
|
||||
assert.equal(ex.verdict.accepted_open.under, 'stop-rule');
|
||||
assert.equal(planHash(planted), planHash(r2.text), 'the planted acceptance is review log');
|
||||
const g = gatePlan(planted);
|
||||
assert.equal(g.code, 1, JSON.stringify(g));
|
||||
assert.equal(g.status, 'stop');
|
||||
assert.ok(g.reasons.some(x => x.code === 'PLAN_GATE_STOP_RULE'), JSON.stringify(g.reasons));
|
||||
});
|
||||
|
||||
test('gate guard: a planted acceptance covering a blocker without `under: "stop-rule"` leaves the gate at STOP', () => {
|
||||
const { r2 } = sameClassTwice();
|
||||
const planted = putVerdict(r2.text, v => ({
|
||||
...v,
|
||||
accepted_open: { round: v.round, plan_sha256: v.plan_sha256, accepted_at: NOW, findings: v.findings.map(x => ({ id: x.id ?? null, text: x.text ?? '' })) },
|
||||
}));
|
||||
const ex = extractVerdict(planted);
|
||||
assert.equal(ex.error, null);
|
||||
assert.equal(ex.verdict.accepted_open.under, undefined);
|
||||
assert.equal(planHash(planted), planHash(r2.text), 'the planted acceptance is review log');
|
||||
const g = gatePlan(planted);
|
||||
assert.equal(g.code, 1, JSON.stringify(g));
|
||||
assert.equal(g.status, 'stop');
|
||||
assert.ok(g.reasons.some(x => x.code === 'PLAN_GATE_STOP_RULE'), JSON.stringify(g.reasons));
|
||||
});
|
||||
|
||||
// ---- --accept-open under the stop rule (220) ----------------------------------------
|
||||
|
||||
test('accept-open: after a fired stop rule it is allowed with rounds left, covers the blocker, and the gate warns', () => {
|
||||
|
|
@ -216,6 +280,40 @@ test('accept-open: without the stop rule it behaves as before (rounds left -> re
|
|||
const a = acceptOpen(r1.text, { now: NOW });
|
||||
assert.equal(a.code, 1);
|
||||
assert.match(a.message, /round\(s\) left/);
|
||||
// A blocker open, the stop rule not fired: refused as a blocker.
|
||||
const b1 = rec(t1, dd({ pc: [f('blocker', at(t1, R1_CHANGES), B1, 'dep/manifest')] }));
|
||||
assert.equal(b1.verdict.stop_rule?.fired, false);
|
||||
const b = acceptOpen(b1.text, { now: NOW });
|
||||
assert.equal(b.code, 1);
|
||||
assert.match(b.message, /a blocker is never accepted/);
|
||||
assert.equal(b.text, undefined, 'nothing is written');
|
||||
});
|
||||
|
||||
const M3 = 'Rollback wording in the second step names a script that the repository lacks';
|
||||
|
||||
test('accept-open: after a fired stop rule it covers an open MAJOR (no blocker) and the gate passes with a WARN', () => {
|
||||
const t1 = buildPlan();
|
||||
const r1 = rec(t1, dd({ pc: [f('major', at(t1, R1_CHANGES), M2, 'rollback/wording')] }));
|
||||
const id = r1.verdict.findings[0].id;
|
||||
const t2 = editA(r1.text);
|
||||
const r2 = rec(t2, dd({
|
||||
pc: [f('major', at(t2, 'its error path'), M3, 'rollback/missing-script')],
|
||||
pcPrev: [{ id, resolved: true }],
|
||||
}), { base: baseOf(r1.text) });
|
||||
assert.equal(r2.verdict.stop_rule?.fired, true, JSON.stringify(r2.verdict.stop_rule));
|
||||
assert.deepEqual(r2.verdict.findings.map(x => x.severity.toLowerCase()), ['major']);
|
||||
// Before the acceptance: STOP on the stop rule.
|
||||
assert.deepEqual(gatePlan(r2.text).reasons.map(x => x.code), ['PLAN_GATE_STOP_RULE']);
|
||||
const a = acceptOpen(r2.text, { now: NOW });
|
||||
assert.equal(a.code, 0, a.message);
|
||||
const v = extractVerdict(a.text).verdict;
|
||||
assert.equal(v.accepted_open.under, 'stop-rule');
|
||||
assert.deepEqual(v.accepted_open.findings.map(x => x.text), [M3]);
|
||||
const g = gatePlan(a.text);
|
||||
assert.equal(g.code, 0, JSON.stringify(g.reasons));
|
||||
assert.equal(g.status, 'pass');
|
||||
assert.ok(g.warnings.some(w => w.code === 'PLAN_GATE_OPEN_MAJOR' && w.message.includes(M3)), JSON.stringify(g.warnings));
|
||||
assert.ok(!g.warnings.some(w => w.code === 'PLAN_GATE_OPEN_BLOCKER'), JSON.stringify(g.warnings));
|
||||
});
|
||||
|
||||
// ---- known issue: the Pass 2 heading is plan text now ---------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue