test(land): composition + scrub guard, coverage, postfail relocation, touchfiles

- gen-skill-docs: assert land-and-deploy composes {{INVOKE_SKILL:land}} and
  /land carries no deploy/canary machinery (H9 generated-doc scrub)
- relocate the PR #1620 post-failure invariant test to /land (where the
  merge now lives), preserving every pinned invariant
- register /land in the skill coverage matrix
- link land/**, bin/gstack-merge, lib/merge.ts into the land-and-deploy and
  setup-deploy E2E touchfiles so the composition path re-runs on change

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-05-31 09:13:50 -07:00
parent 3054acac40
commit f60a150da5
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
4 changed files with 99 additions and 32 deletions

View File

@ -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', () => {

View File

@ -207,7 +207,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
// 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<string, string[]> = {
// 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/**'],

View File

@ -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"/);
});
});

View File

@ -140,6 +140,7 @@ export const SKILL_COVERAGE: Record<string, SkillCoverage> = {
'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: [] },