/*
RFC-1327 AC-4: mission.reconcile run-twice test — a second reconcile on an
already-reconciled workpiece head exits 0 as a no-op (alreadyReconciled),
leaves the cache-clone tree untouched, and generated files untracked by the
RFC-0913 pass do not resurface as dirty state after the run.
- Not a merge-semantics test — divergence/conflict behaviour lives in the RFC-0918/0568 suites.
- RFC-1327: initial rerun test — full clone+workpiece fixture, generated-file untracking, second-run convergence via expectRerunSafe.
- RFC-1327: run-twice idempotency — declarations, command.idempotency.validate, rerun tests, reconcile/ship/abort convergence fixes
- RFC-1327: explicit test timeouts on rerun tests — root vitest testTimeout is 5s
*/
import { test, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { tmpdir } from "node:os";
import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { expectRerunSafe, snapshotGitTree } from "../testing/run-twice.ts";
vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
executeKernelCommand: vi.fn(async () => ({
exitCode: 0,
data: { systemId: "test-system", syncedAt: "2026-10-17T00:00:00.000Z" },
summary: "ok",
})),
};
});
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 gitCommit(dir: string, msg: string): void {
execSync("git add -A", { cwd: dir, stdio: "pipe" });
execSync(`git commit -m ${JSON.stringify(msg)}`, { cwd: dir, stdio: "pipe" });
}
let testRoot: string;
let tmpWorkspace: string;
let cacheDir: string;
let workpieceDir: string;
beforeEach(() => {
testRoot = mkdtempSync(join(tmpdir(), "tmp-rerun-reconcile-"));
tmpWorkspace = join(testRoot, "werkstatt");
mkdirSync(tmpWorkspace, { recursive: true });
});
afterEach(() => {
rmSync(testRoot, { recursive: true, force: true });
});
function setup(): void {
gitInit(tmpWorkspace);
writeFileSync(join(tmpWorkspace, "README.md"), "# test\n");
gitCommit(tmpWorkspace, "initial");
cacheDir = join(testRoot, "systems-cache", "test-system");
mkdirSync(cacheDir, { recursive: true });
writeFileSync(
join(cacheDir, "system-config.yaml"),
`schemaVersion: system-config/v1
id: test-system
cosmicStar: Vega
mirrors:
- path: "../systems-cache/test-system"
storageType: non-bare
- path: "../systems-cache/test-system.git"
storageType: bare
pinnedPlatform: "4.5.0"
status: active
registeredAt: "2026-01-01T00:00:00Z"
notes: ""
`,
);
writeFileSync(
join(cacheDir, "system-state.yaml"),
`schemaVersion: system-state/v1
systemId: test-system
currentMission: test-system-m000001
lastRelease: null
`,
);
gitInit(cacheDir);
writeFileSync(join(cacheDir, "README.md"), "# system\n");
gitCommit(cacheDir, "initial system");
mkdirSync(join(cacheDir, "bordbuch"), { recursive: true });
writeFileSync(join(cacheDir, "bordbuch", "events.ndjson"), "");
gitCommit(cacheDir, "add bordbuch");
const bareDir = join(testRoot, "systems-cache", "test-system.git");
execSync(`git clone --bare ${JSON.stringify(cacheDir)} ${JSON.stringify(bareDir)}`, {
stdio: "pipe",
});
execSync(`git remote add origin ${JSON.stringify(bareDir)}`, { cwd: cacheDir, stdio: "pipe" });
const missionDir = join(tmpWorkspace, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
mkdirSync(join(missionDir, "evidence"), { recursive: true });
writeFileSync(
join(missionDir, "mission.yaml"),
JSON.stringify(
{
schemaVersion: "1.0.0",
missionId: "test-system-m000001",
systemId: "test-system",
state: "open",
brief: "Test mission",
openedAt: "2026-10-17T00:00:00.000Z",
openedBy: "test-agent",
closedAt: null,
closedBy: null,
pinAtOpen: "1.0.0",
materializedAt: "2026-10-17T01:00:00.000Z",
migratedAt: null,
reconciledAt: null,
releaseId: null,
rfcId: null,
operationId: "op-001",
},
null,
2,
) + "\n",
);
writeFileSync(
join(missionDir, "evidence", "validation-report.json"),
JSON.stringify({
contractFull: { passed: true },
timestamp: "2026-10-17T01:30:00.000Z",
}) + "\n",
);
workpieceDir = join(missionDir, "workpiece");
execSync(`git clone ${JSON.stringify(cacheDir)} ${JSON.stringify(workpieceDir)}`, {
stdio: "pipe",
});
mkdirSync(join(workpieceDir, "src", "content"), { recursive: true });
writeFileSync(join(workpieceDir, "src", "content", "system.md"), "---\nid: test\n---\n# Test\n");
// A generated artifact the RFC-0913 untrack pass must strip from tracking.
writeFileSync(join(workpieceDir, "src", "surface.generated.yaml"), "generated: true\n");
gitCommit(workpieceDir, "workpiece change");
gitCommit(tmpWorkspace, "add mission");
}
test("mission.reconcile second run is a no-op on the same workpiece head (RFC-1327)", { timeout: 60_000 }, async () => {
setup();
const { runMissionReconcile } = await import("./mission-materialization-commands.ts");
const input = {
flags: { mission: "test-system-m000001" },
} as unknown as KernelCommandInput;
const context = {
io: createDefaultIO().io,
workspaceRoot: tmpWorkspace,
logger: { info: () => {}, warn: () => {} },
} as unknown as KernelRuntimeContext;
const { first, second, snapshot } = await expectRerunSafe({
run: async () => {
const r = await runMissionReconcile(input, context);
return { exitCode: r.exitCode, status: r.exitCode === 0 ? "ok" : "failed" };
},
snapshot: () => `${snapshotGitTree(cacheDir)}\n${snapshotGitTree(workpieceDir)}`,
});
expect(first.exitCode).toBe(0);
expect(second.exitCode).toBe(0);
// The untracked generated file must not linger on disk — the rewind on a
// subsequent run would un-ignore it and the dirty guard would fire.
expect(
existsSync(join(cacheDir, "src", "surface.generated.yaml")),
"generated file must be deleted from the clone working tree after untracking",
).toBe(false);
expect(snapshot).not.toContain("surface.generated.yaml");
});