import { test, expect } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runValidatorInventoryGenerate } from "../validator-inventory.ts";
import { buildActualState } from "../../runtime/reconciler.ts";
import type {
KernelCommandInput,
KernelRuntimeContext,
KernelCommandDefinition,
} from "@warpgogol/werkstatt-shared/kernel";
import type { ModuleExport } from "@warpgogol/werkstatt-shared/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
/*
RFC-0963: unit tests for validator.inventory.generate — fail-closed on
missing contract/rules, dry-run warnings, consolidation candidate detection,
and output shape verification.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- Fixtures stay inline and minimal so each case states its own setup.
- RFC-1311: Compass header sweep — declares the validator suite's contract for test-file scaffold coverage.
*/
// @ai-invariant fixtures must not couple to internals beyond the public surface under test
function makeCommand(
name: string,
overrides: Partial = {},
): KernelCommandDefinition {
return {
name,
description: `Test command ${name}`,
scope: "workspace",
flags: {},
execute: async () => ({ exitCode: 0, summary: "ok" }),
modulePath: "packages/werkstatt-engine/src/kernel/tests/validator-inventory.test.ts",
...overrides,
};
}
function makeContext(
commands: KernelCommandDefinition[],
workspaceRoot: string,
pipelines: [string, { command: string }[]][] = [],
): KernelRuntimeContext {
const mod: ModuleExport = {
name: "test-module",
version: "1.0.0",
declarations: [],
commands,
pipelines: pipelines.map(([name, steps]) => ({ name, steps })),
};
const actualState = buildActualState([mod]);
return {
io: createDefaultIO().io,
workspaceRoot,
actualState,
dryRun: false,
} as unknown as KernelRuntimeContext;
}
const baseInput: KernelCommandInput = {
argv: [],
flags: {},
};
test("fail-closed when a validator is missing contract", async () => {
const dir = await mkdtemp(join(tmpdir(), "vi-test-"));
try {
const commands = [
makeCommand("content.links.validate", { contract: "content", rules: ["LINK-01"] }),
makeCommand("seo.meta.validate"),
];
const context = makeContext(commands, dir);
const result = await runValidatorInventoryGenerate(baseInput, context);
expect(result.exitCode).toBe(1);
expect(result.summary).toContain("FAIL-CLOSED");
expect(result.summary).toContain("seo.meta.validate");
expect(result.data!.untaggedCount).toBe(1);
} finally {
await rm(dir, { recursive: true });
}
});
test("fail-closed when a validator is missing rules", async () => {
const dir = await mkdtemp(join(tmpdir(), "vi-test-"));
try {
const commands = [makeCommand("content.links.validate", { contract: "content" })];
const context = makeContext(commands, dir);
const result = await runValidatorInventoryGenerate(baseInput, context);
expect(result.exitCode).toBe(1);
expect(result.summary).toContain("FAIL-CLOSED");
expect(result.data!.untaggedCount).toBe(1);
} finally {
await rm(dir, { recursive: true });
}
});
test("dry-run emits warnings instead of failing", async () => {
const dir = await mkdtemp(join(tmpdir(), "vi-test-"));
try {
const commands = [
makeCommand("content.links.validate", { contract: "content", rules: ["LINK-01"] }),
makeCommand("seo.meta.validate"),
];
const context = makeContext(commands, dir);
const dryRunInput = { ...baseInput, flags: { "dry-run": true } };
const result = await runValidatorInventoryGenerate(dryRunInput, context);
expect(result.exitCode).toBe(0);
expect(result.summary).toContain("untagged");
expect(result.summary).toContain("dry-run");
expect(result.data!.untaggedCount).toBe(1);
expect(result.data!.written).toBe(false);
} finally {
await rm(dir, { recursive: true });
}
});
test("contracts with >1 validator appear in consolidationCandidates", async () => {
const dir = await mkdtemp(join(tmpdir(), "vi-test-"));
try {
const commands = [
makeCommand("canonical.url.validate", { contract: "canonical-url", rules: ["CANON-01"] }),
makeCommand("canonical.html-parity.validate", {
contract: "canonical-url",
rules: ["CANON-04"],
}),
makeCommand("mirroring.validate", { contract: "mirroring", rules: ["MIRROR-MISSING"] }),
];
const context = makeContext(commands, dir);
const result = await runValidatorInventoryGenerate(baseInput, context);
expect(result.exitCode).toBe(0);
expect(result.data!.consolidationCandidateCount).toBe(1);
expect(result.data!.contractCount).toBe(2);
expect(result.data!.validatorCount).toBe(3);
} finally {
await rm(dir, { recursive: true });
}
});
test("non-validator commands are excluded from inventory", async () => {
const dir = await mkdtemp(join(tmpdir(), "vi-test-"));
try {
const commands = [
makeCommand("content.links.validate", { contract: "content", rules: ["LINK-01"] }),
makeCommand("build.prepare", { contract: "build", rules: [] }),
makeCommand("config.regenerate"),
];
const context = makeContext(commands, dir);
const result = await runValidatorInventoryGenerate(baseInput, context);
expect(result.exitCode).toBe(0);
expect(result.data!.validatorCount).toBe(1);
} finally {
await rm(dir, { recursive: true });
}
});
test("pipeline phase is derived from registry pipelines", async () => {
const dir = await mkdtemp(join(tmpdir(), "vi-test-"));
try {
const commands = [
makeCommand("content.links.validate", { contract: "content", rules: ["LINK-01"] }),
];
const pipelines: [string, { command: string }[]][] = [
["sites-check.author", [{ command: "content.links.validate" }]],
];
const context = makeContext(commands, dir, pipelines);
const result = await runValidatorInventoryGenerate(baseInput, context);
expect(result.exitCode).toBe(0);
expect(result.data!.contractCount).toBe(1);
} finally {
await rm(dir, { recursive: true });
}
});