diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index a405c2da9..39deb4ec1 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1390,6 +1390,75 @@ describe('INVOKE_SKILL resolver', () => { }); }); +// --- /land skill: composition into /land-and-deploy + generated-doc scrub (H9) --- + +describe('/land skill composition', () => { + const landTmpl = fs.readFileSync(path.join(ROOT, 'land', 'SKILL.md.tmpl'), 'utf-8'); + const landMd = fs.readFileSync(path.join(ROOT, 'land', 'SKILL.md'), 'utf-8'); + const ladTmpl = fs.readFileSync(path.join(ROOT, 'land-and-deploy', 'SKILL.md.tmpl'), 'utf-8'); + const ladMd = fs.readFileSync(path.join(ROOT, 'land-and-deploy', 'SKILL.md'), 'utf-8'); + + test('land-and-deploy composes /land via {{INVOKE_SKILL:land}}', () => { + expect(ladTmpl).toContain('{{INVOKE_SKILL:land}}'); + }); + + test('land-and-deploy SKILL.md resolves the composition prose to the land skill', () => { + expect(ladMd).toContain('land/SKILL.md'); + expect(ladMd).toContain('Follow its instructions from top to bottom'); + }); + + test('land-and-deploy no longer carries its own merge step (merge lives in /land)', () => { + // The parent must compose /land, not run gh pr merge itself. + expect(ladMd).not.toContain('gh pr merge --auto --delete-branch'); + expect(ladMd).not.toContain('## Step 4: Merge the PR'); + }); + + test('land-and-deploy consumes the handoff via gstack-merge read-state', () => { + expect(ladMd).toContain('gstack-merge read-state'); + expect(ladMd).toContain('last-land.json'); + }); + + // H9 — generated-doc scrub: extracting the land half "verbatim" risks dragging + // deploy/canary machinery into /land. Forbid the machinery tokens (not the words + // "deploy"/"canary", which legitimately appear in /land-and-deploy cross-refs). + test('/land SKILL.md carries no deploy/canary machinery (H9)', () => { + const forbidden = [ + 'deploy-reports', + 'DEPLOY_BOOTSTRAP', + '$B goto', + '$B console', + '$B perf', + '$B snapshot', + 'Canary verification', + 'Wait for deploy', + 'gh run list', + ]; + const offenders = forbidden.filter((s) => landMd.includes(s)); + expect(offenders).toEqual([]); + }); + + test('/land drives the merge through the gstack-merge helper', () => { + expect(landMd).toContain('gstack-merge submit'); + expect(landMd).toContain('gstack-merge wait'); + expect(landMd).toContain('gstack-merge write-state'); + expect(landMd).toContain('gstack-merge detect'); + }); + + test('/land uses {{BASE_BRANCH_DETECT}} so composition correctly skips the duplicate', () => { + // The INVOKE_SKILL skip-list skips "Step 0: Detect platform and base branch", + // which is exactly what BASE_BRANCH_DETECT emits — so the parent's detection + // wins when composed, and standalone /land runs its own. + expect(landTmpl).toContain('{{BASE_BRANCH_DETECT}}'); + }); + + test('/land documents all three merge regimes', () => { + expect(landMd).toContain('trunk'); + expect(landMd).toContain('/trunk merge'); + expect(landMd).toMatch(/gh pr merge .*--squash/); + expect(landMd).toContain('--auto'); + }); +}); + // --- {{CHANGELOG_WORKFLOW}} resolver tests --- describe('CHANGELOG_WORKFLOW resolver', () => { diff --git a/test/helpers/touchfiles.ts b/test/helpers/touchfiles.ts index b3c87b1e7..f2d40fff8 100644 --- a/test/helpers/touchfiles.ts +++ b/test/helpers/touchfiles.ts @@ -207,7 +207,7 @@ export const E2E_TOUCHFILES: Record = { // Ship 'ship-base-branch': ['ship/**', 'bin/gstack-repo-mode'], 'ship-local-workflow': ['ship/**', 'scripts/gen-skill-docs.ts'], - 'review-dashboard-via': ['ship/**', 'scripts/resolvers/review.ts', 'codex/**', 'autoplan/**', 'land-and-deploy/**'], + 'review-dashboard-via': ['ship/**', 'scripts/resolvers/review.ts', 'codex/**', 'autoplan/**', 'land-and-deploy/**', 'land/**'], 'ship-plan-completion': ['ship/**', 'scripts/gen-skill-docs.ts'], 'ship-plan-verification': ['ship/**', 'scripts/gen-skill-docs.ts'], @@ -287,13 +287,16 @@ export const E2E_TOUCHFILES: Record = { // gstack-upgrade 'gstack-upgrade-happy-path': ['gstack-upgrade/**'], - // Deploy skills - 'land-and-deploy-workflow': ['land-and-deploy/**', 'scripts/gen-skill-docs.ts'], - 'land-and-deploy-first-run': ['land-and-deploy/**', 'scripts/gen-skill-docs.ts', 'bin/gstack-slug'], - 'land-and-deploy-review-gate': ['land-and-deploy/**', 'bin/gstack-review-read'], + // Deploy skills. land-and-deploy now composes /land and drives merges through + // bin/gstack-merge (lib/merge.ts), so those are dependencies of every + // land-and-deploy E2E — a change to the land skill or the merge helper must + // re-run the composition path. + 'land-and-deploy-workflow': ['land-and-deploy/**', 'land/**', 'bin/gstack-merge', 'lib/merge.ts', 'scripts/gen-skill-docs.ts'], + 'land-and-deploy-first-run': ['land-and-deploy/**', 'land/**', 'bin/gstack-merge', 'lib/merge.ts', 'scripts/gen-skill-docs.ts', 'bin/gstack-slug'], + 'land-and-deploy-review-gate': ['land-and-deploy/**', 'land/**', 'bin/gstack-review-read'], 'canary-workflow': ['canary/**', 'browse/src/**'], 'benchmark-workflow': ['benchmark/**', 'browse/src/**'], - 'setup-deploy-workflow': ['setup-deploy/**', 'scripts/gen-skill-docs.ts'], + 'setup-deploy-workflow': ['setup-deploy/**', 'bin/gstack-merge', 'lib/merge.ts', 'scripts/gen-skill-docs.ts'], // Sidebar agent 'sidebar-navigate': ['browse/src/server.ts', 'browse/src/sidebar-agent.ts', 'browse/src/sidebar-utils.ts', 'extension/**'], diff --git a/test/land-and-deploy-postfail.test.ts b/test/land-and-deploy-postfail.test.ts index f89d77518..a0599e8c5 100644 --- a/test/land-and-deploy-postfail.test.ts +++ b/test/land-and-deploy-postfail.test.ts @@ -2,31 +2,30 @@ * Coverage for PR #1620 — Post-failure PR-state check after `gh pr merge` * non-zero exit. * - * The fix lives in land-and-deploy/SKILL.md.tmpl as Step §4a-postfail. - * After ANY non-zero `gh pr merge`, the skill must query authoritative PR - * state via `gh pr view --json state,mergeCommit,mergedAt,mergedBy` and - * branch on the result instead of retrying `gh pr merge` (cli/cli#3442, - * cli/cli#13380). + * The merge step (and this invariant) moved out of /land-and-deploy into the + * extracted /land skill (§4.2a). After ANY non-zero `gh pr merge`, the skill + * must query authoritative PR state via + * `gh pr view --json state,mergeCommit,mergedAt,mergedBy` and branch on the + * result instead of retrying `gh pr merge` (cli/cli#3442, cli/cli#13380). * - * Static invariants pin: - * - §4a-postfail header present + * Static invariants pin (now in /land): + * - §4.2a Post-failure PR-state check header present * - Universal invariant text + reference to upstream gh bugs * - All three state branches (MERGED, OPEN, CLOSED) named explicitly * - MERGED branch: capture merge SHA via mergeCommit.oid * - MERGED branch: non-destructive worktree cleanup with uncommitted-work guard - * - MERGED branch: continues to §4a CI watch * - OPEN branch: checks autoMergeRequest before treating as failure * - CLOSED branch: STOPs * - Hard rule: never retry `gh pr merge` - * - .tmpl edit propagated to generated SKILL.md (atomic per T-Codex-3) + * - .tmpl edit propagated to generated SKILL.md (atomic regen) */ import { describe, expect, test } from "bun:test"; import * as fs from "node:fs"; import * as path from "node:path"; const ROOT = path.resolve(import.meta.dir, ".."); -const TMPL = path.join(ROOT, "land-and-deploy", "SKILL.md.tmpl"); -const MD = path.join(ROOT, "land-and-deploy", "SKILL.md"); +const TMPL = path.join(ROOT, "land", "SKILL.md.tmpl"); +const MD = path.join(ROOT, "land", "SKILL.md"); function readTmpl(): string { return fs.readFileSync(TMPL, "utf-8"); @@ -35,18 +34,18 @@ function readMd(): string { return fs.readFileSync(MD, "utf-8"); } -describe("PR #1620 §4a-postfail in land-and-deploy template", () => { - test("§4a-postfail header present in template", () => { - expect(readTmpl()).toMatch(/### 4a-postfail: Post-failure PR-state check/); +describe("PR #1620 post-failure PR-state check in /land template", () => { + test("post-failure header present in template", () => { + expect(readTmpl()).toMatch(/### 4\.2a: Post-failure PR-state check/); }); - test("§4a-postfail comes before §4a (Merge queue detection)", () => { + test("post-failure check comes before the wait step", () => { const body = readTmpl(); - const postfail = body.indexOf("### 4a-postfail:"); - const queue = body.indexOf("### 4a: Merge queue detection"); + const postfail = body.indexOf("### 4.2a: Post-failure PR-state check"); + const wait = body.indexOf("### 4.3: Wait for it to land"); expect(postfail).toBeGreaterThan(-1); - expect(queue).toBeGreaterThan(-1); - expect(postfail).toBeLessThan(queue); + expect(wait).toBeGreaterThan(-1); + expect(postfail).toBeLessThan(wait); }); test("Universal invariant + upstream gh bug references", () => { @@ -82,11 +81,6 @@ describe("PR #1620 §4a-postfail in land-and-deploy template", () => { expect(body).toMatch(/Do NOT remove the user's primary working tree/); }); - test("MERGED branch continues to §4a CI auto-deploy detection", () => { - const body = readTmpl(); - expect(body).toMatch(/continue to §4a/); - }); - test("OPEN branch checks autoMergeRequest before treating as failure", () => { const body = readTmpl(); expect(body).toMatch(/gh pr view --json autoMergeRequest/); @@ -103,9 +97,9 @@ describe("PR #1620 §4a-postfail in land-and-deploy template", () => { expect(body).toMatch(/never call `gh pr merge` a second time/); }); - test("Generated SKILL.md carries the §4a-postfail section (atomic regen per T-Codex-3)", () => { + test("Generated SKILL.md carries the post-failure section (atomic regen)", () => { const md = readMd(); - expect(md).toMatch(/### 4a-postfail: Post-failure PR-state check/); + expect(md).toMatch(/### 4\.2a: Post-failure PR-state check/); expect(md).toMatch(/state == "MERGED"/); }); }); diff --git a/test/skill-coverage-matrix.ts b/test/skill-coverage-matrix.ts index 101918bda..e5a4d5f13 100644 --- a/test/skill-coverage-matrix.ts +++ b/test/skill-coverage-matrix.ts @@ -140,6 +140,7 @@ export const SKILL_COVERAGE: Record = { 'document-generate': { gate: ['test/skill-coverage-floor.test.ts'], periodic: [] }, // ─── Ops + integrations ───────────────────────────────────── + land: { gate: ['test/gstack-merge.test.ts', 'test/gstack-merge-cli.test.ts', 'test/land-and-deploy-postfail.test.ts', 'test/gen-skill-docs.test.ts', 'test/skill-coverage-floor.test.ts'], periodic: [] }, 'land-and-deploy': { gate: ['test/skill-e2e-deploy.test.ts', 'test/skill-coverage-floor.test.ts'], periodic: [] }, canary: { gate: ['test/skill-coverage-floor.test.ts'], periodic: [] }, benchmark: { gate: ['test/skill-e2e-benchmark-providers.test.ts', 'test/skill-coverage-floor.test.ts'], periodic: [] },