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 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 resolution prose is inline // (not deferred to a reference), and the pipeline-mode override guarantees // automated downstream consumers always get markdown. 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 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 describes the Output Mode resolution inline (not solely in a reference)", () => { // The resolution is load-bearing — it determines whether HTML emits at all. // Per the AGENTS.md skill design principle ("SKILL.md content caches at // session start; references load on demand"), load-bearing rules must live // inline. References can describe the HTML composition mechanics, but the // arg/config/default precedence and pipeline override must be reachable // from the cached skill body. expect( /Output Mode|OUTPUT_FORMAT/i.test(SKILL_BODY), "SKILL.md must contain an Output Mode resolution section that establishes OUTPUT_FORMAT before downstream phases reference it.", ).toBe(true) // Precedence must be stated: in-prompt request > user-stated preference > // config > default, with a pipeline // override. All three signals must be named so an agent reading the file // resolves correctly without consulting a reference. const phaseStart = SKILL_BODY.indexOf("#### 0.0") expect( phaseStart, "ce-plan SKILL.md no longer contains the Phase 0.0 anchor — Output Mode resolution was removed or moved without updating the test.", ).toBeGreaterThan(-1) const phaseRegion = SKILL_BODY.slice(phaseStart, phaseStart + 4500) 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 : token" and // accidentally consume conventional commit prefixes. const phaseStart = SKILL_BODY.indexOf("#### 0.0") const phaseRegion = SKILL_BODY.slice(phaseStart, phaseStart + 4500) 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("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 = SKILL_BODY.indexOf("#### 0.0") const phaseRegion = SKILL_BODY.slice(phaseStart, phaseStart + 4500) 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) expect( /# plan_output: html|commented examples|shipped config template/i.test(phaseRegion), "Phase 0.0 must cite the specific failure mode (the shipped template's commented `# plan_output: html` example) so the rationale survives future edits.", ).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 = SKILL_BODY.indexOf("#### 0.0") const phaseRegion = SKILL_BODY.slice(phaseStart, phaseStart + 4500) expect( /using |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 names the HTML/ce-doc-review timing relationship", () => { // Composition timing rule: Phase 5.2 must surface the relationship between // HTML emission and ce-doc-review so an agent doesn't ship a pre-review // artifact without understanding the gap. ce-doc-review's mutation mechanics // are markdown-only today (its walkthrough applies single-file markdown // edits and the open-questions flow inserts `##`/`###` headings), so HTML // plans skip the 5.3.8 doc-review pass entirely — see plan-handoff.md format // gate. Phase 5.2 must reference that gap inline at the write phase, either // by naming the format gate, calling out that ce-doc-review is skipped on // HTML, or otherwise tying the two phases together so the agent knows what // the first HTML emission does and does not reflect. const phase52Start = SKILL_BODY.indexOf("#### 5.2 Write Plan File") expect(phase52Start).toBeGreaterThan(-1) const phase52Region = SKILL_BODY.slice(phase52Start, phase52Start + 2000) expect( /skipped in HTML|markdown-only|format gate|Phase 5\.3\.8|safe_auto|after.*ce-doc-review/i.test( phase52Region, ), "Phase 5.2 must surface the HTML/ce-doc-review relationship inline — either naming the 5.3.8 format gate that skips ce-doc-review on HTML, or stating that ce-doc-review is markdown-only today, so an agent composing the HTML knows what the artifact does and does not reflect.", ).toBe(true) }) test("Phase 0.0 points at format-rendering refs based on resolved value", () => { const phaseStart = SKILL_BODY.indexOf("#### 0.0") const phaseRegion = SKILL_BODY.slice(phaseStart, phaseStart + 4500) 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 format-keyed option 4 (Proof for md, browser for html)", () => { // Under exclusive output mode, the plan is exactly one artifact — either // .md or .html. The menu's option 4 is format-keyed: Proof for md (Proof // operates on markdown), browser for html. The legacy mutual-exclusion // gate with sibling-rerender logic is gone. const phaseStart = SKILL_BODY.indexOf("##### 5.3.8") expect(phaseStart).toBeGreaterThan(-1) const phaseRegion = SKILL_BODY.slice(phaseStart) expect( /Open in browser/.test(phaseRegion), "SKILL.md Phase 5.4 menu must include 'Open in browser' option for HTML mode.", ).toBe(true) expect( /Publish to Proof/.test(phaseRegion), "SKILL.md Phase 5.4 menu must include 'Publish to Proof' option for markdown mode.", ).toBe(true) expect( /OUTPUT_FORMAT=md|OUTPUT_FORMAT=html|format-keyed/i.test(phaseRegion), "SKILL.md must state the format-keyed rendering rule for option 4.", ).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(SKILL_BODY), "SKILL.md must state that output mode is exclusive — markdown OR HTML, never both. Defends against re-introducing the sibling model.", ).toBe(true) // OUTPUT_FORMAT_SOURCE was used by the sibling tracking; it should not // re-appear. expect( /OUTPUT_FORMAT_SOURCE/.test(SKILL_BODY), "SKILL.md must not reference OUTPUT_FORMAT_SOURCE — the source-tracking variable existed only to support sibling-rerender logic which is removed under exclusive output mode.", ).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) }) 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) }) })