/* RFC-1263: mission.content-edit composite tests — eligibility routing (AC-2), gate ordering + drift stop (AC-1/AC-3), shared reconcile/close precondition (AC-4), fastLane manifest record + bordbuch lane (AC-6), advisory activation. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1263: initial test file. RFC-1263 */ import { test, expect, beforeEach, afterEach, vi } from "vitest"; import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { execSync } from "node:child_process"; import { tmpdir } from "node:os"; import { parse as parseYaml } from "yaml"; import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel"; import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; import { runMissionContentEdit } from "./mission-content-edit.ts"; import { runMissionReconcile } from "./mission-materialization-commands.ts"; import { runMissionClose } from "./mission-close.ts"; import { missionHasGreenCoverage } from "./lifecycle-precondition.ts"; import { readMissionManifest } from "./mission-io.ts"; // ─── mocks ─────────────────────────────────────────────────────────────────── const mockExecuteKernelCommand = vi.hoisted(() => vi.fn()); const mockRunMissionValidate = vi.hoisted(() => vi.fn(async () => ({ exitCode: 0, data: { contractFull: { passed: true, validators: [] } }, summary: "Validation passed", })), ); const mockAppendBordbuch = vi.hoisted(() => vi.fn(async () => ({ entry: { id: "event-000001", kind: "mission-close" }, commitResult: { commitSha: "abc123", pushed: true, error: null }, })), ); vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, executeKernelCommand: mockExecuteKernelCommand }; }); vi.mock("../mission/mission-materialization-commands.ts", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, // mission.close's inline gate is the only surface swapped — runMissionReconcile // stays real so AC-4 exercises the actual coverage predicate. runMissionValidate: mockRunMissionValidate, }; }); vi.mock("../bordbuch/bordbuch-io.ts", () => ({ validateBordbuch: vi.fn(async () => ({ entries: 0, violations: [] })), readBordbuch: vi.fn(async () => []), commitAndPushBordbuch: vi.fn(async () => ({ commitSha: "abc123", pushed: true, error: null })), })); vi.mock("../bordbuch/bordbuch-commit-helper.ts", () => ({ appendAndCommitBordbuch: mockAppendBordbuch, })); // ─── fixture ───────────────────────────────────────────────────────────────── const SITE = "test-sys"; const MISSION = `${SITE}-m000001`; let testRoot: string; let workspaceRoot: string; // tmpDir/workspace — engine workspace let cacheClone: string; // tmpDir/systems-cache/ — sibling convention let workpiece: string; // missions//workpiece const SYSTEM_MD = `--- app: ${SITE} version: "1.0.0" identity: systemStar: Polaris biome: core clientEditable: - pages - prose --- # System `; function write(base: string, rel: string, content: string): void { const abs = join(base, rel); mkdirSync(join(abs, ".."), { recursive: true }); writeFileSync(abs, content); } function gitInit(dir: string): void { execSync("git init -b main", { cwd: dir, stdio: "pipe" }); execSync("git config user.email test@test.com", { cwd: dir, stdio: "pipe" }); execSync("git config user.name Test", { cwd: dir, stdio: "pipe" }); } function gitCommitAll(dir: string, msg: string): void { execSync("git add -A", { cwd: dir, stdio: "pipe" }); execSync(`git commit -m ${JSON.stringify(msg)}`, { cwd: dir, stdio: "pipe" }); } function seedCacheClone(contentEditLane?: string): void { write( cacheClone, "system-state.yaml", [ `schemaVersion: "1.0.0"`, `systemId: ${SITE}`, `currentMission: ${MISSION}`, `lastRelease: null`, `accessPin: null`, `passportRequired: false`, `ownershipRequired: false`, contentEditLane ? `contentEditLane: ${contentEditLane}` : null, "", ] .filter((l): l is string => l !== null) .join("\n"), ); write(cacheClone, "src/content/system.md", SYSTEM_MD); } function seedMission(opts: { fastLane?: object } = {}): void { const missionDir = join(workspaceRoot, "missions", MISSION); mkdirSync(join(missionDir, "evidence"), { recursive: true }); mkdirSync(join(missionDir, "workpiece"), { recursive: true }); workpiece = join(missionDir, "workpiece"); write(workpiece, "src/content/system.md", SYSTEM_MD); write(workpiece, "src/content/pages/de/test.md", "---\ntitle: Alt\nlang: de\n---\n"); gitInit(workpiece); gitCommitAll(workpiece, "materialize"); const manifest = { schemaVersion: "1.0.0", missionId: MISSION, systemId: SITE, state: "open", brief: "Test mission", openedAt: "2026-07-30T00:00:00.000Z", openedBy: "test-agent", closedAt: null, closedBy: null, pinAtOpen: "1.0.0", materializedAt: "2026-07-30T01:00:00.000Z", migratedAt: null, reconciledAt: null, releaseId: null, rfcId: null, operationId: "op-001", ...(opts.fastLane ? { fastLane: opts.fastLane } : {}), }; writeFileSync(join(missionDir, "mission.yaml"), JSON.stringify(manifest, null, 2) + "\n"); } function makeInput(flags: Record): KernelCommandInput { return { argv: [], flags } as unknown as KernelCommandInput; } function makeContext(): KernelRuntimeContext { return { io: createDefaultIO().io, workspaceRoot, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, debug: () => {} }, dryRun: false, siteExplicit: false, outputFormat: "json", } as unknown as KernelRuntimeContext; } /** Default green gate chain for the composite's delegated steps. */ function mockGreenSteps(overrides: Record = {}): void { mockExecuteKernelCommand.mockImplementation(async ({ commandName }: { commandName: string }) => { if (overrides[commandName]) { return { exitCode: overrides[commandName].exitCode, data: overrides[commandName].data ?? {} }; } switch (commandName) { case "mission.open": return { exitCode: 0, data: { missionId: MISSION, state: "open" } }; case "content.patch": return { exitCode: 0, data: { status: "applied", diagnostics: [], results: [], validators: [{ command: "typography.validate", status: "pass", violations: 0 }], }, }; case "content.regression.check": return { exitCode: 0, data: { diagnostics: [] } }; case "mission.deploy-preflight": return { exitCode: 0, data: { totalViolations: 1 } }; case "mission.git.commit": return { exitCode: 0, data: { commitSha: "c".repeat(40) } }; case "mission.reconcile": return { exitCode: 0, data: {} }; case "mission.close": return { exitCode: 0, data: { state: "closed" } }; default: return { exitCode: 0, data: {} }; } }); } function callsOf(name: string): string[][] { return mockExecuteKernelCommand.mock.calls .filter((c) => (c[0] as { commandName: string }).commandName === name) .map((c) => (c[0] as { argv: string[] }).argv); } function writeOpsFile(entries: Array>): string { const opsPath = join(testRoot, "ops.yaml"); writeFileSync(opsPath, `ops:\n${entries.map((e) => ` - ${JSON.stringify(e)}`).join("\n")}\n`); return opsPath; } beforeEach(() => { mockExecuteKernelCommand.mockReset(); mockRunMissionValidate.mockClear(); mockAppendBordbuch.mockClear(); testRoot = mkdtempSync(join(tmpdir(), "tmp-content-edit-")); workspaceRoot = join(testRoot, "workspace"); cacheClone = join(testRoot, "systems-cache", SITE); mkdirSync(workspaceRoot, { recursive: true }); }); afterEach(() => { rmSync(testRoot, { recursive: true, force: true }); }); // ─── AC-1: admitted mission runs the gate set, commits, reconciles, closes ──── test("AC-1 advisory: ops file inside clientEditable runs gates, commits, reconciles, stops before close", async () => { seedMission(); seedCacheClone(); // no contentEditLane → advisory mockGreenSteps(); const opsFile = writeOpsFile([{ op: "set", selector: "pages/de/test#title", value: "Neu" }]); const res = await runMissionContentEdit( makeInput({ mission: MISSION, "ops-file": opsFile }), makeContext(), ); expect(res.exitCode).toBe(0); expect(res.data?.status).toBe("fast-lane-ready"); expect(res.data?.lane).toBe("fast"); expect(res.data?.reconciled).toBe(true); expect(res.data?.closed).toBe(false); const order = mockExecuteKernelCommand.mock.calls.map( (c) => (c[0] as { commandName: string }).commandName, ); expect(order).toEqual([ "content.patch", "content.regression.check", "mission.deploy-preflight", "mission.git.commit", "mission.reconcile", ]); expect(callsOf("mission.close")).toHaveLength(0); // Gate record persisted on the mission manifest (AC-6 first half). const manifest = await readMissionManifest(workspaceRoot, MISSION); expect(manifest.fastLane?.green).toBe(true); expect(manifest.fastLane?.gates.map((g) => g.gate)).toEqual([ "content-ops", "author-validators", "content-regression", "deploy-preflight", "mission.git.commit", "mission.reconcile", ]); }); test("AC-1 active: contentEditLane=active closes the mission after reconcile", async () => { seedMission(); seedCacheClone("active"); mockGreenSteps(); const opsFile = writeOpsFile([{ op: "set", selector: "pages/de/test#title", value: "Neu" }]); const res = await runMissionContentEdit( makeInput({ mission: MISSION, "ops-file": opsFile }), makeContext(), ); expect(res.exitCode).toBe(0); expect(res.data?.status).toBe("closed"); expect(res.data?.closed).toBe(true); expect(callsOf("mission.close")).toHaveLength(1); const gates = res.data?.gates ?? []; expect(gates.find((g) => g.gate === "deploy-preflight")?.status).toBe("advisory"); expect(gates.every((g) => g.status !== "fail")).toBe(true); }); test("AC-1 open path: --site opens a content-lane mission then runs the gate set", async () => { seedCacheClone(); // advisory — stops before close write(cacheClone, "src/content/pages/de/test.md", "---\ntitle: Alt\nlang: de\n---\n"); mockExecuteKernelCommand.mockImplementation( async ({ commandName, argv }: { commandName: string; argv: string[] }) => { if (commandName === "mission.open") { // Emulate the materialized mission the real open produces. const missionDir = join(workspaceRoot, "missions", MISSION); workpiece = join(missionDir, "workpiece"); mkdirSync(join(missionDir, "evidence"), { recursive: true }); write(workpiece, "src/content/system.md", SYSTEM_MD); write(workpiece, "src/content/pages/de/test.md", "---\ntitle: Alt\nlang: de\n---\n"); gitInit(workpiece); gitCommitAll(workpiece, "materialize"); writeFileSync( join(missionDir, "mission.yaml"), JSON.stringify( { schemaVersion: "1.0.0", missionId: MISSION, systemId: SITE, state: "open", brief: "test", openedAt: "2026-07-30T00:00:00.000Z", openedBy: "test-agent", closedAt: null, closedBy: null, pinAtOpen: "1.0.0", materializedAt: "2026-07-30T01:00:00.000Z", migratedAt: null, reconciledAt: null, releaseId: null, rfcId: null, operationId: "op-001", lane: "content", }, null, 2, ) + "\n", ); expect(argv).toContain("--lane=content"); return { exitCode: 0, data: { missionId: MISSION, state: "open" } }; } if (commandName === "content.patch") { return { exitCode: 0, data: { status: "applied", diagnostics: [], results: [], validators: [] }, }; } if (commandName === "mission.git.commit") { return { exitCode: 0, data: { commitSha: "c".repeat(40) } }; } return { exitCode: 0, data: {} }; }, ); const opsFile = writeOpsFile([{ op: "set", selector: "pages/de/test#title", value: "Neu" }]); const res = await runMissionContentEdit( makeInput({ site: SITE, brief: "wording fix", "ops-file": opsFile }), makeContext(), ); expect(res.exitCode).toBe(0); expect(res.data?.mission).toBe(MISSION); expect(res.data?.status).toBe("fast-lane-ready"); const order = mockExecuteKernelCommand.mock.calls.map( (c) => (c[0] as { commandName: string }).commandName, ); expect(order[0]).toBe("mission.open"); expect(order).toContain("mission.reconcile"); }); // ─── AC-2: ineligible touched set → lane "full", nothing runs ───────────────── test("AC-2: selector outside clientEditable rejects with lane full and runs nothing", async () => { seedMission(); seedCacheClone(); mockGreenSteps(); const opsFile = writeOpsFile([{ op: "set", selector: "config/secrets#token", value: "x" }]); const res = await runMissionContentEdit( makeInput({ mission: MISSION, "ops-file": opsFile }), makeContext(), ); expect(res.exitCode).toBe(1); expect(res.data?.status).toBe("rejected"); expect(res.data?.lane).toBe("full"); expect(res.data?.eligibility.reasons.join(" ")).toContain("clientEditable"); expect(mockExecuteKernelCommand).not.toHaveBeenCalled(); }); test("AC-2: staged file outside src/content rejects", async () => { seedMission(); seedCacheClone(); mockGreenSteps(); // Dirty a non-content file in the workpiece. write(workpiece, "astro.config.mjs", "export default {};\n"); const res = await runMissionContentEdit(makeInput({ mission: MISSION }), makeContext()); expect(res.data?.status).toBe("rejected"); expect(res.data?.lane).toBe("full"); expect(res.data?.eligibility.reasons.join(" ")).toContain("src/content/"); expect(mockExecuteKernelCommand).not.toHaveBeenCalled(); }); test("AC-2: non-editable generated path rejects with owner reason", async () => { seedMission(); seedCacheClone(); mockGreenSteps(); write( workpiece, ".werkstatt/generated-paths.generated.yaml", `version: 1\npaths:\n - path: "src/content/pages/gen/**"\n owner: "pages.gen"\n`, ); write(workpiece, "src/content/pages/gen/x.md", "---\ntitle: g\nlang: de\n---\n"); const opsFile = writeOpsFile([{ op: "set", selector: "pages/gen/x#title", value: "y" }]); const res = await runMissionContentEdit( makeInput({ mission: MISSION, "ops-file": opsFile }), makeContext(), ); expect(res.data?.status).toBe("rejected"); expect(res.data?.eligibility.reasons.join(" ")).toContain("generated"); expect(mockExecuteKernelCommand).not.toHaveBeenCalled(); }); test("AC-2: block.* op routes to the full lane", async () => { seedMission(); seedCacheClone(); mockGreenSteps(); const opsFile = writeOpsFile([{ op: "block.remove", selector: "pages/de/test#blocks[id=hero]" }]); const res = await runMissionContentEdit( makeInput({ mission: MISSION, "ops-file": opsFile }), makeContext(), ); expect(res.data?.status).toBe("rejected"); expect(res.data?.lane).toBe("full"); expect(res.data?.eligibility.reasons.join(" ")).toContain("structural"); expect(mockExecuteKernelCommand).not.toHaveBeenCalled(); }); // ─── AC-3: CREG drift stops before commit ───────────────────────────────────── test("AC-3: content-regression drift stops before mission.git.commit", async () => { seedMission(); seedCacheClone(); mockGreenSteps({ "content.regression.check": { exitCode: 1, data: { diagnostics: [{ severity: "error", ruleId: "CREG-01" }] }, }, }); const opsFile = writeOpsFile([{ op: "set", selector: "pages/de/test#title", value: "Neu" }]); const res = await runMissionContentEdit( makeInput({ mission: MISSION, "ops-file": opsFile }), makeContext(), ); expect(res.exitCode).toBe(1); expect(res.data?.status).toBe("gate-failed"); expect(res.data?.gates.find((g) => g.gate === "content-regression")?.status).toBe("fail"); expect(res.summary).toContain("RFC-0734"); expect(callsOf("mission.git.commit")).toHaveLength(0); // No fastLane record persisted — reconcile must still refuse the mission. const manifest = await readMissionManifest(workspaceRoot, MISSION); expect(manifest.fastLane).toBeUndefined(); }); // ─── AC-4: reconcile accepts green fastLane record; refuses neither ─────────── test("AC-4: missionHasGreenCoverage — report green, fastLane green, neither", () => { const base = {} as Parameters[0]; expect(missionHasGreenCoverage(base, { contractFull: { passed: true } })).toBe(true); expect( missionHasGreenCoverage( { fastLane: { green: true, gates: [], recordedAt: "2026-07-30T00:00:00.000Z" } }, null, ), ).toBe(true); expect(missionHasGreenCoverage(base, { contractFull: { passed: false } })).toBe(false); expect(missionHasGreenCoverage(base, null)).toBe(false); }); test("AC-4: reconcile without report and without fastLane refuses", async () => { seedMission(); seedCacheClone(); await expect(runMissionReconcile(makeInput({ mission: MISSION }), makeContext())).rejects.toThrow( "run mission.validate first", ); }); test("AC-4: reconcile with green fastLane record and no validation report proceeds past the coverage gate", async () => { seedMission({ fastLane: { green: true, gates: [{ gate: "content-ops", status: "pass" }], recordedAt: "2026-07-30T03:00:00.000Z", }, }); seedCacheClone(); // The coverage gate passes — reconcile then fails on a downstream step // (locks/git in a partial fixture), never on the validation precondition. const err = await runMissionReconcile(makeInput({ mission: MISSION }), makeContext()).then( () => null, (e: unknown) => (e instanceof Error ? e.message : String(e)), ); expect(err === null || !/mission\.validate|validation did not pass/.test(err)).toBe(true); }); // ─── AC-6: close bordbuch event carries lane ────────────────────────────────── test("AC-6: mission.close on a fast-lane mission skips inline validate and records lane: fast", async () => { seedMission({ fastLane: { green: true, gates: [{ gate: "content-ops", status: "pass" }], recordedAt: "2026-07-30T03:00:00.000Z", }, }); seedCacheClone("active"); // Close needs reconciledAt + the reconciliation report the mirror check reads. const missionDir = join(workspaceRoot, "missions", MISSION); const manifestRaw = parseYaml(readFileSync(join(missionDir, "mission.yaml"), "utf8")) as Record< string, unknown >; manifestRaw.reconciledAt = "2026-07-30T02:00:00.000Z"; writeFileSync(join(missionDir, "mission.yaml"), JSON.stringify(manifestRaw, null, 2) + "\n"); const head = execSync("git rev-parse HEAD", { cwd: workpiece, encoding: "utf-8" }).trim(); write( missionDir, "evidence/reconciliation-report.json", JSON.stringify({ workpieceHeadAtReconcile: head }) + "\n", ); gitInit(testRoot); write( cacheClone, "system-config.yaml", `schemaVersion: system-config/v1\nid: ${SITE}\ncosmicStar: Vega\nmirrors:\n - path: "../systems-cache/${SITE}"\n storageType: non-bare\npinnedPlatform: "1.0.0"\nstatus: active\nregisteredAt: "2026-01-01T00:00:00Z"\nnotes: ""\n`, ); write(cacheClone, "system.pin.json", JSON.stringify({ platform: { version: "1.0.0" } })); mkdirSync(join(cacheClone, "bordbuch"), { recursive: true }); write(cacheClone, "bordbuch/events.ndjson", ""); gitCommitAll(testRoot, "fixture baseline"); // Close delegates several steps (template sync, pin, claims, sync) through // executeKernelCommand — everything is green in this fixture. mockExecuteKernelCommand.mockImplementation(async () => ({ exitCode: 0, data: {} })); const res = await runMissionClose( makeInput({ mission: MISSION, actor: "test-agent", "allow-no-op": true, "skip-reconcile-check": true, "skip-auto-sync": true, "skip-evidence-sync": true, }), makeContext(), ); expect(res.exitCode).toBe(0); expect(mockRunMissionValidate).not.toHaveBeenCalled(); const closeCall = (mockAppendBordbuch.mock.calls as unknown as unknown[][]).find( (c) => c[2] === "mission-close" && (c[3] as string).includes("closed"), ); expect(closeCall).toBeDefined(); expect((closeCall![5] as { metadata?: { lane?: string } }).metadata?.lane).toBe("fast"); expect(res.data?.state).toBe("closed"); });