mirror of
https://github.com/EveryInc/compound-engineering-plugin.git
synced 2026-09-19 01:09:55 +08:00
d254fa6846
Two ordering conflicts introduced by earlier fixes in this PR. - Restoring the work branch before construction left gh stack add unable to run, since it must run from the parent. The managed arm now plans from the restored work branch and checks the parent out again to attach - A fork parent materialized from refs/pull/<n>/head has no remote-tracking branch, so construction's fetch-and-verify step could not be satisfied. It now accepts Topology's verified local branch Declined a third finding: the standalone arm was reported as having no branch-only path and as having dropped the refs/pull fetch. The branch-only clause is present, and the fetch is in gh-stack-cli.md, which Topology points at by design of the earlier consolidation.
430 lines
24 KiB
TypeScript
430 lines
24 KiB
TypeScript
import { readFile } from "fs/promises"
|
|
import path from "path"
|
|
import { describe, expect, test } from "bun:test"
|
|
|
|
async function readRepoFile(relativePath: string): Promise<string> {
|
|
return readFile(path.join(process.cwd(), relativePath), "utf8")
|
|
}
|
|
|
|
describe("ce-commit-push-pr contract", () => {
|
|
test("reconciles the complete branch scope before composition", async () => {
|
|
const content = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
|
|
expect(content).not.toContain("Read every commit")
|
|
const sizingSection = content.match(
|
|
/## Step A: Size the description([\s\S]+?)## Step B:/,
|
|
)?.[1]
|
|
expect(sizingSection).toContain(
|
|
"complete oneline commit list and final three-dot diff",
|
|
)
|
|
expect(sizingSection).toContain("scope map")
|
|
expect(sizingSection).toContain("umbrella outcome")
|
|
expect(sizingSection).toContain("consult the fuller messages only")
|
|
const titleSection = content.match(
|
|
/## Step B: Compose the title([\s\S]+?)## Step B1:/,
|
|
)?.[1]
|
|
expect(titleSection).toContain("scope map")
|
|
expect(titleSection).toContain("umbrella outcome")
|
|
const auditSection = content.match(
|
|
/## Step E: Pre-apply coverage audit([\s\S]+)\s*$/,
|
|
)?.[1]
|
|
expect(auditSection).toContain("scope map")
|
|
expect(auditSection).toContain("every material outcome")
|
|
})
|
|
|
|
test("elevates multi-PR program altitude with lead-in and lead-out", async () => {
|
|
const content = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
|
|
const sizingSection = content.match(
|
|
/## Step A: Size the description([\s\S]+?)## Step B:/,
|
|
)?.[1]
|
|
expect(sizingSection).toBeDefined()
|
|
expect(sizingSection).toContain("Program altitude")
|
|
expect(sizingSection).toContain("lead-in")
|
|
expect(sizingSection).toContain("lead-out")
|
|
expect(sizingSection).toContain("Program outcome")
|
|
expect(sizingSection).toMatch(/this PR's contribution/i)
|
|
expect(sizingSection).toContain("Do **not** invent a series")
|
|
expect(sizingSection).toMatch(
|
|
/program → lead-in \(if any\) → this contribution → lead-out \(if any\)/,
|
|
)
|
|
// Bidirectional contrast: middle PR needs prior + residual, not local-only
|
|
expect(sizingSection).toContain("too local for a middle PR")
|
|
expect(sizingSection).toContain("Continues the session-revocation rewrite")
|
|
|
|
const auditSection = content.match(
|
|
/## Step E: Pre-apply coverage audit([\s\S]+)\s*$/,
|
|
)?.[1]
|
|
expect(auditSection).toBeDefined()
|
|
expect(auditSection).toMatch(
|
|
/program context was present.+lead place this PR on the arc/is,
|
|
)
|
|
expect(auditSection).toMatch(
|
|
/program context was absent.+invent a multi-PR series/is,
|
|
)
|
|
|
|
// Tracker refs stay separate from series narrative
|
|
const relatedSection = content.match(
|
|
/## Step B1: Resolve related work references([\s\S]+?)## Step B2:/,
|
|
)?.[1]
|
|
expect(relatedSection).toBeDefined()
|
|
expect(relatedSection).toMatch(
|
|
/Sibling PR \/ series narrative belongs in Step A's program altitude/i,
|
|
)
|
|
})
|
|
|
|
test("scopes STE-inspired prose to non-load-bearing wording", async () => {
|
|
const content = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
|
|
expect(content).toContain("ASD-STE100 Simplified Technical English")
|
|
expect(content).toMatch(
|
|
/Prefer plain wording wherever domain terms are not load-bearing/i,
|
|
)
|
|
expect(content).toMatch(
|
|
/Keep necessary technical jargon.+where they \*are\* the claim/is,
|
|
)
|
|
expect(content).toMatch(
|
|
/do not dilute mechanism language into vague plain English/i,
|
|
)
|
|
// Contrast pins both failure directions: decorative jargon vs load-bearing terms
|
|
expect(content).toContain("jargon without need")
|
|
expect(content).toContain("jargon is the claim")
|
|
expect(content).toContain("`TokenStore.invalidate` is now atomic under concurrent refresh.")
|
|
|
|
const auditSection = content.match(
|
|
/## Step E: Pre-apply coverage audit([\s\S]+)\s*$/,
|
|
)?.[1]
|
|
expect(auditSection).toBeDefined()
|
|
expect(auditSection).toMatch(
|
|
/domain jargon that is not load-bearing/i,
|
|
)
|
|
})
|
|
|
|
test("repository PR-body contracts set structure without replacing editorial guidance", async () => {
|
|
const content = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
|
|
const contractIndex = content.indexOf("## Project PR-body contract")
|
|
expect(contractIndex).toBeGreaterThan(-1)
|
|
expect(contractIndex).toBeLessThan(content.indexOf("## Step Pre-A"))
|
|
expect(content).toMatch(/template as a minimum.+exact\/template-only body/is)
|
|
expect(content).toContain("add no sections beyond those the project permits")
|
|
expect(content).toMatch(/structural floor.+sizes the content within it/is)
|
|
expect(content).toMatch(/Step C:[\s\S]+preserve that structure.+sections it permits/i)
|
|
expect(content).toMatch(/project PR-body contract supplies a heading or location for the opening.+place it there without inventing or renaming a heading/is)
|
|
expect(content).toMatch(/Otherwise, the opening goes under `## Summary`.+bare paragraph/is)
|
|
expect(content).toMatch(/Step E:[\s\S]+except for headings, fields, checklists, or boilerplate.+requires/i)
|
|
})
|
|
|
|
test("existing PR rewrites carry the old body into composition", async () => {
|
|
const content = await readRepoFile("skills/ce-commit-push-pr/SKILL.md")
|
|
|
|
// Existing-PR detection uses `gh pr list` (exits 0, returns `[]` when none)
|
|
// rather than `gh pr view` (exits 1 with no PR, which aborted `!` load).
|
|
expect(content).toContain("gh pr list --head <branch> --state open --json number,url,title,body,state,isDraft,headRefName,headRepositoryOwner")
|
|
// Multi-fork same-branch matches are disambiguated by head owner, not index 0 (PR #1109 review).
|
|
expect(content).toContain("do **not** blindly take index 0")
|
|
expect(content).toContain("Note the URL and body from that entry")
|
|
expect(content).toContain("If Step 1 found an existing PR, pass its URL to Step 4")
|
|
expect(content).toContain("existing body")
|
|
expect(content).toMatch(/preserve.+Related.+Fixes/is)
|
|
})
|
|
|
|
test("requires related work references to use tracker-specific closing semantics", async () => {
|
|
const content = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
|
|
expect(content).toContain("## Step B1: Resolve related work references")
|
|
expect(content).toContain("closing reference")
|
|
expect(content).toContain("non-closing reference")
|
|
expect(content).toContain("Do not invent a closing keyword")
|
|
expect(content).toMatch(/git log\s+--format=fuller/)
|
|
expect(content).toContain("full commit messages")
|
|
expect(content).toContain("Do not put a non-closing reference next to close/fix/resolve/address/report wording")
|
|
expect(content).toContain("Use the table's non-closing reference labels exactly")
|
|
expect(content).toContain("Non-closing references always get their own sentence or `## Related` block")
|
|
expect(content).toContain("For a non-closing reference, the tracker ID appears only in that related-reference sentence or block, never in the summary/opening/body prose")
|
|
expect(content).toContain('Bad: "closing one corruption path from #123"')
|
|
expect(content).toContain('Bad: "This addresses the retry-related corruption path reported in #123."')
|
|
expect(content).toContain('Good: "This covers the duplicate-row retry path; concurrent cancellation remains follow-up work."')
|
|
|
|
expect(content).toContain("GitHub Issues")
|
|
expect(content).toContain("Fixes #123")
|
|
expect(content).toContain("Fixes owner/repo#123")
|
|
expect(content).toMatch(/target.+default branch/i)
|
|
|
|
expect(content).toContain("Linear")
|
|
expect(content).toContain("Fixes ENG-123")
|
|
expect(content).toContain("Related to ENG-123")
|
|
expect(content).toMatch(/PR description.+not.+comment/i)
|
|
})
|
|
|
|
test("adds generic Compound Engineering branding only on an explicit signal", async () => {
|
|
const reference = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
const skill = await readRepoFile("skills/ce-commit-push-pr/SKILL.md")
|
|
|
|
expect(reference).toContain("Built_with-Compound_Engineering")
|
|
expect(reference).not.toContain("MODEL_SLUG")
|
|
expect(reference).not.toMatch(/\| Harness \|/)
|
|
expect(reference).not.toMatch(/model slug/i)
|
|
expect(reference).toMatch(/new PR body.+resolved branding gate is on/is)
|
|
expect(reference).toMatch(/otherwise omit/is)
|
|
expect(reference).toMatch(/existing PR body.+preserve.+verbatim/is)
|
|
expect(reference).toMatch(/never add one when absent/is)
|
|
expect(reference).toMatch(/explicitly asks.+remove or replace/is)
|
|
expect(skill).toMatch(/branding-only delta.+explicitly request/is)
|
|
expect(skill).toMatch(/branding is \*\*off unless.+branding:on/is)
|
|
expect(skill).toContain("normalize that natural-language request to `branding:on`")
|
|
expect(skill).toContain("If both tokens are present, stop and report the conflict")
|
|
expect(skill).not.toContain("pr_branding")
|
|
expect(skill).toMatch(/branding:on\|off/)
|
|
})
|
|
|
|
test("babysit handoff is default-on with off-switches and drivable fork PRs", async () => {
|
|
const content = await readRepoFile("skills/ce-commit-push-pr/SKILL.md")
|
|
|
|
// Default-on: completion gate, announce, transfer ownership, never ask yes/no.
|
|
expect(content).toMatch(/completion gate/i)
|
|
expect(content).toMatch(/Auto-hand off by default/i)
|
|
expect(content).toMatch(/never ask yes\/no/i)
|
|
// Off is the explicit choice: per-run token + standing config opt-out.
|
|
expect(content).toContain("babysit:off")
|
|
expect(content).toContain("auto_babysit: false")
|
|
// Hard-off cases (orchestrated, no PR, non-GitHub, non-pushable head).
|
|
expect(content).toMatch(/do not fire/i)
|
|
expect(content).toMatch(/mode:pipeline/)
|
|
expect(content).toMatch(/head branch you cannot push to/i)
|
|
// Fork PRs are drivable, gated on head-pushability (not fork-ness); base read / head push.
|
|
expect(content).toMatch(/fork PRs are drivable/i)
|
|
expect(content).toMatch(/reads state on the \*\*base\*\* repo/i)
|
|
expect(content).toMatch(/pushes fixes to the \*\*head\*\* repo/i)
|
|
})
|
|
|
|
test("config template and example keep branding out of ambient configuration", async () => {
|
|
for (const p of [
|
|
"skills/ce-setup/references/config-template.yaml",
|
|
".compound-engineering/config.example.yaml",
|
|
]) {
|
|
const template = await readRepoFile(p)
|
|
expect(template).toContain("auto_babysit")
|
|
expect(template).not.toContain("pr_branding")
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("PR concept teaching contract", () => {
|
|
test("SKILL.md wires the teaching gate, pipeline mode, and trailer", async () => {
|
|
const content = await readRepoFile("skills/ce-commit-push-pr/SKILL.md")
|
|
const trailerStart = content.indexOf("**User-runnable invocation rendering.**")
|
|
const trailerEnd = content.indexOf("**Babysit handoff", trailerStart)
|
|
const trailer = content.slice(trailerStart, trailerEnd)
|
|
|
|
// Non-interactive modifier for orchestrated callers
|
|
expect(content).toContain("mode:pipeline")
|
|
expect(content).toContain("suppress every blocking ask")
|
|
|
|
// Config gate: both keys, active-key-only resolution, single-gate semantics
|
|
expect(content).toContain("pr_teaching_section")
|
|
expect(content).toContain("pr_teaching_archive")
|
|
expect(content).toContain("active (non-commented)")
|
|
expect(content).toContain("Step B2")
|
|
|
|
// Machine-readable trailer + host-rendered interactive offer
|
|
expect(trailerStart).toBeGreaterThan(-1)
|
|
expect(trailerEnd).toBeGreaterThan(trailerStart)
|
|
expect(trailer).toContain("New concepts:")
|
|
expect(trailer).toContain("using the rendering rule above")
|
|
expect(trailer).toContain("$ce-explain <name>")
|
|
expect(trailer).toContain("/ce-explain <name>")
|
|
expect(trailer).not.toContain("/skill:ce-explain")
|
|
expect(trailer).toMatch(/default to `\/ce-explain <name>`[\s\S]{0,220}Codex[\s\S]{0,260}output one form only/i)
|
|
})
|
|
|
|
test("SKILL.md archival transition guards ordering, gitignore, and modes", async () => {
|
|
const content = await readRepoFile("skills/ce-commit-push-pr/SKILL.md")
|
|
|
|
expect(content).toContain("<root>/explainers/")
|
|
expect(content).toContain("input_shape: concept")
|
|
expect(content).toContain("docs(explainer): teach")
|
|
// Declined rewrite must not leave a stray committed-but-unlinked doc
|
|
expect(content).toContain("declined rewrite skips archival")
|
|
// Never force-add an ignored path
|
|
expect(content).toContain("never `git add -f`")
|
|
})
|
|
|
|
test("reference composes the section via Step B2 with base-ref novelty checks", async () => {
|
|
const content = await readRepoFile(
|
|
"skills/ce-commit-push-pr/references/pr-description-writing.md",
|
|
)
|
|
|
|
expect(content).toContain("## Step B2: Judge new concepts")
|
|
// Self-detection trap: novelty is judged against the base ref
|
|
expect(content).toContain("never the working tree")
|
|
expect(content).toMatch(/git grep[^\n]*<base-remote>\/<base>/)
|
|
// Negative constraint keeps absence the common case
|
|
expect(content).toContain("absence is the common case")
|
|
// Section heading and its slot in Step C's assembly order
|
|
expect(content).toContain("## New concepts")
|
|
expect(content).toContain("New concepts section when Step B2 produced one")
|
|
// Rewrite preservation mirrors the Demo/Screenshots rule
|
|
expect(content).toMatch(/preserve an existing `## New concepts` section/i)
|
|
})
|
|
|
|
test("config template documents both teaching keys", async () => {
|
|
const template = await readRepoFile("skills/ce-setup/references/config-template.yaml")
|
|
|
|
expect(template).toContain("pr_teaching_section")
|
|
expect(template).toContain("pr_teaching_archive")
|
|
})
|
|
|
|
test("babysit handoff requires ownership transfer, forbids substitutes, hard-fails on load failure", async () => {
|
|
const content = await readRepoFile("skills/ce-commit-push-pr/SKILL.md")
|
|
|
|
const handoff = content.match(/\*\*Babysit handoff — default on; completion gate\.\*\*[\s\S]+?(?=\n\n)/)?.[0]
|
|
expect(handoff).toBeDefined()
|
|
// Completion gate: PR URL alone is not done; ce-babysit-pr must own follow-on.
|
|
expect(handoff).toMatch(/not done.+until `ce-babysit-pr` owns/is)
|
|
expect(handoff).toMatch(/Reporting the PR URL alone is not success/)
|
|
expect(handoff).toMatch(/\*\*Success\*\*.+`ce-babysit-pr` has started/is)
|
|
// Harness-agnostic load: use the host's normal skill mechanism without a platform matrix.
|
|
expect(handoff).toMatch(/host's normal skill-invocation mechanism/)
|
|
expect(handoff).not.toContain("Claude Code `Skill` tool")
|
|
// Observed drift (Nugget PR #1933): bare `pr-snapshot watch` instead of loading
|
|
// ce-babysit-pr. Keep anti-reinvention at this seam.
|
|
expect(handoff).toContain("Never start babysit mechanics yourself")
|
|
expect(handoff).toContain("`pr-snapshot`")
|
|
// Observed drift (Nugget PR #1983): Cursor agent substituted Task(ci-watcher)
|
|
// when skill load failed. Name non-substitutes and require hard-fail.
|
|
expect(handoff).toContain("`ci-watcher`")
|
|
expect(handoff).toContain("`gh pr checks --watch`")
|
|
expect(handoff).toMatch(/Handoff blocked/i)
|
|
expect(handoff).toMatch(/cannot be loaded or started/i)
|
|
expect(handoff).toMatch(/Do not invent a parallel or narrower watch/)
|
|
|
|
// Observed drift (Nugget PR #1934): auto-babysit fired on a draft design PR, forcing the
|
|
// session to improvise "never mark ready" caveats. Drafts are a not-ready signal; the
|
|
// auto-handoff must not fire on them (explicit babysit tokens still force it).
|
|
// Soft-degrade is checkpoint-only after successful ownership — not a failed-handoff fallback.
|
|
const doNotFire = content.match(/\*\*Do not fire \(auto-detected[\s\S]+?(?=\n\n)/)?.[0]
|
|
expect(doNotFire).toBeDefined()
|
|
expect(doNotFire).toContain("draft")
|
|
expect(doNotFire).toContain("`babysit:continuous`")
|
|
expect(doNotFire).toMatch(/after successful handoff only/i)
|
|
expect(doNotFire).toMatch(/not a substitute for a failed handoff/i)
|
|
})
|
|
|
|
test("opt-in stack mode constructs or submits a stack and hands off with posture", async () => {
|
|
const [skill, submit, cli] = await Promise.all([
|
|
readRepoFile("skills/ce-commit-push-pr/SKILL.md"),
|
|
readRepoFile("skills/ce-commit-push-pr/references/stack-submit.md"),
|
|
readRepoFile("skills/ce-commit-push-pr/references/gh-stack-cli.md"),
|
|
])
|
|
|
|
expect(submit).toMatch(/named a parent PR or branch to stack on, classify it/i)
|
|
expect(submit).toContain("references/gh-stack-cli.md")
|
|
expect(submit).toMatch(/Classify by \*\*PR number\*\*.{0,140}pulls a stack down from GitHub/is)
|
|
expect(submit).toMatch(/exit 0 — parent now checked out/i)
|
|
expect(submit).toMatch(/exit 2 — nothing checked out/i)
|
|
// Classification moves HEAD; construction reads the checked-out branch as the original.
|
|
expect(submit).toMatch(/record your work branch and its tip \*\*before\*\* classifying and return to them before construction/i)
|
|
expect(submit).toMatch(/plan the layers from your restored work branch, then check the parent out again/i)
|
|
expect(submit).toMatch(/Exit \*\*5\*\* means that parent is not the top: residual/i)
|
|
expect(submit).toMatch(/Never clear it with `gh stack top`/i)
|
|
// The facts file must not prescribe the recovery Topology forbids.
|
|
expect(cli).toMatch(/moving there with `gh stack top` is a\s+decision, not a fix/i)
|
|
expect(submit).toMatch(/resolve `<parent-branch>` first/i)
|
|
// Both conditions below regressed twice while this block was compressed.
|
|
expect(submit).toMatch(/From a branch with no PR, fetch and verify that ref directly/i)
|
|
expect(submit).toMatch(/make sure a local branch sits \*\*at `headRefOid`\*\*/i)
|
|
expect(submit).toMatch(/verify it is at that commit and stop with a residual otherwise/i)
|
|
expect(cli).toMatch(/reachability leaves the commit with no branch to name/i)
|
|
expect(cli).toMatch(/no documented branch ordering, so do not derive position/i)
|
|
expect(submit).toMatch(/only when `author` is the current user/i)
|
|
expect(submit).toMatch(/in place of the generic one shown in construction/i)
|
|
expect(submit).toMatch(/\*\*Unproven\*\* — a residual, not a guess/i)
|
|
expect(cli).toMatch(/Existing branches are adopted;\s+missing ones are created/i)
|
|
expect(submit).toMatch(/stop with a residual on a name that fails/i)
|
|
expect(cli).toMatch(/gh stack version 0\.1\.0/i)
|
|
expect(cli).toMatch(/--help` is authoritative/i)
|
|
expect(cli).toMatch(/must run from the \*\*top\*\* branch.{0,120}exits \*\*5\*\*/is)
|
|
expect(cli).toMatch(/Existing branches are adopted;\s+missing ones are created/i)
|
|
expect(cli).toMatch(/gh stack link`\*\* — GitHub-only by design.{0,140}no local tracking/is)
|
|
expect(cli).toMatch(/gh pr merge`\*\* on a stack member/i)
|
|
expect(skill).toContain("## Stack mode (opt-in)")
|
|
expect(skill).toContain("**Do not** proactively suggest PR stacks")
|
|
expect(skill).toMatch(/explicit stack request is \*\*required intent\*\*.{0,120}not re-read it as a single PR/is)
|
|
expect(skill).toMatch(/did \*\*not\*\* ask for one, \*\*refuse\*\* nonsense stacks/i)
|
|
expect(skill).toContain("references/stack-submit.md")
|
|
expect(skill).toMatch(/do not add `posture:` to this skill's argument-hint/i)
|
|
expect(skill).toMatch(/before Step 3[\s\S]{0,180}Probe[\s\S]{0,80}Topology[\s\S]{0,120}Retrospective construction/is)
|
|
expect(skill).toMatch(/before Step 3[\s\S]{0,260}do not submit/is)
|
|
expect(skill).toMatch(/Step 5 exclusively owns stack submission[\s\S]{0,160}PRs created in this run/is)
|
|
expect(skill).toMatch(/replaces ordinary Step 3/i)
|
|
expect(skill).toContain("posture:stack-ready")
|
|
expect(skill).toContain("posture:stack-land")
|
|
expect(skill).toMatch(/bottom open non-draft/i)
|
|
expect(skill).toMatch(/Stack mode[\s\S]{0,80}still follow the Submit section of `references\/stack-submit\.md`/i)
|
|
expect(skill).toMatch(/mode:pipeline` \*\*except\*\* when this run completed a stack-mode submit/i)
|
|
expect(skill).toMatch(/outer orchestrator[\s\S]{0,80}second bare babysit/i)
|
|
expect(skill).toMatch(/mode:pipeline[\s\S]{0,160}started-only is not enough/i)
|
|
expect(submit).toMatch(/authoritative parent tip/i)
|
|
expect(submit).toContain('git checkout -b -- "<branch-name>" "<parent-tip>"')
|
|
expect(submit).toMatch(/Do not hard-code `origin\/<parent>`/i)
|
|
expect(submit).toMatch(/starts on the resolved default branch.+follow `references\/branch-creation\.md`/is)
|
|
expect(submit).toMatch(/starts on an existing feature branch.+do not follow `references\/branch-creation\.md`/is)
|
|
expect(submit).toMatch(/feature branch.+fetch the resolved base `<base>` from Topology.+verify the fetched remote-tracking tip/is)
|
|
expect(submit).toMatch(/no remote-tracking branch to fetch or verify/i)
|
|
expect(submit).toMatch(/stop with a residual on a name that fails/i)
|
|
expect(submit).toMatch(/did not ask for a stack in this request.{0,80}standing preference alone is not asking/is)
|
|
expect(submit).toMatch(/refuse the stack.{0,80}explicit request is not refusable/is)
|
|
expect(submit).toMatch(/original tip.+recovery (ref|branch)/is)
|
|
expect(submit).toMatch(/committed.+planned commit tip/is)
|
|
expect(submit).toMatch(/uncommitted.+save.+tracked and untracked.+restore.+planned layer/is)
|
|
expect(submit).toMatch(/do not treat.+feature commits.+unpushed commits.+local default/is)
|
|
expect(submit).toMatch(/upstack.+do \*\*not\*\* follow `references\/branch-creation\.md`/is)
|
|
expect(submit).toContain("## Retrospective construction")
|
|
expect(submit).toMatch(/Before ordinary Step 3[\s\S]{0,180}do not run Submit/is)
|
|
expect(submit).toMatch(/Step 5[\s\S]{0,100}only phase that runs Submit/is)
|
|
expect(submit).toMatch(/complete change set/i)
|
|
expect(submit).toMatch(/smallest useful set.+independently reviewable/is)
|
|
expect(submit).not.toMatch(/2-3.+layers/is)
|
|
expect(submit).toMatch(/dependency order/i)
|
|
expect(submit).toMatch(/one safe topology is clear.+proceed/is)
|
|
expect(submit).toMatch(/multiple reasonable topologies.+ask the user/is)
|
|
expect(submit).toMatch(/mode:pipeline.+stop.+residual/is)
|
|
expect(submit).toContain('gh stack init --base "<base>" "<bottom-branch>"')
|
|
expect(submit).toContain('gh stack add "<next-branch>"')
|
|
expect(submit).toMatch(/whole-file groups|existing commit boundaries/i)
|
|
expect(submit).toMatch(/published history.+explicit confirmation/is)
|
|
expect(submit).toMatch(/mode:pipeline.+do not split or rewrite.+residual.+explicit confirmation/is)
|
|
expect(submit).toMatch(/after submit.+every PR created in this run.+explicit PR URL/is)
|
|
expect(submit).toMatch(/new PR.+PR-description composition.+PR mode.+immediate parent.+exact head/is)
|
|
expect(submit).toContain('gh pr edit "<pr-url>"')
|
|
expect(submit).toMatch(/never rely on the restored current branch to select the PR/is)
|
|
expect(submit).toMatch(/Existing stack PRs retain their titles and bodies.+explicitly requested a rewrite/is)
|
|
expect(submit).toMatch(/mode:pipeline.+conservative no-rewrite default/is)
|
|
expect(skill).not.toContain("`base:<layer-base>`")
|
|
expect(submit).toMatch(/resolve.+`pr_teaching_archive`.+`archive:on\|off`.+before submit/is)
|
|
expect(submit).toMatch(/archival is on.+stop.+before `gh stack submit`/is)
|
|
expect(submit).toMatch(/do not create an explainer commit after submission/is)
|
|
expect(submit).toMatch(/rerun with `archive:off`.+safe post-submit description path/is)
|
|
expect(submit).toContain("gh stack submit --auto --open")
|
|
expect(submit).toMatch(/existing draft/i)
|
|
expect(submit).toMatch(/do \*\*not\*\* pass `--open`/i)
|
|
expect(submit).not.toMatch(/does \*\*not\*\* invent commit-splitting/i)
|
|
expect(submit).toMatch(/required[\s\S]{0,120}hard-stop/i)
|
|
expect(submit).toMatch(/soft[\s\S]{0,120}single-PR/i)
|
|
expect(submit).toMatch(/Forbidden on managed members/i)
|
|
})
|
|
})
|