// @vitest-environment node
/*
RFC-0976: Unit tests for auto-commit of generated artifacts after successful validation.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0976: initial unit tests for auto-commit on full-build and distribution-reuse paths.
- 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 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(),
mockIsWorkpieceDirty: vi.fn(),
mockCommitWorkpieceIfDirty: vi.fn(),
mockCommitCacheCloneIfDirty: vi.fn(),
mockResolveCacheClonePath: vi.fn(),
mockCleanupBordbuchOnFailure: vi.fn(),
mockValidateNoStaleMissionEntries: vi.fn(),
}));
vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => {
const original = await importOriginal();
return {
...original,
executeKernelPipeline: hoisted.mockExecuteKernelPipeline,
executeKernelCommand: hoisted.mockExecuteKernelCommand,
};
});
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: hoisted.mockResolveCacheClonePath,
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: [] }),
}));
vi.mock("../mission-git-commit.ts", () => ({
isWorkpieceDirty: hoisted.mockIsWorkpieceDirty,
investigateUntrackedFiles: vi.fn(),
commitWorkpieceIfDirty: hoisted.mockCommitWorkpieceIfDirty,
commitCacheCloneIfDirty: hoisted.mockCommitCacheCloneIfDirty,
cacheCloneCommit: vi.fn(),
}));
vi.mock("../bordbuch-cleanup.ts", () => ({
cleanupBordbuchOnFailure: hoisted.mockCleanupBordbuchOnFailure,
}));
vi.mock("../stale-entries.ts", () => ({
validateNoStaleMissionEntries: hoisted.mockValidateNoStaleMissionEntries,
}));
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-rfc0976-"));
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.mockIsWorkpieceDirty.mockReset();
hoisted.mockCommitWorkpieceIfDirty.mockReset();
hoisted.mockCommitCacheCloneIfDirty.mockReset();
hoisted.mockResolveCacheClonePath.mockReset();
hoisted.mockCleanupBordbuchOnFailure.mockReset();
hoisted.mockValidateNoStaleMissionEntries.mockReset();
hoisted.mockReadMissionManifest.mockResolvedValue(baseManifest);
hoisted.mockResolveMissionDir.mockResolvedValue(missionDir);
hoisted.mockCheckDifferentKindOperation.mockResolvedValue({ blocked: false });
hoisted.mockCommitBordbuchProjections.mockResolvedValue({ committed: false, filesCommitted: [] });
hoisted.mockComputeBuildInputHash.mockResolvedValue({ buildInputHash: "sha256:mismatch" });
hoisted.mockResolveCacheClonePath.mockResolvedValue("/nonexistent/cache-clone");
hoisted.mockCleanupBordbuchOnFailure.mockResolvedValue(undefined);
hoisted.mockValidateNoStaleMissionEntries.mockReturnValue([]);
hoisted.mockCommitCacheCloneIfDirty.mockReturnValue({ committed: false, commitSha: null });
hoisted.mockIsWorkpieceDirty.mockReturnValue({ dirty: false, fileCount: 0, files: [] });
hoisted.mockCommitWorkpieceIfDirty.mockReturnValue({
committed: true,
commitSha: "abc1234567",
});
// Default: runOperation simulates a successful validation
hoisted.mockRunOperation.mockImplementation((_journalPath, _op, ctx) => {
ctx.staticPassed = true;
ctx.buildSucceeded = true;
ctx.stepCount = 5;
ctx.routeCount = 10;
ctx.pipelineReport = { steps: [] };
ctx.failedSteps = [];
return { completed: true };
});
hoisted.mockExecuteKernelPipeline.mockResolvedValue({
ok: true,
steps: [],
timing: { failedStep: null },
});
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
test("RFC-0976: successful validation with uncommitted files → auto-commit called", async () => {
hoisted.mockIsWorkpieceDirty
.mockReturnValueOnce({
dirty: true,
fileCount: 2,
files: ["dist/index.html", "evidence/validation-report.json"],
})
.mockReturnValueOnce({ dirty: false, fileCount: 0, files: [] });
const result = await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCommitWorkpieceIfDirty,
"commitWorkpieceIfDirty must be called when workpiece is dirty and validation passed",
).toHaveBeenCalledTimes(1);
expect(
hoisted.mockCommitWorkpieceIfDirty.mock.calls[0][2],
"commit message must be the RFC-specified message",
).toBe("chore: post-validation artifacts (mission.validate)");
expect(result.summary, "validation must pass").toContain("passed");
});
test("RFC-0976: successful validation with clean workpiece → no auto-commit", async () => {
hoisted.mockIsWorkpieceDirty.mockReturnValue({ dirty: false, fileCount: 0, files: [] });
const result = await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCommitWorkpieceIfDirty,
"commitWorkpieceIfDirty must NOT be called when workpiece is clean",
).not.toHaveBeenCalled();
expect(result.summary, "validation must pass").toContain("passed");
});
test("RFC-0976: failed validation → no auto-commit", async () => {
hoisted.mockRunOperation.mockImplementation((_journalPath, _op, ctx) => {
ctx.staticPassed = false;
ctx.buildSucceeded = false;
ctx.stepCount = 5;
ctx.routeCount = 0;
ctx.pipelineReport = { steps: [{ commandName: "test.check", ok: false, exitCode: 1 }] };
ctx.failedSteps = [{ name: "test.check", error: "failed" }];
return { completed: true };
});
hoisted.mockIsWorkpieceDirty.mockReturnValue({ dirty: true, fileCount: 2, files: ["dist/"] });
const result = await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCommitWorkpieceIfDirty,
"commitWorkpieceIfDirty must NOT be called on validation failure",
).not.toHaveBeenCalled();
expect(result.exitCode, "validation must fail with exitCode 1").toBe(1);
});
test("RFC-0976: --skip-auto-commit → no auto-commit even on success", async () => {
hoisted.mockIsWorkpieceDirty.mockReturnValue({ dirty: true, fileCount: 2, files: ["dist/"] });
const result = await runMissionValidate(
{ argv: [], flags: { mission: "test-mission", "skip-auto-commit": true } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCommitWorkpieceIfDirty,
"commitWorkpieceIfDirty must NOT be called when --skip-auto-commit is set",
).not.toHaveBeenCalled();
expect(result.summary, "validation must still pass").toContain("passed");
});
test("RFC-0976: distribution-reuse path with uncommitted files → auto-commit called", async () => {
// Force distribution-reuse path: hash matches, dist exists
hoisted.mockComputeBuildInputHash.mockResolvedValue({ buildInputHash: "sha256:matching" });
const missionDir = path.join(tmpDir, "missions", "test-mission");
const distributionDir = path.join(missionDir, "distribution");
const distributionDistDir = path.join(distributionDir, "dist");
await fs.mkdir(distributionDistDir, { recursive: true });
await fs.writeFile(
path.join(distributionDir, "build-input-hash.json"),
JSON.stringify({ buildInputHash: "sha256:matching" }),
);
await fs.writeFile(path.join(distributionDistDir, "index.html"), "");
hoisted.mockIsWorkpieceDirty
.mockReturnValueOnce({ dirty: true, fileCount: 1, files: ["dist/index.html"] })
.mockReturnValueOnce({ dirty: false, fileCount: 0, files: [] });
const result = await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCommitWorkpieceIfDirty,
"commitWorkpieceIfDirty must be called on distribution-reuse path when workpiece is dirty",
).toHaveBeenCalledTimes(1);
expect(
hoisted.mockCommitWorkpieceIfDirty.mock.calls[0][2],
"commit message must be the RFC-specified message",
).toBe("chore: post-validation artifacts (mission.validate)");
expect(result.summary, "validation must pass on reuse path").toContain("passed");
expect(result.data, "distributionReused must be true").toHaveProperty("distributionReused", true);
});
test("RFC-0976: auto-commit failure → warning logged, validation still passes", async () => {
hoisted.mockIsWorkpieceDirty.mockReturnValue({ dirty: true, fileCount: 2, files: ["dist/"] });
hoisted.mockCommitWorkpieceIfDirty.mockImplementation(() => {
throw new Error("git commit failed");
});
const result = await runMissionValidate(
{ argv: [], flags: { mission: "test-mission" } } as any,
makeContext(tmpDir),
);
expect(
hoisted.mockCommitWorkpieceIfDirty,
"commitWorkpieceIfDirty must be called (and fail)",
).toHaveBeenCalledTimes(1);
expect(result.summary, "validation must still pass despite auto-commit failure").toContain(
"passed",
);
expect(result.exitCode ?? 0, "exitCode must not be 1 on auto-commit failure").toBe(0);
});