// @vitest-environment node
/*
Unit tests for mission validate force clear covering its documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0973: initial unit tests for --force auto-clears all caches and stale artifacts.
- 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(() => ({
mockCacheClear: vi.fn(),
mockCacheClose: vi.fn(),
mockClearPipelineCacheHits: vi.fn(),
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: hoisted.mockCacheClear,
close: hoisted.mockCacheClose,
status: vi.fn().mockResolvedValue({ entries: 0 }),
}),
}));
vi.mock("../../kernel/runtime/execute-pipeline.ts", async (importOriginal) => {
const original =
await importOriginal();
return {
...original,
clearPipelineCacheHits: hoisted.mockClearPipelineCacheHits,
};
});
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-rfc0973-"));
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.mockCacheClear.mockReset();
hoisted.mockCacheClose.mockReset();
hoisted.mockClearPipelineCacheHits.mockReset();
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 },
});
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
test("RFC-0973: --force clears kernel cache, pipeline cache hits, journal, and validation report", async () => {
const missionDir = path.join(tmpDir, "missions", "test-mission");
const evidenceDir = path.join(missionDir, "evidence");
// Pre-create stale artifacts
await fs.writeFile(path.join(missionDir, "journal.jsonl"), "stale journal\n");
await fs.writeFile(path.join(evidenceDir, "validation-report.json"), '{"stale":true}');
await runMissionValidate(
{ argv: [], flags: { mission: "test-mission", force: true } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCacheClear,
"kernel cache.clear() must be called on --force",
).toHaveBeenCalledTimes(1);
expect(hoisted.mockCacheClose, "cache.close() must be called after clear").toHaveBeenCalledTimes(
1,
);
expect(
hoisted.mockClearPipelineCacheHits,
"clearPipelineCacheHits must be called on --force",
).toHaveBeenCalledTimes(1);
expect(
existsSync(path.join(missionDir, "journal.jsonl")),
"journal.jsonl must be deleted on --force",
).toBe(false);
expect(
existsSync(path.join(evidenceDir, "validation-report.json")),
"validation-report.json must be deleted on --force",
).toBe(false);
});
test("RFC-0973: without --force, no caches or artifacts are cleared", async () => {
const missionDir = path.join(tmpDir, "missions", "test-mission");
const evidenceDir = path.join(missionDir, "evidence");
await fs.writeFile(path.join(missionDir, "journal.jsonl"), "stale journal\n");
await fs.writeFile(path.join(evidenceDir, "validation-report.json"), '{"stale":true}');
// Without force, distribution reuse path may trigger — mock computeBuildInputHash to not match
hoisted.mockComputeBuildInputHash.mockResolvedValue({ buildInputHash: "sha256:mismatch" });
await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCacheClear,
"cache.clear() must NOT be called without --force",
).not.toHaveBeenCalled();
expect(
hoisted.mockClearPipelineCacheHits,
"clearPipelineCacheHits must NOT be called without --force",
).not.toHaveBeenCalled();
// journal.jsonl may be modified by runOperation, but it should not be deleted before runOperation
// The key assertion is that cache clear functions were not called
});
test("RFC-0973: missing journal and validation report do not cause errors on --force", async () => {
// No journal.jsonl or validation-report.json pre-created
await runMissionValidate(
{ argv: [], flags: { mission: "test-mission", force: true } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCacheClear,
"cache.clear() must be called even without stale files",
).toHaveBeenCalledTimes(1);
expect(
hoisted.mockClearPipelineCacheHits,
"clearPipelineCacheHits must be called even without stale files",
).toHaveBeenCalledTimes(1);
// No throw — idempotent deletion via .catch(() => {})
});
test("RFC-0973: force:true is passed to executeKernelPipeline calls when --force is set", async () => {
await runMissionValidate(
{ argv: [], flags: { mission: "test-mission", force: true } } as any,
makeContext(tmpDir),
);
// runOperation calls buildValidateSteps which calls executeKernelPipeline
// The mock for runOperation returns { completed: true } without calling steps,
// so we verify via the runOperation call that steps were built with force in ctx.
expect(hoisted.mockRunOperation, "runOperation must be called").toHaveBeenCalledTimes(1);
const opArg = hoisted.mockRunOperation.mock.calls[0][1];
expect(opArg.op, "operation name must be mission.validate").toBe("mission.validate");
// The steps array and validateCtx are passed — verify ctx has force=true
const validateCtx = hoisted.mockRunOperation.mock.calls[0][2];
expect(validateCtx.force, "validateCtx.force must be true when --force is set").toBe(true);
});