Files
gstack/test/ship-control-flow.test.ts
Garry Tanandcapy-ai[bot] 148d28ea81 feat(ship): /ship and /autoplan size their review to the diff (GSTA-131) (#3527)
* wip(GSTA-131): review depth, fix-check transitions, dispatch counters, carry pregate, autoplan round policy

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

* wip(GSTA-131): reviewers stay at full effort; audits light

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

* feat(ship): Step 9 dispatches what gstack-review-plan plans; fix-check after below-floor fixes; cap two full passes plus one fix-check

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

* test: review-dispatch-compare has a free lane; review-depth fixtures registered free; resumed max_rounds case off the old default

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

* fix(GSTA-131): ship review pass-1 repairs: fix-check binds under its own skill, Step 11.5 accepts a fix-check or a planned adversarial skip, delta carry with a pregate, carry --pregate pinned to the checkout, quiet-after-cap revise regates, skill-dir markdown and rule docs classed code, gate signals from tests class up, plan fails closed on git errors, per-class compare ratios

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

* fix(GSTA-131): ship review pass-2 repairs: Step 11 follows the last full-pass plan, Important Rules restored, ship goldens regenerated, true per-class medians, review-policy transitions and once-per-invocation voices, docs/** assets stay docs-class, CLI and fix-check binding tests

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

* docs(changelog): GSTA-131 review depth, fix-check, delta /ship and autoplan rounds

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

* docs: ship-policy cap and counters, review-policy /review cycle cap, unattended passes (docs audit)

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

* fix(review): 400-word findings cap fits the /review size pins (five host pins lowered by 2); CHANGELOG max_rounds wording

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

* docs(changelog): gate integrity names the size-ledger entry and the lowered /review pins

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

* test: the round-budget case asserts the default through ROUNDS output only (pregate literals heuristic)

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

* test: curate ship-receipt-carry-pregate out of the Windows lane like its --pregate siblings

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

* docs(changelog): end the Windows curation sentence instead of a mid-sentence colon

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

* fix(ship): one decision each for the review-loop exits, the planned adversarial skip and Step 16's TODOS rerun (screen judge repair)

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

* fix(ship): fix-check repairs: pin the renamed Step 11 heading, keep the below-floor exit at the cap, name the plan run a repeat reuses, print the native skip on outside-only plans

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>
2026-10-28 05:45:54 +00:00

360 lines
22 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { ALL_HOST_CONFIGS } from '../hosts';
import { generateAdversarialStep } from '../scripts/resolvers/outside-voice-steps';
import { generatePlanCompletionGateShip } from '../scripts/resolvers/plan-gates';
import { generateReviewDashboard } from '../scripts/resolvers/review-dashboard';
import { HOST_PATHS } from '../scripts/resolvers/types';
import { expectMentions } from './helpers/prompt-structure';
const read = (file: string) => readFileSync(new URL(`../${file}`, import.meta.url), 'utf8');
const compact = (text: string) => text.replace(/\s+/g, ' ');
const entry = read('ship/SKILL.md.tmpl');
const start = entry.indexOf('### Ship control flow');
const end = entry.indexOf('{{SECTION_INDEX:ship}}');
const controller = entry.slice(start, end);
const review = compact(read('ship/sections/review-army.md.tmpl'));
const adversarial = compact(generateAdversarialStep({ host: 'claude', skillName: 'ship', tmplPath: '', paths: HOST_PATHS.claude }));
const gate = compact(entry.slice(entry.indexOf('## Step 16:'), entry.indexOf('## Step 17:')));
describe('ship source controller', () => {
test('documentation freshness covers selected code paths as well as release metadata', () => {
const docs = compact(read('ship/sections/documentation.md.tmpl'));
// Meaning: the snapshot hashes selected release (code) paths, not only release metadata.
expect(docs).toContain('hashes of release paths, generated outputs and docs');
expect(docs).toContain('`--select <path>` narrows release paths');
const freshness = gate.slice(gate.indexOf('### 3.'), gate.indexOf('### 4.'));
expectMentions(freshness, [['never', 'qualifies', 'decision']], 'freshness');
});
test('changed documentation inputs permit only the remaining bounded re-audit', () => {
const docs = compact(read('ship/sections/documentation.md.tmpl'));
expect(docs.indexOf('## Blocked recovery')).toBeGreaterThan(-1);
expect(docs).toMatch(/never a third attempt/i);
const freshness = gate.slice(gate.indexOf('### 3.'), gate.indexOf('### 4.'));
expect(freshness).toContain('**An attempt remains, with changed inputs or an available repair:**');
expect(freshness).toMatch(/\*\*Otherwise:\*\* stop unless the user accepts/i);
expect(freshness).not.toContain('**No attempt remains or no repair is available:**');
});
test('the review plan drives the repeat, the fix-check binds under its own skill, and Step 11.5 accepts a fix-check or a planned adversarial skip', () => {
const decide = review.slice(review.indexOf('### Decide whether to repeat Step 9'), review.indexOf('**Required-probe parent gate:**'));
for (const flag of ['--fixed-blocking', '--fixed-below', '--blocking-open', '--touched-gate', '--critical-found', '--adversarial-ran', '--reviewed-numbers <fp>']) expect(decide).toContain(flag);
expect(decide).toContain('its `NEXT:` alone picks the next decision: `stop` 2, `full` 3, `fix-check` 3.5, `done` 4.');
expect(decide).toContain('`gstack-review-log --start review-fix-check` token');
expect(decide).toContain('`"skill":"review-fix-check"` with `--finish <token>`');
expect(compact(read('ship/sections/adversarial.md.tmpl'))).toContain("Step 9's last full-pass plan governs this step");
const bind = compact(entry.slice(entry.indexOf('## Step 11.5:'), entry.indexOf('## Step 12:')));
expect(bind).toContain("last 3.5 `review-fix-check`");
expect(bind).toContain('none after `ADVERSARIAL: skipped`');
expect(bind).toContain('`review_freshness` `CURRENT`');
});
test('Step 1 carries a receipt only after a dry run and a pregate at HEAD', () => {
const carry = compact(entry.slice(entry.indexOf('4. **Carry check.**'), entry.indexOf('5. Display historical readiness')));
const order = ['carry --pr <n> --base <base> --dry-run', '`CARRY: would carry`', 'skip review', '`gstack-pregate --allow-repo-commands`', 'carry without `--dry-run`', '`--pregate .gstack/tmp/pregate.json`', '`CARRY: carried` binds HEAD', 'A failed check, HEAD moving after pregate, `refused` or `current` re-runs Steps 2–16.'];
const at = order.map(t => carry.indexOf(t));
expect(at.every(i => i >= 0)).toBe(true);
expect(at).toEqual([...at].sort((a, b) => a - b));
});
test('repairs use one ordered work list without a return stack', () => {
const text = compact(controller);
expect(compact(entry)).toContain('**Next steps:**');
expect(text).toContain('5 → 6 → 7 → 8 → 9 → 10 → 11 → 11.5');
expect(text).toMatch(/initial-plus-one limit never resets/i);
});
test('the roadmap and recovery groups define ownership and zero-edit eligibility', () => {
const text = compact(entry);
expect(text).toContain('integrate (1–3) → test and review (4–11.5) → prepare the release (12–15) → verify frozen content (16) → push and publish (17–21)');
expectMentions(text, [['not', 'permission', 'children']], 'text');
expect(review).toContain('**No edits in this pass:**');
expectMentions(review, [['only', 'continue', 'clears']], 'review');
expectMentions(review, [['block', 'failed/unavailable', 'continuation']], 'review');
expectMentions(review, [['cannot', 'test/security', 'independent']], 'review');
expectMentions(text, [['never', 'approvals', 'expands']], 'text');
expect(text).not.toContain('eligible zero-edit pass');
expect(text).not.toContain('the same waiver');
expect(text).not.toContain("the controller's detour");
});
test('distribution discovery is a shortlist, not a manifest-only artifact decision', () => {
const distribution = compact(entry.slice(entry.indexOf('## Step 2:'), entry.indexOf('## Step 3:')));
expect(distribution).toContain('**New artifact without a pipeline');
expect(distribution).toContain('AskUserQuestion');
expect(distribution).toMatch(/do not publish a release during `\/ship`/i);
});
test('queue qualification exhaustively distinguishes online, git fallback and unusable output', () => {
const version = compact(entry.slice(entry.indexOf('## Step 12:'), entry.indexOf('{{SECTION:changelog}}')));
const qualify = version.indexOf('**Qualify first:**');
const usable = version.indexOf('**Usable candidate:**');
const missing = version.indexOf('**No usable candidate:**');
expect(qualify).toBeGreaterThan(0);
expect(usable).toBeGreaterThan(qualify);
expect(missing).toBeGreaterThan(usable);
expect(version).toContain('`NEW_VERSION=CANDIDATE_VERSION`');
expectMentions(version, [['only', 'approval', 'existing']], 'version');
expect(version).toContain('`>= NEW_VERSION`');
expect(version.slice(missing)).toContain('`BUMP_LEVEL`');
expect(version.slice(missing)).toContain('`currentVersion`');
});
test.each(['home', 'override', 'plugin'])('the actual nudge shell honors %s roots, repeat suppression and enabled tuning', mode => {
const root = mkdtempSync(join(tmpdir(), 'gstack-ship-nudge-'));
try {
const home = join(root, 'home');
const state = mode === 'home' ? join(home, '.gstack') : join(root, 'state root');
mkdirSync(home, { recursive: true });
const nudge = entry.slice(entry.indexOf('## Step 21:'), entry.indexOf('## Section self-check'));
const block = nudge.match(/```bash\n([\s\S]*?)\n```/);
expect(block).not.toBeNull();
const script = block![1].replaceAll('~/.claude/skills/gstack', JSON.stringify(resolve(import.meta.dir, '..')));
const run = () => spawnSync('bash', ['-c', script], {
cwd: root,
env: {
PATH: process.env.PATH,
HOME: home,
TMPDIR: join(root, 'tmp'),
...(mode === 'override' ? { GSTACK_HOME: state } : {}),
...(mode === 'plugin' ? { CLAUDE_PLUGIN_ROOT: join(root, 'gstack'), CLAUDE_PLUGIN_DATA: state } : {}),
},
encoding: 'utf8',
timeout: 10_000,
});
const marker = join(state, '.plan-tune-nudge-shown');
const first = run();
expect(first.status).toBe(0);
expect(first.stdout).toContain('Run /plan-tune to opt in');
expect(existsSync(marker)).toBe(true);
if (mode !== 'home') expect(existsSync(join(home, '.gstack', '.plan-tune-nudge-shown'))).toBe(false);
const repeated = run();
expect(repeated.status).toBe(0);
expect(repeated.stdout).toBe('');
rmSync(marker);
writeFileSync(join(state, 'config.yaml'), 'question_tuning: true\n');
const enabled = run();
expect(enabled.status).toBe(0);
expect(enabled.stdout).toBe('');
expect(existsSync(marker)).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('nested repairs resume the unfinished outer range before its destination', () => {
const text = compact(controller);
expect(text).toContain('`9 → 10 → 11` before 11.5');
expect(text).toContain('`5 → 6 → 7 → 8 → 9 → 10 → 11 → 11.5`');
expect(text).not.toContain('Enter Step 9 before REVIEW_START capture and full review');
});
test('shared foreground dispatch stays mandatory at all three owned dispatch sites', () => {
const coverage = read('ship/sections/test-coverage.md.tmpl');
const shared = coverage.indexOf('### Shared subagent dispatch');
expect(shared).toBeGreaterThan(0);
expect(shared).toBeLessThan(coverage.indexOf('Dispatch the audit through Agent'));
const contract = compact(coverage.slice(shared, coverage.indexOf('Dispatch the audit through Agent')));
expect(contract).toContain('use the Agent tool with `run_in_background: false`');
expectMentions(contract, [['do not', 'instead', 'invoke']], 'contract');
for (const section of ['test-coverage', 'plan-completion', 'greptile']) {
const text = read(`ship/sections/${section}.md.tmpl`);
expect(text).toContain('shared foreground-dispatch rule');
expect(text).toContain('run_in_background: false');
expect(text).not.toContain('{{FOREGROUND_DISPATCH_NOTE}}');
}
expect(entry).not.toContain('### Shared subagent dispatch');
for (const section of ['plan-completion', 'greptile']) {
expect(read(`ship/sections/${section}.md.tmpl`)).toContain("Step 7's shared foreground-dispatch rule");
}
});
test('ship dashboard displays actual historical records without a repeated sample panel', () => {
const ctx = { host: 'claude' as const, skillName: 'ship', tmplPath: '', paths: HOST_PATHS.claude };
const ship = generateReviewDashboard(ctx);
expect(ship).toContain('REVIEW READINESS DASHBOARD');
expect(ship).toContain('| Review | Runs | Last run | Status | Required |');
expectMentions(ship, [['only', 'required', 'review']], 'ship');
expect(ship).toContain('{actual status and reason}');
expect(ship).toContain('VERDICT: {CLEARED or NOT CLEARED} — {reason}');
expect(ship).not.toContain('+====================================================================+');
const review = generateReviewDashboard({ ...ctx, skillName: 'review' });
expect(review).toContain('+====================================================================+');
});
test('coverage fallback settles the child and still applies the coverage gate', () => {
const text = compact(read('ship/sections/test-coverage.md.tmpl'));
const fallback = text.slice(text.indexOf('**Audit failure:**'), text.indexOf('{{TEST_COVERAGE_GATE_SHIP}}'));
expectMentions(fallback, [['confirm', 'stopped', 'running']], 'fallback');
expectMentions(fallback, [['does not', 'coverage', 'bypass']], 'fallback');
expect(text).not.toContain('partial results are better than none');
});
test('no-plan gate skips only the audit and retains verification and the remaining section', () => {
const text = generatePlanCompletionGateShip({ host: 'claude', skillName: 'ship', tmplPath: '', paths: HOST_PATHS.claude });
const noPlan = compact(text.slice(text.indexOf('**No plan file found:**')));
expectMentions(noPlan, [['only', 'completion', 'audit']], 'noPlan');
expect(noPlan).toContain('Step 8.1');
expect(noPlan).toMatch(/Step 9 QA still runs/i);
expect(noPlan).not.toContain('Skip entirely');
});
test('binding separates record selection, three snapshots and probe outcomes', () => {
const text = compact(entry.slice(entry.indexOf('## Step 11.5:'), entry.indexOf('## Step 12:')));
const steps = ['1. **Select', '2. **Compare', '3. **Preserve', '4. **Save'];
const positions = steps.map(step => text.indexOf(step));
expect(positions.every(position => position >= 0)).toBe(true);
expect(positions).toEqual([...positions].sort((a,b) => a-b));
expectMentions(text, [['must', 'snapshots', 'three']], 'text');
expectMentions(text, [['does not', 'failed', 'probes']], 'text');
});
test('late classification and docs freshness are ordered decisions, not metadata assumptions', () => {
const text = compact(entry.slice(entry.indexOf('## Step 16:'), entry.indexOf('## Step 17:')));
expect(text).toContain('**Behavior, tests or build inputs changed:**');
expect(text).toContain('**Only authored docs or release metadata changed:**');
expect(text).toContain('**No changes, or the docs-only checks still support the plan:**');
expect(text).toContain('| Continue to stage 4');
expect(text).not.toContain('Every listed change below is metadata:');
});
test('one early controller owns detours without replacing local gates', () => {
expect(start).toBeGreaterThan(entry.indexOf('# Ship:'));
expect(start).toBeLessThan(end);
expect(end).toBeLessThan(entry.indexOf('{{BASE_BRANCH_DETECT}}'));
expect(entry.match(/### Ship control flow/g)).toHaveLength(1);
const text = compact(controller);
expect(text).toMatch(/the \*\*parent\*\* running \/ship, own advancement/i);
expectMentions(text, [['stop', 'askuserquestion', 'repairs']], 'text');
expectMentions(text, [['never', 'unlisted', 'range']], 'text');
expectMentions(text, [['does not', 'ending', 'range']], 'text');
expect(text).not.toContain('| At step |');
expect(entry.match(/Ship control flow/g)).toHaveLength(1);
expect(review).toMatch(/captures a fresh REVIEW_START/);
});
test('distribution and bootstrap detours end at their exact forward resume', () => {
const merge = compact(entry.slice(entry.indexOf('## Step 3:'), entry.indexOf('{{SECTION:tests}}')));
const tests = compact(read('ship/sections/tests.md.tmpl'));
expect(tests).toContain('A) Add tests (recommended)');
expectMentions(tests, [['not', 'declining', 'bootstrap']], 'tests');
});
test('missing dispatched output outranks the cycle cap, fixes and zero-edit continuation', () => {
const decisions = review.slice(review.indexOf('### Decide whether to repeat Step 9'));
const names = ['1. **Dispatched reviewer output missing:**', '2. **Third fixing cycle reached (`CYCLES >= 3`):**', '3. **Fixes applied below the cap:**', '4. **No edits in this pass:**'];
const positions = names.map(name => decisions.indexOf(name));
expect(positions.every(position => position >= 0)).toBe(true);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
const missing = decisions.slice(positions[0], positions[1]);
expect(missing).toMatch(/^1\. \*\*Dispatched reviewer output missing:\*\* stop\b/i);
expectMentions(missing, [['cannot', 'dispatched', 'coverage']], 'missing');
const cap = decisions.slice(positions[1], positions[2]);
expect(cap).toMatch(/\bstop\b/i);
expect(cap).toContain('`converged:false`');
expectMentions(cap, [['do not', 'fourth', 'fixing']], 'cap');
const fixes = decisions.slice(positions[2], positions[3]);
expectMentions(fixes, [['before', 'affected', 'pending']], 'fixes');
expectMentions(fixes, [['approval', 'pre-existing', 'verified']], 'fixes');
expectMentions(decisions.slice(positions[3]), [['only', 'continue', 'clears']], 'decisions.slice(positions[3])');
expectMentions(review, [['never', 'inconclusive', 'required']], 'review');
});
test('comment fixes resume triage and preserve settled approvals and replies', () => {
const section = compact(read('ship/sections/greptile.md.tmpl'));
expectMentions(review, [['before', 'complete', 'applying']], 'review');
expectMentions(section, [['no', 'continue', 'queued']], 'section');
expectMentions(section, [['does not', 'optional', 'triage']], 'section');
expect(section).not.toContain('return to Step 9');
});
test('native recovery has one corrected attempt and cannot borrow outside completion', () => {
const finish = adversarial.slice(adversarial.indexOf('### Finish the adversarial phase'));
const recovery = finish.slice(finish.indexOf('1. **Required native'), finish.indexOf('2. **Fixes queued'));
expectMentions(recovery, [['stop', 'confirm', 'stopped']], 'recovery');
expectMentions(recovery, [['cannot', 'outside-provider', 'replace']], 'recovery');
expectMentions(recovery, [['only', 'prerequisite', 'correction']], 'recovery');
expect(recovery).toContain('PASS_START');
expectMentions(recovery, [['ask', 'blocked', 'repair']], 'recovery');
const queued = finish.slice(finish.indexOf('2. **Fixes queued'), finish.indexOf('3. **Native complete'));
expectMentions(queued, [['before', 'pending', 'insert']], 'queued');
expect(queued).toMatch(/not recovery retries/i);
expect(finish.slice(finish.indexOf('3. **Native complete'))).toContain('then continue to Step 11.5');
});
test.each(ALL_HOST_CONFIGS.map(({ name }) => name))('%s cannot skip Step 11.5 or change standalone review control flow', host => {
const ctx = { host, skillName: 'ship', tmplPath: '', paths: HOST_PATHS[host] };
const ship = generateAdversarialStep(ctx);
const finish = compact(ship.slice(ship.indexOf('### Finish the adversarial phase')));
const branches = ['Required native review incomplete', 'Fixes queued after native completion', 'Native complete with no queued fixes']
.map(label => finish.indexOf(label));
expect(branches.every(index => index >= 0)).toBe(true);
expect(branches).toEqual([...branches].sort((a, b) => a - b));
expectMentions(finish, [['cannot', 'outside-provider', 'replace']], 'finish');
expect(finish).toContain('Step 11.5');
expect(finish).not.toContain('proceed to Step 12');
expect(finish).not.toContain('return to Step 9');
const review = generateAdversarialStep({ ...ctx, skillName: 'review' });
expect(review).not.toContain('Ship control flow');
expect(review).not.toContain('Step 11.5');
expectMentions(review, [['do not', 'repair', 'start']], 'review');
});
test('Step 11.5 verifies original record identity before any release write', () => {
const bindingStart = entry.indexOf('## Step 11.5:');
expect(bindingStart).toBeGreaterThan(entry.indexOf('{{SECTION:adversarial}}'));
expect(bindingStart).toBeLessThan(entry.indexOf('## Step 12:'));
const binding = compact(entry.slice(bindingStart, entry.indexOf('## Step 12:')));
for (const field of ['skill:"review"', 'via:"ship"', 'skill:"adversarial-review"',
'review_binding.state', 'verified', 'review_binding.start_wtree', 'review_binding.end_wtree']) expect(binding).toContain(field);
expectMentions(binding, [['never', 'attach', 'tokens']], 'binding');
expectMentions(binding, [['does not', 'failed', 'probes']], 'binding');
expect(binding).toContain('`9 → 10 → 11 → 11.5` before Step 12');
});
test('late build and behavior changes complete bounded ranges before docs', () => {
const build = gate.slice(gate.indexOf('### 1.'), gate.indexOf('### 2.'));
expect(build).toMatch(/repeat stage 1/i);
expect(build).toMatch(/continue to stage 2/i);
const behavior = gate.slice(gate.indexOf('1. **Behavior'), gate.indexOf('2. **Only authored'));
expect(behavior).toContain('Insert `5–11.5 → 12–14 → 16` before the pending Step 17');
expect(behavior).toMatch(/excludes Step 14\.5/i);
expectMentions(behavior, [['before', 'rebuild', 'compare']], 'behavior');
const plan = gate.slice(gate.indexOf('2. **Only authored'), gate.indexOf('3. **No changes'));
expectMentions(plan, [['only', 'decision', 'return']], 'plan');
expectMentions(plan, [['never', 'yourself', 'counts']], 'plan');
});
test('docs freshness has bounded re-audit and exact-content exception routes', () => {
const docs = gate.slice(gate.indexOf('### 3.'), gate.indexOf('### 4.'));
expect(docs).toContain('**An attempt remains, with changed inputs or an available repair:** insert `14.5 → 15 → 16` before Step 17');
expectMentions(docs, [['stop', 'documentation', 'unwaivable']], 'docs');
expect(docs).toMatch(/never run a third audit/i);
expect(docs).toContain('`Documentation: blocked`');
expect(docs).toMatch(/never silently refresh hashes/i);
});
test('tests, absent test approval and new writes reopen final verification', () => {
const tests = gate.slice(gate.indexOf('### 4.'), gate.indexOf('### 5.'));
expect(tests).toMatch(/\*\*New, changed or unwaived test failure:\*\* stop publication\. Run Steps 5–15/i);
expectMentions(tests, [['no', 'including', 'no-tests']], 'tests');
expectMentions(tests, [['does not', 'authorize', 'attempt']], 'tests');
expectMentions(gate, [['before', 'complete', 'restart']], 'gate');
});
test('push failure kinds and publication races have distinct resumes', () => {
const push = compact(entry.slice(entry.indexOf('## Step 17:'), entry.indexOf('## Step 18:')));
expectMentions(push, [['no', 'publication', 'fails']], 'push');
expectMentions(push, [['before', 'returning', 'steps']], 'push');
expect(push).toMatch(/never rewrite history/i);
expect(push).toMatch(/never bypass failed guards/i);
const publication = compact(read('ship/sections/pr-body.md.tmpl'));
expect(publication).toMatch(/repeat Step 18's identity\/title preparation/i);
expectMentions(publication, [['fresh body', 'before', 'publishing']], 'publication');
});
});