import { test, expect, describe } from "bun:test"; import { createRegistry } from "../../src/level-blocks/registry.ts"; import { pipelineGraphSchema, validateGraph, type PipelineGraph, } from "../../src/level-blocks/graph.ts"; import { makeDefs } from "./pipelineFixtures.ts"; const registry = createRegistry(makeDefs()); function makeGraph(): PipelineGraph { return { formatVersion: 1, systemCode: "OHT", blocks: [ { instanceId: "b1", code: "ToyCap", params: { resource: "crane", max: 2 }, provenance: "curated", shadow: true, }, { instanceId: "b2", code: "ToyWeight", params: { weight: 0.5 }, provenance: "candidate", shadow: false, }, { instanceId: "b3", code: "ToyGreedy", params: { seed: 7 }, provenance: "curated", shadow: false, }, ], }; } describe("pipelineGraphSchema", () => { test("applies provenance/shadow defaults", () => { const parsed = pipelineGraphSchema.parse({ formatVersion: 1, blocks: [{ instanceId: "b1", code: "ToyCap", params: {} }], }); expect(parsed.blocks[0]!.provenance).toBe("curated"); expect(parsed.blocks[0]!.shadow).toBe(false); }); test("rejects unknown formatVersion", () => { expect(() => pipelineGraphSchema.parse({ formatVersion: 2, blocks: [] }), ).toThrow(); }); }); describe("validateGraph — round trip", () => { test("build → stringify → parse → validate → deep-equal", () => { const graph = makeGraph(); const revived: unknown = JSON.parse(JSON.stringify(graph)); const result = validateGraph(registry, revived); expect(result.ok).toBe(true); if (result.ok) expect(result.graph).toEqual(graph); }); test("accepts a graph without optional systemCode", () => { const graph = makeGraph(); delete graph.systemCode; const result = validateGraph(registry, JSON.parse(JSON.stringify(graph))); expect(result.ok).toBe(true); }); }); describe("validateGraph — rejections", () => { test("rejects non-graph values with shape issues", () => { const result = validateGraph(registry, { nope: true }); expect(result.ok).toBe(false); if (!result.ok) expect(result.issues.length).toBeGreaterThan(0); }); test("rejects unknown block codes", () => { const graph = makeGraph(); graph.blocks[0]!.code = "Mystery"; const result = validateGraph(registry, graph); expect(result.ok).toBe(false); if (!result.ok) { expect(result.issues).toContainEqual({ instanceId: "b1", code: "Mystery", message: "Unknown block code: Mystery", }); } }); test("rejects params that fail the block input schema", () => { const graph = makeGraph(); graph.blocks[0]!.params = { resource: "crane", max: -1 }; const result = validateGraph(registry, graph); expect(result.ok).toBe(false); if (!result.ok) { expect( result.issues.some( (i) => i.instanceId === "b1" && i.message.includes("Invalid params"), ), ).toBe(true); } }); test("rejects a graph with zero search blocks", () => { const graph = makeGraph(); graph.blocks = graph.blocks.filter((b) => b.code !== "ToyGreedy"); const result = validateGraph(registry, graph); expect(result.ok).toBe(false); if (!result.ok) { expect( result.issues.some((i) => i.message.includes("exactly one search")), ).toBe(true); } }); test("rejects a graph with two search blocks", () => { const graph = makeGraph(); graph.blocks.push({ instanceId: "b4", code: "ToyGreedy", params: { seed: 8 }, provenance: "curated", shadow: false, }); const result = validateGraph(registry, graph); expect(result.ok).toBe(false); if (!result.ok) { expect(result.issues.some((i) => i.message.includes("found 2"))).toBe( true, ); } }); test("rejects duplicate instanceIds", () => { const graph = makeGraph(); graph.blocks[1]!.instanceId = "b1"; const result = validateGraph(registry, graph); expect(result.ok).toBe(false); if (!result.ok) { expect( result.issues.some((i) => i.message.includes("Duplicate instanceId: b1"), ), ).toBe(true); } }); test("collects multiple issues in one pass", () => { const graph = makeGraph(); graph.blocks[0]!.code = "Mystery"; graph.blocks[1]!.params = { weight: -1 }; graph.blocks = graph.blocks.filter((b) => b.code !== "ToyGreedy"); const result = validateGraph(registry, graph); expect(result.ok).toBe(false); if (!result.ok) expect(result.issues.length).toBeGreaterThanOrEqual(3); }); });