diff --git a/test/ship-document-release-dispatch.test.ts b/test/ship-document-release-dispatch.test.ts index 118124a00..638fd0124 100644 --- a/test/ship-document-release-dispatch.test.ts +++ b/test/ship-document-release-dispatch.test.ts @@ -59,12 +59,16 @@ describe('/ship Step 18 dispatches /document-release (carve visibility)', () => expect(content).toContain('"documentation_section"'); // Deliberate design: docs sync never holds a ship hostage. expect(content).toContain('Do not block /ship on subagent failure'); - // These two strings are the ship-docsync E2E's dispatch-matcher markers - // (test/skill-e2e-ship-docsync.test.ts) — they come verbatim from the - // dictated Step 18 subagent prompt. Rewording them in pr-body.md.tmpl - // silently decouples the paid matcher; update both in lockstep. + // These four strings are the ship-docsync E2E's dispatch-matcher markers + // (test/skill-e2e-ship-docsync.test.ts): the first two are INCLUSION + // markers (verbatim from the dictated Step 18 subagent prompt); the last + // two are EXCLUSION markers (section scaffolding that disqualifies a + // whole-section paste). Rewording any of them in pr-body.md.tmpl silently + // deadens the paid matcher; update all four in lockstep. expect(content).toContain('You are executing the /document-release workflow'); expect(content).toContain('.claude/skills/gstack/document-release/SKILL.md'); + expect(content).toContain('## Step 19: Create PR/MR'); + expect(content).toContain('Parent processing:'); } }); @@ -86,6 +90,15 @@ describe('/ship Step 18 dispatches /document-release (carve visibility)', () => expect(content).toMatch( /> \*\*STOP\.\*\*[^\n]*Read `[^`]*ship\/sections\/pr-body\.md`/ ); + // Ordering pin: the hoisted invariant must sit ABOVE the pr-body STOP + // pointer (mustStayInSkeleton asserts presence only — a future edit could + // drift the paragraph below the STOP with every registry check green). + const invariantIdx = content.indexOf('**Doc-sync invariant'); + const stopIdx = content.indexOf( + '> **STOP.** Before dispatching the /document-release subagent' + ); + expect(invariantIdx).toBeGreaterThan(-1); + expect(stopIdx).toBeGreaterThan(invariantIdx); }); test('the dispatch imperative stays carved out of the claude skeleton', () => { diff --git a/test/skill-e2e-ship-docsync.test.ts b/test/skill-e2e-ship-docsync.test.ts index 048d626cb..ba0d7f1df 100644 --- a/test/skill-e2e-ship-docsync.test.ts +++ b/test/skill-e2e-ship-docsync.test.ts @@ -129,7 +129,11 @@ describeE2E('Ship doc-sync dispatch E2E (gate)', () => { path.join(repoDir, 'CHANGELOG.md'), '# Changelog\n\n## [0.1.0.0] - 2026-01-01\n\n- Initial release\n' ); - run('git', ['add', 'app.ts', 'VERSION', 'CHANGELOG.md']); + // The cwd-relative pr-body plant (below) lives inside this working tree; + // ignore it so the fixture repo stays clean and the agent never tries to + // commit test scaffolding. + fs.writeFileSync(path.join(repoDir, '.gitignore'), 'ship/\n'); + run('git', ['add', 'app.ts', 'VERSION', 'CHANGELOG.md', '.gitignore']); run('git', ['commit', '-m', 'initial']); run('git', ['push', '-u', 'origin', 'main']); run('git', ['checkout', '-b', 'feature/docsync-test']); @@ -167,6 +171,11 @@ describeE2E('Ship doc-sync dispatch E2E (gate)', () => { fs.mkdirSync(path.join(plantedSkills, 'document-release'), { recursive: true }); fs.writeFileSync(path.join(plantedSkills, 'ship', 'sections', 'pr-body.md'), prBody); fs.writeFileSync(path.join(workDir, 'ship', 'sections', 'pr-body.md'), prBody); + // Third plant: resolvable relative to the agent's cwd (repoDir), not just + // relative to SKILL-tail.md — saves a wasted turn if the agent tries a + // cwd-relative read before the ~ path. + fs.mkdirSync(path.join(repoDir, 'ship', 'sections'), { recursive: true }); + fs.writeFileSync(path.join(repoDir, 'ship', 'sections', 'pr-body.md'), prBody); fs.writeFileSync( path.join(plantedSkills, 'document-release', 'SKILL.md'), DOC_RELEASE_STUB