// @vitest-environment node
/*
RFC-1020: Unit tests for stale validation-report.json deletion at the start of runMissionValidate on non-force runs.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1020: initial unit tests for stale validation-report.json deletion on non-force runs.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
*/
import { test, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import os from "node:os";
const hoisted = vi.hoisted(() => ({
mockExecuteKernelPipeline: vi.fn(),
mockExecuteKernelCommand: vi.fn(),
mockReadMissionManifest: vi.fn(),
mockResolveMissionDir: vi.fn(),
mockRunOperation: vi.fn(),
mockCheckDifferentKindOperation: vi.fn(),
mockCommitBordbuchProjections: vi.fn(),
mockComputeBuildInputHash: vi.fn(),
}));
vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => {
const original = await importOriginal();
return {
...original,
executeKernelPipeline: hoisted.mockExecuteKernelPipeline,
executeKernelCommand: hoisted.mockExecuteKernelCommand,
};
});
vi.mock("../../kernel/cache/cache-layer.ts", () => ({
createCacheLayer: vi.fn().mockResolvedValue({
clear: vi.fn(),
close: vi.fn(),
status: vi.fn().mockResolvedValue({ entries: 0 }),
}),
}));
vi.mock("../../kernel/runtime/execute-pipeline.ts", async (importOriginal) => {
const original =
await importOriginal();
return {
...original,
clearPipelineCacheHits: vi.fn(),
};
});
vi.mock("../mission-io.ts", () => ({
readMissionManifest: hoisted.mockReadMissionManifest,
writeMissionManifest: vi.fn(),
resolveMissionDir: hoisted.mockResolveMissionDir,
}));
vi.mock("../../journal/runner.ts", () => ({
runOperation: hoisted.mockRunOperation,
}));
vi.mock("../../journal/index.ts", () => ({
checkDifferentKindOperation: hoisted.mockCheckDifferentKindOperation,
}));
vi.mock("../../bordbuch/bordbuch-commit.ts", () => ({
commitBordbuchProjections: hoisted.mockCommitBordbuchProjections,
}));
vi.mock("../../handoff/build-pipeline-helpers.ts", () => ({
runPipelinePhase: vi.fn(),
computeBuildInputHash: hoisted.mockComputeBuildInputHash,
writePreliminaryBuildIdentity: vi.fn(),
cleanupPreliminaryBuildIdentity: vi.fn(),
}));
vi.mock("../snapshot-auto-regen.ts", () => ({
orchestrateSnap01Recovery: vi.fn(),
bootstrapCommittedSnapshot: vi.fn().mockResolvedValue({ committed: true }),
SNAPSHOT_FILENAME: "behavior.snapshot.generated.yaml",
}));
vi.mock("../actor-identity.ts", () => ({
resolveActor: vi.fn().mockReturnValue({ kind: "human", id: "test-operator" }),
}));
vi.mock("../../sternsystem/registry-io.ts", () => ({
resolveCacheClonePath: vi.fn(),
readSystemConfig: vi.fn(),
}));
vi.mock("../cache-clone-gitignore.ts", () => ({
restoreCacheCloneGitignore: vi.fn(),
untrackForbiddenGeneratedFiles: vi.fn(),
CACHE_CLONE_GENERATED_PATTERNS: [],
}));
vi.mock("../workpiece-config-presence-check.ts", () => ({
checkWorkpieceConfigPresence: vi.fn().mockResolvedValue({ allPresent: true, missing: [] }),
}));
import { runMissionValidate } from "../mission-materialization-commands.ts";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const baseManifest = {
id: "test-mission",
systemId: "test-system",
state: "open",
materializedAt: "2026-01-01T00:00:00Z",
brief: "test",
createdAt: "2026-01-01T00:00:00Z",
};
function makeContext(tmpDir: string) {
return {
io: createDefaultIO().io,
workspaceRoot: tmpDir,
logger: {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
},
} as any;
}
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "test-rfc1020-"));
const missionDir = path.join(tmpDir, "missions", "test-mission");
const evidenceDir = path.join(missionDir, "evidence");
const workpieceDir = path.join(missionDir, "workpiece");
await fs.mkdir(evidenceDir, { recursive: true });
await fs.mkdir(workpieceDir, { recursive: true });
hoisted.mockExecuteKernelPipeline.mockReset();
hoisted.mockExecuteKernelCommand.mockReset();
hoisted.mockReadMissionManifest.mockReset();
hoisted.mockResolveMissionDir.mockReset();
hoisted.mockRunOperation.mockReset();
hoisted.mockCheckDifferentKindOperation.mockReset();
hoisted.mockCommitBordbuchProjections.mockReset();
hoisted.mockComputeBuildInputHash.mockReset();
hoisted.mockReadMissionManifest.mockResolvedValue(baseManifest);
hoisted.mockResolveMissionDir.mockResolvedValue(missionDir);
hoisted.mockCheckDifferentKindOperation.mockResolvedValue({ blocked: false });
hoisted.mockCommitBordbuchProjections.mockResolvedValue({ committed: false, filesCommitted: [] });
hoisted.mockRunOperation.mockResolvedValue({ completed: true });
hoisted.mockExecuteKernelPipeline.mockResolvedValue({
ok: true,
steps: [],
timing: { failedStep: null },
});
hoisted.mockComputeBuildInputHash.mockResolvedValue({ buildInputHash: "sha256:mismatch" });
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
test("RFC-1020: stale validation-report.json is deleted at the start of runMissionValidate on non-force runs", async () => {
const missionDir = path.join(tmpDir, "missions", "test-mission");
const evidenceDir = path.join(missionDir, "evidence");
await fs.writeFile(
path.join(evidenceDir, "validation-report.json"),
'{"passed":false,"stale":true}',
);
await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
existsSync(path.join(evidenceDir, "validation-report.json")),
"stale validation-report.json must be deleted before pipeline steps run on non-force runs",
).toBe(false);
});
test("RFC-1020: missing validation-report.json does not cause errors on non-force runs", async () => {
// No validation-report.json pre-created
await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
// No throw — idempotent deletion via .catch(() => {})
expect(true, "runMissionValidate must not throw when validation-report.json does not exist").toBe(
true,
);
});