import { spawnSync } from "node:child_process"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { validateKovaWorkflowEvidence } from "../../scripts/lib/kova-workflow-evidence.mjs"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; type JsonObject = Record; type Pair = { scenario: string; state: string; }; const PROFILE = "release"; const TARGET = "local-build:/work/openclaw"; const MODEL = "gpt-5.6"; const INCLUDE_FILTERS = ["scenario:scenario-a", "scenario:scenario-b"]; const FIRST_PAIR: Pair = { scenario: "scenario-a", state: "state-a" }; const PAIRS: Pair[] = [FIRST_PAIR, { scenario: "scenario-b", state: "state-b" }]; const SCRIPT_PATH = "scripts/lib/kova-workflow-evidence.mjs"; const tempRoots = useAutoCleanupTempDirTracker(afterEach); function plan( pairs: Pair[] = PAIRS, repeat = 2, includeFilters: string[] = INCLUDE_FILTERS, ): JsonObject { return { schemaVersion: "kova.matrix.plan.v1", profile: { id: PROFILE }, target: TARGET, controls: { include: includeFilters, repeat, }, entries: pairs.map((pair) => ({ scenario: { id: pair.scenario }, state: { id: pair.state }, status: "SELECTED", })), }; } function record(pair: Pair, repeatIndex: number, repeat: number, authMode: "live" | "mock") { return { scenario: pair.scenario, state: { id: pair.state }, status: "PASS", repeat: { index: repeatIndex, total: repeat, }, auth: { mode: authMode, environmentDependent: authMode === "live", }, providerEvidence: authMode === "live" ? { authMode: "live", available: true, environmentDependent: true, requestCount: 1, models: [{ value: MODEL, count: 1 }], source: "openclaw-timeline", } : { authMode: "mock", available: true, environmentDependent: false, requestCount: 1, source: "mock-provider-log", }, }; } function report({ authMode = "mock", includeFilters = INCLUDE_FILTERS, pairs = PAIRS, repeat = 2, }: { authMode?: "live" | "mock"; includeFilters?: string[]; pairs?: Pair[]; repeat?: number; } = {}): JsonObject { return { schemaVersion: "kova.report.v1", mode: "execution", profile: { id: PROFILE }, target: TARGET, controls: { include: includeFilters, repeat, }, auth: { requestedMode: authMode, live: { environmentDependent: authMode === "live", }, }, records: pairs.flatMap((pair) => Array.from({ length: repeat }, (_, index) => record(pair, index + 1, repeat, authMode)), ), }; } function validate( lanePlan: JsonObject, laneReport: JsonObject, options: { authMode?: "live" | "mock"; expectedModel?: string; includeFilters?: string[]; repeat?: number; } = {}, ) { return validateKovaWorkflowEvidence({ plan: lanePlan, report: laneReport, profile: PROFILE, target: TARGET, repeat: options.repeat ?? 2, includeFilters: options.includeFilters ?? INCLUDE_FILTERS, authMode: options.authMode ?? "mock", expectedModel: options.expectedModel ?? MODEL, }); } function recordsOf(laneReport: JsonObject): JsonObject[] { return laneReport.records as JsonObject[]; } function firstRecordOf(laneReport: JsonObject): JsonObject { const first = recordsOf(laneReport)[0]; if (!first) { throw new Error("fixture report has no records"); } return first; } function runCli({ lanePlan = JSON.stringify(plan()), laneReport = JSON.stringify(report()), repeat = "2", includeAuth = true, }: { lanePlan?: string; laneReport?: string; repeat?: string; includeAuth?: boolean; } = {}) { const root = tempRoots.make("openclaw-kova-evidence-"); const planPath = join(root, "plan.json"); const reportPath = join(root, "report.json"); writeFileSync(planPath, lanePlan); writeFileSync(reportPath, laneReport); const args = [ SCRIPT_PATH, "--plan", planPath, "--report", reportPath, "--profile", PROFILE, "--target", TARGET, "--repeat", repeat, "--include", INCLUDE_FILTERS.join(","), "--model", MODEL, ]; if (includeAuth) { args.push("--auth", "mock"); } return spawnSync(process.execPath, args, { cwd: process.cwd(), encoding: "utf8", }); } describe("Kova workflow evidence", () => { it("accepts exact selected-pair coverage for every repeat", () => { expect(validate(plan(), report())).toEqual({ authMode: "mock", pairCount: 2, recordCount: 4, repeat: 2, }); }); it("rejects collapsed repeat coverage", () => { const laneReport = report(); recordsOf(laneReport).pop(); expect(() => validate(plan(), laneReport)).toThrow( "lane report collapsed scenario-b/state-b coverage to 1/2 repeats", ); }); it("rejects report pairs outside the selected plan", () => { const laneReport = report(); const firstRecord = firstRecordOf(laneReport); firstRecord.scenario = "scenario-extra"; expect(() => validate(plan(), laneReport)).toThrow( "lane report contained unexpected scenario-extra/state-a", ); }); it("rejects live records backed by mock-provider evidence", () => { const liveFilters = ["scenario:scenario-a"]; const lanePlan = plan([FIRST_PAIR], 1, liveFilters); const laneReport = report({ authMode: "live", includeFilters: liveFilters, pairs: [FIRST_PAIR], repeat: 1, }); const firstRecord = firstRecordOf(laneReport); (firstRecord.providerEvidence as JsonObject).source = "mock-provider-log"; expect(() => validate(lanePlan, laneReport, { authMode: "live", includeFilters: liveFilters, repeat: 1, }), ).toThrow("live record scenario-a/state-a provider source was mock-provider-log"); }); it("accepts live timeline evidence with observed provider requests", () => { const liveFilters = ["scenario:scenario-a"]; const lanePlan = plan([FIRST_PAIR], 1, liveFilters); const laneReport = report({ authMode: "live", includeFilters: liveFilters, pairs: [FIRST_PAIR], repeat: 1, }); expect( validate(lanePlan, laneReport, { authMode: "live", includeFilters: liveFilters, repeat: 1, }), ).toEqual({ authMode: "live", pairCount: 1, recordCount: 1, repeat: 1, }); }); it("rejects live evidence for a different provider model", () => { const liveFilters = ["scenario:scenario-a"]; const lanePlan = plan([FIRST_PAIR], 1, liveFilters); const laneReport = report({ authMode: "live", includeFilters: liveFilters, pairs: [FIRST_PAIR], repeat: 1, }); (firstRecordOf(laneReport).providerEvidence as JsonObject).models = [ { value: "gpt-5.5", count: 1 }, ]; expect(() => validate(lanePlan, laneReport, { authMode: "live", includeFilters: liveFilters, repeat: 1, }), ).toThrow("live record scenario-a/state-a provider model did not match gpt-5.6"); }); it("rejects mixed live provider models", () => { const liveFilters = ["scenario:scenario-a"]; const lanePlan = plan([FIRST_PAIR], 1, liveFilters); const laneReport = report({ authMode: "live", includeFilters: liveFilters, pairs: [FIRST_PAIR], repeat: 1, }); const providerEvidence = firstRecordOf(laneReport).providerEvidence as JsonObject; providerEvidence.requestCount = 2; providerEvidence.models = [ { value: MODEL, count: 1 }, { value: "gpt-5.5", count: 1 }, ]; expect(() => validate(lanePlan, laneReport, { authMode: "live", includeFilters: liveFilters, repeat: 1, }), ).toThrow("live record scenario-a/state-a provider model evidence was not exact"); }); it("rejects live provider model count drift", () => { const liveFilters = ["scenario:scenario-a"]; const lanePlan = plan([FIRST_PAIR], 1, liveFilters); const laneReport = report({ authMode: "live", includeFilters: liveFilters, pairs: [FIRST_PAIR], repeat: 1, }); (firstRecordOf(laneReport).providerEvidence as JsonObject).models = [ { value: MODEL, count: 2 }, ]; expect(() => validate(lanePlan, laneReport, { authMode: "live", includeFilters: liveFilters, repeat: 1, }), ).toThrow("live record scenario-a/state-a provider model count did not match request count"); }); it("rejects plan and report schema drift", () => { const badPlan = plan(); badPlan.schemaVersion = "kova.matrix.plan.v0"; expect(() => validate(badPlan, report())).toThrow("unexpected lane plan schema"); const badReport = report(); badReport.schemaVersion = "kova.report.v0"; expect(() => validate(plan(), badReport)).toThrow("unexpected lane report schema"); }); it("rejects plan and report include-filter drift", () => { const badPlan = plan(); (badPlan.controls as JsonObject).include = ["scenario:scenario-a"]; expect(() => validate(badPlan, report())).toThrow("lane plan include filters did not match"); const badReport = report(); (badReport.controls as JsonObject).include = ["scenario:scenario-a"]; expect(() => validate(plan(), badReport)).toThrow("lane report include filters did not match"); }); it("rejects requested and record auth drift", () => { const laneReport = report(); (laneReport.auth as JsonObject).requestedMode = "live"; expect(() => validate(plan(), laneReport)).toThrow("lane report requested auth did not match"); const recordAuthDrift = report(); (firstRecordOf(recordAuthDrift).auth as JsonObject).mode = "live"; expect(() => validate(plan(), recordAuthDrift)).toThrow( "record scenario-a/state-a auth mode did not match", ); }); it("rejects malformed record state", () => { const laneReport = report(); firstRecordOf(laneReport).state = "state-a"; expect(() => validate(plan(), laneReport)).toThrow("invalid lane report record 0 state"); }); it("rejects malformed JSON through the CLI", () => { const result = runCli({ lanePlan: "{not-json" }); expect(result.status).toBe(1); expect(result.stderr).toContain("--plan is not valid JSON"); }); it("rejects zero repeat and missing CLI arguments", () => { const zeroRepeat = runCli({ repeat: "0" }); expect(zeroRepeat.status).toBe(1); expect(zeroRepeat.stderr).toContain("invalid expected repeat"); const missingAuth = runCli({ includeAuth: false }); expect(missingAuth.status).toBe(1); expect(missingAuth.stderr).toContain("invalid --auth"); }); });