/*
Unit tests for reconciler covering RFC-1038: computeDelta; RFC-1038: reconcile; RFC-1038: buildActualState and buildDesiredState.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the reconciler suite's contract for test-file scaffold coverage.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
computeDelta,
reconcile,
buildActualState,
buildDesiredState,
resetReconciliationLock,
reconcileFromPersisted,
} from "../reconciler.ts";
import { persistDesiredState, loadPersistedDesiredState } from "../desired-state-persistence.ts";
import { validateDeclarations } from "../validate-declarations.ts";
import type {
ComponentDeclaration,
DesiredState,
MutableActualState,
ModuleExport,
CommandDeclaration,
ModuleBuildContext,
} from "@warpgogol/werkstatt-shared/kernel";
import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint";
const VALID_SHA = ("sha256:" + "a".repeat(64)) as Sha256Digest;
function makeDeclaration(
id: string,
overrides: Partial = {},
): ComponentDeclaration {
return {
schema: "werkstatt/component-declaration@1",
componentId: id as `${string}/${string}`,
version: "1.0.0",
artifactHash: VALID_SHA,
scope: "per-workshop",
provides: [],
requires: [],
requestedGrants: [],
effects: [],
isolation: { tier: 0, adapterId: null },
resources: [],
priority: 0,
active: true,
...overrides,
};
}
function makeDesiredState(
components: ComponentDeclaration[] = [],
overrides: Partial = {},
): DesiredState {
return {
components: new Map(components.map((c) => [c.componentId, c])),
requiredCapabilities: [],
availableArtifacts: new Map(),
admittedGrants: [],
profileId: "test-profile",
...overrides,
};
}
function makeActualState(components: ComponentDeclaration[] = []): MutableActualState {
return {
components: new Map(
components.map((c) => [c.componentId, { declaration: c, state: "active" as const }]),
),
commands: new Map(),
pipelines: new Map(),
};
}
function makeModuleExport(name: string, overrides: Partial = {}): ModuleExport {
return {
name,
version: "1.0.0",
declarations: [],
commands: [],
pipelines: [],
...overrides,
};
}
describe("RFC-1038: computeDelta", () => {
it("AC-3: component in desired but not actual → toActivate", () => {
const decl = makeDeclaration("werkstatt/engine");
const desired = makeDesiredState([decl]);
const actual = makeActualState();
const delta = computeDelta(desired, actual);
expect(delta.toActivate).toHaveLength(1);
expect(delta.toActivate[0]!.componentId).toBe("werkstatt/engine");
});
it("AC-4: component in actual but not desired → toDeactivate", () => {
const decl = makeDeclaration("werkstatt/old");
const desired = makeDesiredState();
const actual = makeActualState([decl]);
const delta = computeDelta(desired, actual);
expect(delta.toDeactivate).toEqual(["werkstatt/old"]);
});
it("AC-5: config change → toReconfigure", () => {
const decl = makeDeclaration("werkstatt/engine", { config: { timeout: 100 } });
const oldDecl = makeDeclaration("werkstatt/engine", { config: { timeout: 200 } });
const desired = makeDesiredState([decl]);
const actual = makeActualState([oldDecl]);
const delta = computeDelta(desired, actual);
expect(delta.toReconfigure).toHaveLength(1);
expect(delta.toReconfigure[0]!.id).toBe("werkstatt/engine");
expect(delta.toReconfigure[0]!.oldConfig).toEqual({ timeout: 200 });
expect(delta.toReconfigure[0]!.newConfig).toEqual({ timeout: 100 });
});
it("AC-9: missing dependency → missingDependencies", () => {
const decl = makeDeclaration("werkstatt/engine", {
requires: [
{
capability: "werkstatt/missing-cap",
compatibility: "^1.0.0",
schemaHash: VALID_SHA as string,
optional: false,
},
],
});
const desired = makeDesiredState([decl]);
const actual = makeActualState();
const delta = computeDelta(desired, actual);
expect(delta.missingDependencies).toHaveLength(1);
expect(delta.missingDependencies[0]!.capability).toBe("werkstatt/missing-cap");
expect(delta.missingDependencies[0]!.requiredBy).toBe("werkstatt/engine");
});
it("AC-10: idempotent — same desired and actual → no changes", () => {
const decl = makeDeclaration("werkstatt/engine");
const desired = makeDesiredState([decl]);
const actual = makeActualState([decl]);
const delta = computeDelta(desired, actual);
expect(delta.toActivate).toHaveLength(0);
expect(delta.toDeactivate).toHaveLength(0);
expect(delta.toReconfigure).toHaveLength(0);
expect(delta.unchanged).toEqual(["werkstatt/engine"]);
});
it("AC-14: empty desired state → deactivate all", () => {
const decl1 = makeDeclaration("werkstatt/a");
const decl2 = makeDeclaration("werkstatt/b");
const desired = makeDesiredState();
const actual = makeActualState([decl1, decl2]);
const delta = computeDelta(desired, actual);
expect(delta.toActivate).toHaveLength(0);
expect(delta.toDeactivate).toHaveLength(2);
expect(delta.toDeactivate).toContain("werkstatt/a");
expect(delta.toDeactivate).toContain("werkstatt/b");
});
});
describe("RFC-1038: reconcile", () => {
beforeEach(() => {
resetReconciliationLock();
});
it("AC-2: computes delta and applies transactionally", async () => {
const decl = makeDeclaration("werkstatt/engine");
const desired = makeDesiredState([decl]);
const actual = makeActualState();
const result = await reconcile(desired, actual);
expect(result.applied).toBe(true);
expect(result.delta.toActivate).toHaveLength(1);
expect(actual.components.has("werkstatt/engine")).toBe(true);
});
it("AC-3: activates component in desired but not actual", async () => {
const decl = makeDeclaration("werkstatt/new");
const desired = makeDesiredState([decl]);
const actual = makeActualState();
await reconcile(desired, actual);
expect(actual.components.get("werkstatt/new")?.state).toBe("active");
});
it("AC-4: deactivates component in actual but not desired (LIFO)", async () => {
const decl1 = makeDeclaration("werkstatt/a");
const decl2 = makeDeclaration("werkstatt/b");
const desired = makeDesiredState();
const actual = makeActualState([decl1, decl2]);
const deactivated: string[] = [];
await reconcile(desired, actual, {
onDeactivate: async (id) => {
deactivated.push(id);
},
});
expect(actual.components.size).toBe(0);
expect(deactivated).toHaveLength(2);
});
it("AC-5: reconfigures component on config change", async () => {
const decl = makeDeclaration("werkstatt/engine", { config: { timeout: 100 } });
const oldDecl = makeDeclaration("werkstatt/engine", { config: { timeout: 200 } });
const desired = makeDesiredState([decl]);
const actual = makeActualState([oldDecl]);
let reconfigured = false;
await reconcile(desired, actual, {
onReconfigure: async () => {
reconfigured = true;
},
});
expect(reconfigured).toBe(true);
expect(actual.components.get("werkstatt/engine")?.declaration.config).toEqual({
timeout: 100,
});
});
it("AC-6: activation failure → applied: false with failure reason", async () => {
const decl = makeDeclaration("werkstatt/fail");
const desired = makeDesiredState([decl]);
const actual = makeActualState();
const result = await reconcile(desired, actual, {
onActivate: async () => {
throw new Error("activation error");
},
});
expect(result.applied).toBe(false);
expect(result.failures).toHaveLength(1);
expect(result.failures[0]!.reason).toBe("activation error");
});
it("AC-10: idempotent — second run produces no changes", async () => {
const decl = makeDeclaration("werkstatt/engine");
const desired = makeDesiredState([decl]);
const actual = makeActualState();
const result1 = await reconcile(desired, actual);
expect(result1.applied).toBe(true);
const result2 = await reconcile(desired, actual);
expect(result2.delta.toActivate).toHaveLength(0);
expect(result2.delta.toDeactivate).toHaveLength(0);
expect(result2.delta.toReconfigure).toHaveLength(0);
});
it("AC-12: concurrent reconciliation → applied: false without blocking", async () => {
const decl = makeDeclaration("werkstatt/engine");
const desired = makeDesiredState([decl]);
const actual = makeActualState();
const slowActivate = new Promise((resolve) => setTimeout(resolve, 50));
const result1 = reconcile(desired, actual, {
onActivate: async () => {
await slowActivate;
},
});
const result2 = await reconcile(desired, actual);
expect(result2.applied).toBe(false);
expect(result2.failures.some((f) => f.reason === "reconciliation in progress")).toBe(true);
await result1;
});
it("AC-14: empty desired state → deactivate all without error", async () => {
const decl1 = makeDeclaration("werkstatt/a");
const decl2 = makeDeclaration("werkstatt/b");
const desired = makeDesiredState();
const actual = makeActualState([decl1, decl2]);
const result = await reconcile(desired, actual);
expect(result.applied).toBe(true);
expect(actual.components.size).toBe(0);
});
});
describe("RFC-1038: buildActualState and buildDesiredState", () => {
it("AC-15: module exports ModuleExport — buildActualState reads commands", () => {
const cmd: CommandDeclaration = {
name: "test.ping",
modulePath: "test",
description: "Test command",
scope: "workspace",
flags: {},
execute: async () => {},
};
const mod = makeModuleExport("test", { commands: [cmd] });
const actual = buildActualState([mod]);
expect(actual.commands.get("test.ping")).toBe(cmd);
});
it("AC-17: CommandDeclaration carries all metadata fields", () => {
const cmd: CommandDeclaration = {
name: "test.full",
modulePath: "packages/test/test.module.ts",
description: "Full metadata command",
scope: "workspace",
flags: { verbose: { kind: "boolean", description: "Verbose" } },
reads: ["src/**"],
writes: ["dist/**"],
modulePaths: ["src/test.ts"],
validatesOutputs: ["test.validate"],
generates: [{ path: "dist/test.js", phase: "build.post" }],
contract: "test-contract",
rules: ["TEST-01"],
execute: async () => {},
};
const mod = makeModuleExport("test", { commands: [cmd] });
const actual = buildActualState([mod]);
const retrieved = actual.commands.get("test.full")!;
expect(retrieved.flags).toBeDefined();
expect(retrieved.reads).toEqual(["src/**"]);
expect(retrieved.writes).toEqual(["dist/**"]);
expect(retrieved.modulePaths).toEqual(["src/test.ts"]);
expect(retrieved.validatesOutputs).toEqual(["test.validate"]);
expect(retrieved.modulePath).toBe("packages/test/test.module.ts");
expect(retrieved.generates).toHaveLength(1);
expect(retrieved.contract).toBe("test-contract");
expect(retrieved.rules).toEqual(["TEST-01"]);
expect(typeof retrieved.execute).toBe("function");
});
it("AC-18: PipelineDeclaration carries name and steps with dependsOn", () => {
const mod = makeModuleExport("test", {
pipelines: [
{
name: "test.pipeline",
steps: [{ command: "test.step1" }, { command: "test.step2", dependsOn: ["test.step1"] }],
},
],
});
const actual = buildActualState([mod]);
const pipeline = actual.pipelines.get("test.pipeline")!;
expect(pipeline.steps).toHaveLength(2);
expect(pipeline.steps[1]!.dependsOn).toEqual(["test.step1"]);
});
it("AC-19: conditionalBuilder receives ModuleBuildContext and returns declarations", () => {
let receivedContext: ModuleBuildContext | null = null;
const extraDecl = makeDeclaration("werkstatt/conditional");
const mod = makeModuleExport("test", {
conditionalBuilder: (ctx) => {
receivedContext = ctx;
return { declarations: [extraDecl] };
},
});
const context: ModuleBuildContext = {
env: { TEST_FLAG: "1" },
profileId: "test-profile",
longRunning: false,
};
const result = mod.conditionalBuilder!(context);
expect(receivedContext).toBe(context);
expect(result.declarations).toHaveLength(1);
expect(result.declarations![0]!.componentId).toBe("werkstatt/conditional");
});
it("COMPOSITION-02: duplicate command across modules throws", () => {
const cmd: CommandDeclaration = {
name: "test.dup",
modulePath: "test",
description: "dup",
scope: "workspace",
flags: {},
execute: async () => {},
};
const mod1 = makeModuleExport("mod-a", { commands: [cmd] });
const mod2 = makeModuleExport("mod-b", {
commands: [{ ...cmd, modulePath: "test2", execute: async () => ({ exitCode: 1 }) }],
});
expect(() => buildActualState([mod1, mod2])).toThrow(/COMPOSITION-02/);
});
it("COMPOSITION-04: duplicate component declaration throws", () => {
const decl = makeDeclaration("werkstatt/dup");
const mod1 = makeModuleExport("mod-a", { declarations: [decl] });
const mod2 = makeModuleExport("mod-b", {
declarations: [{ ...decl, version: "2.0.0" }],
});
expect(() => buildDesiredState([mod1, mod2], { profileId: "test" })).toThrow(/COMPOSITION-04/);
});
});
describe("RFC-1038: validateDeclarations (AC-20)", () => {
it("warns when .generate command lacks generates", () => {
const commands = new Map([
[
"test.generate",
{
name: "test.generate",
modulePath: "test",
description: "generate",
scope: "workspace",
flags: {},
execute: async () => {},
},
],
]);
const warnings: string[] = [];
const origWarn = console.warn;
console.warn = (msg: string) => warnings.push(msg);
try {
validateDeclarations(commands);
} finally {
console.warn = origWarn;
}
expect(warnings.some((w) => w.includes("test.generate"))).toBe(true);
});
it("warns when validator command lacks contract and rules", () => {
const commands = new Map([
[
"test.validate",
{
name: "test.validate",
modulePath: "test",
description: "validate",
scope: "workspace",
flags: {},
execute: async () => {},
},
],
]);
const warnings: string[] = [];
const origWarn = console.warn;
console.warn = (msg: string) => warnings.push(msg);
try {
validateDeclarations(commands);
} finally {
console.warn = origWarn;
}
expect(warnings.some((w) => w.includes("contract"))).toBe(true);
expect(warnings.some((w) => w.includes("rules"))).toBe(true);
});
it("passes when all declarations are valid", () => {
const commands = new Map([
[
"test.generate",
{
name: "test.generate",
mutatesState: true,
modulePath: "test",
description: "generate",
scope: "workspace",
flags: {},
generates: [{ path: "dist/test.js", phase: "build.post" }],
execute: async () => {},
},
],
]);
const warnings: string[] = [];
const origWarn = console.warn;
console.warn = (msg: string) => warnings.push(msg);
try {
validateDeclarations(commands);
} finally {
console.warn = origWarn;
}
// The test command itself should not produce a warning.
// (The exempt list check may warn about missing exempt commands — that's expected with a minimal map.)
expect(warnings.some((w) => w.includes("test.generate"))).toBe(false);
});
});
describe("RFC-1038: crash recovery (AC-13)", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "reconciler-crash-"));
resetReconciliationLock();
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it("loads persisted desired state, detects drift, and corrects it", async () => {
const declA = makeDeclaration("werkstatt/a");
const declB = makeDeclaration("werkstatt/b");
const desired = makeDesiredState([declA, declB]);
await persistDesiredState(desired, tempDir);
// Simulate crash: actual state has drift — extra component, missing component
const actual = makeActualState([declA]);
const extraDecl = makeDeclaration("werkstatt/extra");
actual.components.set("werkstatt/extra", { declaration: extraDecl, state: "active" });
const result = await reconcileFromPersisted(tempDir, [], actual, {
profileId: "test-profile",
});
// declB should be activated (was missing), werkstatt/extra should be deactivated (not in desired)
expect(result.applied).toBe(true);
expect(result.delta.toActivate.map((d) => d.componentId)).toContain("werkstatt/b");
expect(result.delta.toDeactivate).toContain("werkstatt/extra");
// Actual state should now match desired state
expect(actual.components.has("werkstatt/b")).toBe(true);
expect(actual.components.has("werkstatt/extra")).toBe(false);
});
it("builds desired state from module exports when no persisted state exists", async () => {
const decl = makeDeclaration("werkstatt/new");
const mod = makeModuleExport("test", { declarations: [decl] });
const actual = makeActualState();
const result = await reconcileFromPersisted(tempDir, [mod], actual, {
profileId: "test-profile",
});
expect(result.applied).toBe(true);
expect(actual.components.has("werkstatt/new")).toBe(true);
// Verify state was persisted
const persisted = await loadPersistedDesiredState(tempDir);
expect(persisted).not.toBeNull();
expect(persisted!.components.has("werkstatt/new")).toBe(true);
});
it("handles corrupt persisted file gracefully", async () => {
const { mkdir, writeFile } = await import("node:fs/promises");
await mkdir(join(tempDir, ".werkstatt"), { recursive: true });
await writeFile(join(tempDir, ".werkstatt", "desired-state.json"), "{ not valid json", "utf8");
const decl = makeDeclaration("werkstatt/fallback");
const mod = makeModuleExport("test", { declarations: [decl] });
const actual = makeActualState();
const result = await reconcileFromPersisted(tempDir, [mod], actual, {
profileId: "test-profile",
});
// Should fall back to building from exports, not crash
expect(result.applied).toBe(true);
expect(actual.components.has("werkstatt/fallback")).toBe(true);
});
});