mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* 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>
360 lines
22 KiB
TypeScript
360 lines
22 KiB
TypeScript
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');
|
||
});
|
||
});
|