From 43ce82ebbba719c65da015165adfa1fb558a2197 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 7 Jun 2026 19:37:35 -0700 Subject: [PATCH] refactor(test): fold ship PR-title skeleton guard into carve-guard registry main shipped a generalized carve-guard system (PR #1907) that is now the single source of truth for carved-skill skeleton invariants. Register the PR-title rule there instead of a standalone test: ship's mustStayInSkeleton asserts v$NEW_VERSION + the rewrite helper stay always-loaded, and mustMoveToSection asserts both the create and update PR paths stay carved into pr-body.md (present in the union, out of the skeleton). Delete the standalone ship-pr-title-version-always-loaded test it replaces. The CI-workflow safety tripwire stays standalone (not a carve concern). Co-Authored-By: Claude Opus 4.8 (1M context) --- test/helpers/carve-guards.ts | 10 +- ...hip-pr-title-version-always-loaded.test.ts | 92 ------------------- 2 files changed, 8 insertions(+), 94 deletions(-) delete mode 100644 test/ship-pr-title-version-always-loaded.test.ts diff --git a/test/helpers/carve-guards.ts b/test/helpers/carve-guards.ts index a56ef9a07..6919f284f 100644 --- a/test/helpers/carve-guards.ts +++ b/test/helpers/carve-guards.ts @@ -106,8 +106,14 @@ export const CARVE_GUARDS: Record = { scenario: 'This is a FRESH version-changing ship: the branch has a real code change, VERSION still equals the base version (needs a bump), and CHANGELOG.md needs a new entry. Follow the skill flow for a version-changing ship: run the pre-landing review and prepare the CHANGELOG entry. Produce the ship plan / review report. Do NOT actually commit, push, or open a PR.', staticInvariants: { - mustStayInSkeleton: [], - mustMoveToSection: [], + // The PR-title-version invariant MUST stay always-loaded: the v1.54.0.0 + // carve stranded it in pr-body.md and PRs started landing with bare titles + // (CI backstop: test/pr-title-sync-workflow-safety.test.ts). + mustStayInSkeleton: ['v$NEW_VERSION', 'gstack-pr-title-rewrite'], + // ...while the full create/update procedure stays carved into pr-body.md + // (out of the skeleton, present in the union). Asserts BOTH PR paths + // survive: the create path and the idempotent update path. + mustMoveToSection: ['gh pr create --base', 'gh pr edit --title'], // ship is operational (multi-STOP, not a plan review); no single post-STOP gate. gateAfterStop: undefined, }, diff --git a/test/ship-pr-title-version-always-loaded.test.ts b/test/ship-pr-title-version-always-loaded.test.ts deleted file mode 100644 index fe5bcea5e..000000000 --- a/test/ship-pr-title-version-always-loaded.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * /ship PR-title-version rule is ALWAYS-LOADED — token-reduction safety net (gate, free). - * - * The anxiety this kills: the v1.54.0.0 carve ("carve /ship into skeleton + - * on-demand sections, -59% always-loaded") moved the rule "PR title MUST start - * with v$NEW_VERSION" out of the always-loaded monolith and entirely into the - * lazily-loaded `ship/sections/pr-body.md`. The agent then sets the version - * prefix only if it happens to read that section before creating the PR; when it - * doesn't, PRs land with bare titles. This is the exact regression that shipped - * — every recent open PR lacked a `v...` prefix until this guard + the skeleton - * invariant restored it. - * - * This is the title-rule analogue of `test/auq-format-always-loaded.test.ts`, - * which guards the AskUserQuestion format the same way. A carve that re-buries - * the title rule fails here in milliseconds instead of surfacing weeks later as - * a wave of version-less PR titles. - * - * The guarantee, made mechanical and per-PR: - * 1. SKELETON PRESENCE — `ship/SKILL.md` (the always-loaded skeleton) carries - * the invariant: the `v$NEW_VERSION` token, the single-source-of-truth - * helper name, and a directive near a "PR title" mention. Present the - * instant /ship reaches the push/PR steps, no section read required. - * 2. UNION SURVIVAL (both paths) — the FULL procedure still exists somewhere - * in skeleton+sections for BOTH the create path (`gh pr create --title - * "v$NEW_VERSION ...`) AND the existing-PR update path (the `gh pr edit - * --title` rewrite rule). A drop of either is the failure. - */ -import { describe, test, expect } from 'bun:test'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; - -const ROOT = path.resolve(__dirname, '..'); -const SHIP_SKILL = path.join(ROOT, 'ship', 'SKILL.md'); -const SHIP_SECTIONS = path.join(ROOT, 'ship', 'sections'); - -function readUnion(): string { - let union = fs.readFileSync(SHIP_SKILL, 'utf-8'); - for (const f of fs.readdirSync(SHIP_SECTIONS)) { - if (f.endsWith('.md') && !f.endsWith('.md.tmpl')) { - union += '\n' + fs.readFileSync(path.join(SHIP_SECTIONS, f), 'utf-8'); - } - } - return union; -} - -describe('/ship PR-title-version rule is always-loaded (token-reduction safety net)', () => { - test('sanity: ship is a carved skill (has sections/*.md)', () => { - // Guards against a path regression that would make the union/skeleton checks - // vacuously pass against a non-carved skill. - expect(fs.existsSync(SHIP_SECTIONS)).toBe(true); - expect(fs.readdirSync(SHIP_SECTIONS).some(f => f.endsWith('.md'))).toBe(true); - }); - - test('skeleton (ship/SKILL.md) carries the PR-title-version invariant', () => { - const skeleton = fs.readFileSync(SHIP_SKILL, 'utf-8'); - // Robust independent markers, NOT one brittle full-sentence regex (so - // rewording the prose doesn't false-fail). All three must be present in the - // always-loaded skeleton. - const markers: Array<{ name: string; re: RegExp }> = [ - { name: 'v$NEW_VERSION token', re: /v\$NEW_VERSION/ }, - { name: 'gstack-pr-title-rewrite helper reference', re: /gstack-pr-title-rewrite/ }, - { name: 'a directive (MUST/always/never) near a PR-title mention', re: /(MUST|always|never)[^\n]{0,80}\btitle\b|\btitle\b[^\n]{0,80}(MUST|always|never)/i }, - ]; - const missing = markers.filter(m => !m.re.test(skeleton)); - if (missing.length > 0) { - throw new Error( - `ship/SKILL.md (the always-loaded skeleton) is missing the PR-title-version ` + - `invariant — a carve likely re-buried it in sections/. Missing:\n` + - missing.map(m => ` - ${m.name} (${m.re.source})`).join('\n'), - ); - } - }); - - test('union (skeleton+sections) keeps BOTH the create and the update title rules', () => { - const union = readUnion(); - const paths: Array<{ name: string; re: RegExp }> = [ - // create path: `gh pr create --title "v$NEW_VERSION ...` - { name: 'PR create path prefixes the version', re: /pr create[^\n]*--title[^\n]*v\$NEW_VERSION/i }, - // update/idempotent path: the existing-PR `gh pr edit --title` rewrite rule - { name: 'PR update path rewrites the title', re: /pr edit[^\n]*--title/i }, - ]; - const missing = paths.filter(p => !p.re.test(union)); - if (missing.length > 0) { - throw new Error( - `ship skeleton+sections dropped a PR-title-version code path. The update ` + - `path is the more important idempotent /ship path — a create-only guard ` + - `would miss its rot. Missing:\n` + - missing.map(p => ` - ${p.name} (${p.re.source})`).join('\n'), - ); - } - }); -});