Files
compound-engineering-plugin…/tests/skills/ce-plan-output-mode.test.ts

341 lines
16 KiB
TypeScript

import { readFileSync } from "fs"
import path from "path"
import { describe, expect, test } from "bun:test"
import { load as parseYaml } from "js-yaml"
const SKILL_PATH = path.join(
process.cwd(),
"skills/ce-plan/SKILL.md",
)
const SKILL_BODY = readFileSync(SKILL_PATH, "utf8")
const OUTPUT_MODE_BODY = readFileSync(
path.join(process.cwd(), "skills/ce-plan/references/output-mode.md"),
"utf8",
)
const FINAL_REVIEW_BODY = readFileSync(
path.join(process.cwd(), "skills/ce-plan/references/final-review.md"),
"utf8",
)
const RESUME_BODY = readFileSync(
path.join(process.cwd(), "skills/ce-plan/references/resume.md"),
"utf8",
)
const HANDOFF_BODY = readFileSync(
path.join(process.cwd(), "skills/ce-plan/references/plan-handoff.md"),
"utf8",
)
const HTML_RENDERING_PATH = path.join(
process.cwd(),
"skills/ce-plan/references/html-rendering.md",
)
const PLAN_SECTIONS_PATH = path.join(
process.cwd(),
"skills/ce-plan/references/plan-sections.md",
)
// Regression guard for the `output:html` / `output:md` argument on ce-plan.
// Under exclusive output mode, the plan is written as EITHER markdown OR
// HTML — never both. The skill body must carry the load-bearing surface:
// the argument-hint advertises the flag, the kernel requires the output owner
// before phase interpretation, and that owner preserves the precedence and
// pipeline override that automated downstream consumers rely on.
describe("ce-plan output:html mode", () => {
test("argument-hint advertises output:html", () => {
// argument-hint is in the frontmatter. Extract and parse to confirm
// the token is visible to humans discovering the flag, not just buried
// in skill prose.
const frontmatterMatch = SKILL_BODY.match(/^---\n([\s\S]*?)\n---/)
expect(frontmatterMatch).not.toBeNull()
const frontmatter = parseYaml(frontmatterMatch![1]) as Record<string, unknown>
const hint = frontmatter["argument-hint"]
expect(
typeof hint === "string" && hint.includes("output:html"),
`ce-plan argument-hint must mention 'output:html' so humans discover the flag. Current value: ${JSON.stringify(hint)}`,
).toBe(true)
})
test("SKILL.md requires the output owner before phase interpretation", () => {
expect(
/Read `references\/output-mode\.md` before interpreting any phase/i.test(SKILL_BODY),
"SKILL.md must require output-mode.md before any phase can interpret its inputs.",
).toBe(true)
const phaseStart = OUTPUT_MODE_BODY.indexOf("## 0.0")
expect(
phaseStart,
"output-mode.md no longer contains the Phase 0.0 owner anchor.",
).toBeGreaterThan(-1)
const phaseRegion = OUTPUT_MODE_BODY.slice(phaseStart)
expect(
/output:/.test(phaseRegion),
"Phase 0.0 must name the `output:` argument prefix.",
).toBe(true)
expect(
/plan_output/.test(phaseRegion),
"Phase 0.0 must name the `plan_output` config key.",
).toBe(true)
expect(
/pipeline|disable-model-invocation/i.test(phaseRegion),
"Phase 0.0 must describe the pipeline-mode override that forces markdown.",
).toBe(true)
expect(
/literal[\s-]prefix|literal prefix/i.test(phaseRegion),
"Phase 0.0 must state the literal-prefix token-parsing convention so `feat:`/`fix:`/`chore:` in feature descriptions pass through verbatim.",
).toBe(true)
// A user-stated/remembered format preference must override the config file
// (the config is the persisted fallback, not the top signal), and the skill
// must NOT be told to open instruction files to find it.
expect(
/user-stated preference/i.test(phaseRegion),
"Phase 0.0 must include a user-stated-preference tier above config.",
).toBe(true)
expect(
/overrides\*?\*? the config|more current than the rarely-edited config/i.test(phaseRegion),
"The user-stated preference must be stated to override the config file.",
).toBe(true)
expect(
/do not open or search instruction files|already (present )?in your context/i.test(phaseRegion),
"The stated-preference tier must act on context only, not instruct reading instruction files.",
).toBe(true)
// The in-prompt format trigger must be harness-neutral — reason over the
// user's prompt, NOT a Claude-only $ARGUMENTS token (Cursor uses $1/$2; Kiro
// drops $ARGUMENTS) — and a format named as subject matter must not be
// mistaken for a doc-format request.
expect(
/reason over the user's prompt/i.test(phaseRegion),
"Phase 0.0 step 1 must reason over the user's prompt (harness-neutral), not a $ARGUMENTS token.",
).toBe(true)
expect(
/subject matter|not a doc-format request/i.test(phaseRegion),
"Phase 0.0 step 1 must guard against treating a format named as subject matter as a doc-format request.",
).toBe(true)
const stepOne = phaseRegion.slice(
phaseRegion.indexOf("In-prompt request"),
phaseRegion.indexOf("User-stated preference"),
)
expect(
stepOne.includes("$ARGUMENTS"),
"The output-format trigger must not depend on the Claude-only $ARGUMENTS token.",
).toBe(false)
})
test("token-parsing convention names both mode: and output: as flag prefixes", () => {
// The convention is shared across `mode:`, `output:`, and any future
// flag-token. Both names must appear together in the parsing prose so a
// future implementer doesn't generalize to "any <word>:<word> token" and
// accidentally consume conventional commit prefixes.
const phaseStart = OUTPUT_MODE_BODY.indexOf("## 0.0")
const phaseRegion = OUTPUT_MODE_BODY.slice(phaseStart)
expect(
/mode:/.test(phaseRegion) && /output:/.test(phaseRegion),
"Phase 0.0 token-parsing convention must name both `mode:` and `output:` as literal-prefix flags so the rule generalizes correctly.",
).toBe(true)
})
test("no-artifact routes do not acquire a repository-backed format dependency", () => {
const phaseStart = OUTPUT_MODE_BODY.indexOf("## 0.0")
const phaseRegion = OUTPUT_MODE_BODY.slice(phaseStart)
expect(phaseRegion).toContain("keep config/default resolution pending until an artifact-producing route is known")
expect(phaseRegion).toContain("A terminal no-artifact route never probes config")
expect(phaseRegion).toContain("settle it before selecting a renderer or composing the artifact path")
})
test("software routing completes deferred output resolution before Phase 0.2", () => {
const domainRoute = RESUME_BODY.slice(RESUME_BODY.indexOf("#### 0.1b Classify Task Domain"))
expect(domainRoute).toContain("the artifact-producing route is now known")
expect(domainRoute).toContain("Re-read `references/output-mode.md`")
expect(domainRoute).toMatch(/settle any pending config\/default resolution before selecting the renderer or continuing to Phase 0\.2/i)
})
test("the kernel is the sole model-elevation dispatcher", () => {
expect(SKILL_BODY).toContain("Immediately before authoring, read `references/reasoning-elevation.md`")
expect(FINAL_REVIEW_BODY).toContain("Return to the kernel for its model-elevation boundary")
expect(FINAL_REVIEW_BODY).toContain("does not dispatch the authoring route itself")
expect(FINAL_REVIEW_BODY).not.toContain("load `references/reasoning-elevation.md`")
})
test("config matching rule ignores commented YAML lines (active-key principle)", () => {
// Codex review (2026-05-13, thread PRRT_kwDOP_gZVc6B6OgB) flagged that the
// prior phrasing — "contains `plan_output: md|html`" — would match the
// commented examples shipped in the config template (`# plan_output: html`),
// silently forcing every user into HTML mode. The fix is principle-level:
// require an ACTIVE (non-commented) key, and name the failure mode so a
// future maintainer doesn't loosen it back. We check the principle is
// present, not a specific phrasing.
const phaseStart = OUTPUT_MODE_BODY.indexOf("## 0.0")
const phaseRegion = OUTPUT_MODE_BODY.slice(phaseStart)
expect(
/active.*non-commented|non-commented.*key|lines starting with `#`.*comments|ignore commented/i.test(phaseRegion),
"Phase 0.0 config matching must require an ACTIVE (non-commented) `plan_output:` key, not a raw-text 'contains' match. Without this, the shipped config template's commented examples would silently force HTML mode.",
).toBe(true)
// The rationale-citation pin that used to sit here required the always-loaded
// body to spell out the shipped template's `# plan_output: html` example. The
// invariant it was protecting is the active-key rule asserted just above, which
// the shared ce-config-layers block a few lines below Phase 0.0 also states
// ("Win with the first active (non-commented) value"). Pin the condition, not
// the worked example: the body must say a commented template line is not a
// setting, in whatever words.
expect(
/(commented|template)[^.\n]{0,80}(are not settings|is not a setting|not an active setting)|(not settings|not an? (active )?setting)[^.\n]{0,80}(commented|template)/i.test(
phaseRegion,
),
"Phase 0.0 must say that a commented `plan_output:` line is not an active setting — otherwise the shipped template's examples silently force HTML mode.",
).toBe(true)
expect(
/ordinary-key|next layer|config\.local\.yaml then `config\.yaml`/i.test(phaseRegion),
"Phase 0.0 config step must cascade local then tracked before the skill default.",
).toBe(true)
})
test("unknown-value fallback note reflects final resolved mode, not a hardcoded md", () => {
// Codex review (2026-05-13, thread PRRT_kwDOP_gZVc6B-LIW) flagged that
// hardcoding "defaulting to md" in the unknown-value note is wrong when
// step 2 (config) or step 4 (pipeline override) resolves to a different
// value. The note must reflect the actual final value, not anticipate one.
const phaseStart = OUTPUT_MODE_BODY.indexOf("## 0.0")
const phaseRegion = OUTPUT_MODE_BODY.slice(phaseStart)
expect(
/using <resolved_format>|reflect.*final.*mode|after final resolution|after steps 2-4|Do not hardcode `md`/i.test(phaseRegion),
"Phase 0.0's unknown-value note must reflect the actual resolved OUTPUT_FORMAT after all precedence steps, not a hardcoded 'defaulting to md' that misleads users when config has set HTML.",
).toBe(true)
})
test("Phase 5.2 sends both output formats through ce-doc-review", () => {
const phase52Start = FINAL_REVIEW_BODY.indexOf("#### 5.2 Write Plan File")
expect(phase52Start).toBeGreaterThan(-1)
const phase52Region = FINAL_REVIEW_BODY.slice(phase52Start, phase52Start + 2000)
expect(
/both formats|markdown and HTML|native format/i.test(phase52Region),
"Phase 5.2 must state that markdown and HTML both continue through ce-doc-review in their native format.",
).toBe(true)
})
test("Phase 0.0 points at format-rendering refs based on resolved value", () => {
const phaseStart = OUTPUT_MODE_BODY.indexOf("## 0.0")
const phaseRegion = OUTPUT_MODE_BODY.slice(phaseStart)
expect(
/references\/markdown-rendering\.md|markdown-rendering\.md/i.test(phaseRegion),
"Phase 0.0 must point at markdown-rendering.md for md output mode.",
).toBe(true)
expect(
/references\/html-rendering\.md|html-rendering\.md/i.test(phaseRegion),
"Phase 0.0 must point at html-rendering.md for html output mode.",
).toBe(true)
})
test("post-generation menu offers prototype and browser for HTML, not Proof", () => {
const phaseStart = HANDOFF_BODY.indexOf("## 5.4 Post-Generation Options")
expect(phaseStart).toBeGreaterThan(-1)
const phaseRegion = HANDOFF_BODY.slice(phaseStart)
expect(
/Open in browser/.test(phaseRegion),
"plan-handoff.md Phase 5.4 menu must include 'Open in browser' for HTML mode.",
).toBe(true)
expect(
/Prototype a remaining feel-question/.test(phaseRegion),
"plan-handoff.md Phase 5.4 menu must include the prototype offer.",
).toBe(true)
expect(
/Publish to Proof/.test(phaseRegion),
"software plan Phase 5.4 must omit Share to Proof.",
).toBe(false)
expect(
/OUTPUT_FORMAT=html/i.test(phaseRegion),
"plan-handoff.md must state HTML-only browser rendering.",
).toBe(true)
})
test("no sibling logic — exclusive output mode is documented", () => {
// Defends against drift back to the old sibling model. The skill must
// state exclusivity ("md OR html, never both") so a future maintainer
// doesn't re-introduce sibling generation.
expect(
/exclusive|md OR html|markdown OR HTML|never both/i.test(OUTPUT_MODE_BODY),
"output-mode.md must state that output mode is exclusive — markdown OR HTML, never both.",
).toBe(true)
// OUTPUT_FORMAT_SOURCE was used by the sibling tracking; it should not
// re-appear.
expect(
/OUTPUT_FORMAT_SOURCE/.test(SKILL_BODY + OUTPUT_MODE_BODY + HANDOFF_BODY),
"The ce-plan kernel and output/handoff owners must not reference OUTPUT_FORMAT_SOURCE.",
).toBe(false)
})
test("plan-sections.md enumerates the required plan metadata fields by name", () => {
// PR #826 split the prescriptive plan-template.md into a section contract
// (plan-sections.md) + format-rendering refs. markdown-rendering.md now
// says "Per-skill frontmatter fields are defined in each skill's section
// contract" — so plan-sections.md MUST actually list them or downstream
// tooling that keys on these field names (deepening's
// `deepened: YYYY-MM-DD`, the `origin:` brainstorm traceback) breaks
// silently when agents compose plans from the new refs.
const body = readFileSync(PLAN_SECTIONS_PATH, "utf8")
// Required field names that downstream consumers depend on.
for (const field of ["title", "type", "date"]) {
expect(
new RegExp(`\\b${field}\\b`).test(body),
`plan-sections.md must name the required '${field}' metadata field — downstream tooling keys on it.`,
).toBe(true)
}
// Optional but well-known fields whose names are load-bearing for
// resume/traceback flows.
for (const field of ["origin", "deepened"]) {
expect(
new RegExp(`\\b${field}\\b`).test(body),
`plan-sections.md must name the optional '${field}' metadata field — its presence and exact name are load-bearing for downstream flows.`,
).toBe(true)
}
// Plans carry NO status field — the active → completed lifecycle was
// removed (ce-work no longer mutates the plan; completion is derived from
// git). The contract must say so explicitly so an agent reading it does
// not reintroduce a status field.
expect(
/no .{0,3}status.{0,3} field|carry .{0,6}no .{0,12}status/i.test(body),
"plan-sections.md must state plans carry NO status field.",
).toBe(true)
// The field-name rules below had no mechanical guard, and a real artifact
// shipped with `created:` instead of `date:` and a `feat:` prefix in the
// title. These are greppable contract text, so pin them here rather than
// scanning docs/plans/ (which legacy artifacts would fail).
expect(
body.includes("`date` to `created`"),
"plan-sections.md must name `date` -> `created` as a breaking rename — an artifact shipped with `created:` and downstream consumers key on `date`.",
).toBe(true)
expect(
body.includes("` - Plan` suffix"),
"plan-sections.md must require the ` - Plan` title suffix — artifacts have shipped without it.",
).toBe(true)
expect(
/conventional-commit prefix/i.test(body),
"plan-sections.md must prohibit a conventional-commit prefix in `title` — the `type` field carries that classification.",
).toBe(true)
})
test("html-rendering.md reference exists and is loadable", () => {
const body = readFileSync(HTML_RENDERING_PATH, "utf8")
expect(body.length).toBeGreaterThan(0)
// Spot-check that the major sections we promise the agent are present.
expect(/Hard invariants/i.test(body)).toBe(true)
expect(/Precedence stack/i.test(body)).toBe(true)
expect(/Active-recall/i.test(body)).toBe(true)
expect(/Format principles/i.test(body)).toBe(true)
expect(/Affordance idioms/i.test(body)).toBe(true)
expect(/Agent-consumability rules/i.test(body)).toBe(true)
expect(/Post-compose audit/i.test(body)).toBe(true)
})
})