mirror of https://github.com/garrytan/gstack.git
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:
parent
3054acac40
commit
f60a150da5
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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/**'],
|
||||
|
|
|
|||
|
|
@ -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"/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: [] },
|
||||
|
|
|
|||
Loading…
Reference in New Issue