import { test, expect, describe } from "bun:test"; import { createRegistry } from "../../src/level-blocks/registry.ts"; import type { PipelineGraph } from "../../src/level-blocks/graph.ts"; import { compileGraph, GraphValidationError, type CompiledFamilies, type CompileTarget, } from "../../src/level-blocks/compile.ts"; import { makeDefs } from "./pipelineFixtures.ts"; const registry = createRegistry(makeDefs()); function makeGraph(): PipelineGraph { return { formatVersion: 1, blocks: [ { instanceId: "c1", code: "ToyCap", params: { resource: "crane", max: 2 }, provenance: "curated", shadow: false, }, { instanceId: "c2", code: "ToyCap", params: { resource: "forklift", max: 4 }, provenance: "curated", shadow: false, }, { instanceId: "s1", code: "ToyWeight", params: { weight: 0.5 }, provenance: "curated", shadow: false, }, { instanceId: "g1", code: "ToyGreedy", params: { seed: 7 }, provenance: "curated", shadow: false, }, { instanceId: "p1", code: "ToyProbe", params: { label: "trace" }, provenance: "curated", shadow: false, }, ], }; } /** Toy target proving the interface: echoes the grouped families + graph. */ const echoTarget: CompileTarget<{ families: CompiledFamilies; graph: PipelineGraph; }> = { fromFamilies: (families, graph) => ({ families, graph }), }; describe("compileGraph — family grouping", () => { test("groups outputs by family and routes them to the target", () => { const { families, graph } = compileGraph(registry, makeGraph(), echoTarget); expect(families.constraints.map((c) => c.instance.instanceId)).toEqual([ "c1", "c2", ]); expect(families.scorers.map((s) => s.instance.instanceId)).toEqual(["s1"]); expect(families.search.instance.instanceId).toBe("g1"); expect(families.collectors.map((c) => c.instance.instanceId)).toEqual([ "p1", ]); expect(graph.blocks).toHaveLength(5); }); test("each compiled entry carries def, instance, and apply() output", () => { const { families } = compileGraph(registry, makeGraph(), echoTarget); expect(families.constraints[0]!.def.code).toBe("ToyCap"); expect(families.constraints[0]!.output).toEqual({ tag: "cap", resource: "crane", max: 2, }); expect(families.constraints[1]!.output).toEqual({ tag: "cap", resource: "forklift", max: 4, }); expect(families.scorers[0]!.output).toEqual({ tag: "weight", weight: 0.5 }); expect(families.search.output).toEqual({ tag: "greedy", seed: 7 }); expect(families.collectors[0]!.output).toEqual({ tag: "probe", label: "trace", }); }); test("grouping is independent of block order in the graph", () => { const graph = makeGraph(); graph.blocks.reverse(); const { families } = compileGraph(registry, graph, echoTarget); expect(families.constraints.map((c) => c.instance.instanceId)).toEqual([ "c2", "c1", ]); expect(families.search.instance.instanceId).toBe("g1"); expect(families.scorers).toHaveLength(1); expect(families.collectors).toHaveLength(1); }); test("returns the target's value", () => { const countTarget: CompileTarget = { fromFamilies: (families) => families.constraints.length + families.scorers.length + families.collectors.length + 1, }; expect(compileGraph(registry, makeGraph(), countTarget)).toBe(5); }); }); describe("compileGraph — validation", () => { test("throws GraphValidationError on an invalid graph", () => { const graph = makeGraph(); graph.blocks[0]!.code = "Mystery"; expect(() => compileGraph(registry, graph, echoTarget)).toThrow( GraphValidationError, ); }); test("the thrown error carries the issues", () => { const graph = makeGraph(); graph.blocks[0]!.code = "Mystery"; try { compileGraph(registry, graph, echoTarget); throw new Error("expected compileGraph to throw"); } catch (err) { expect(err).toBeInstanceOf(GraphValidationError); const issues = (err as GraphValidationError).issues; expect(issues.some((i) => i.code === "Mystery")).toBe(true); } }); });