import { readFileSync } from "node:fs"; import { test, expect, describe } from "bun:test"; import { z } from "zod"; import lexicographicRaw from "./fixtures/lexicographic.config.json"; import minimalRaw from "./fixtures/minimal.config.json"; import variadicRaw from "./fixtures/variadic-nested-disabled.config.json"; import { parseConf, type ConfIssue } from "../../src/conf/index.ts"; import { runConfigSchema, type RunConfig, } from "../../src/level-blocks/runConfig.ts"; import { standardRegistry } from "../../src/level-blocks/standard.ts"; // Imported JSON widens literal types (formatVersion → number); read the // goldens back through the schema so they compare as RunConfig. const lexicographicJson: RunConfig = runConfigSchema.parse(lexicographicRaw); const variadicJson: RunConfig = runConfigSchema.parse(variadicRaw); const minimalJson: RunConfig = runConfigSchema.parse(minimalRaw); const registry = standardRegistry(); function fixture(name: string): string { return readFileSync(`${import.meta.dir}/fixtures/${name}.conf`, "utf8"); } function parseOk(name: string) { const result = parseConf(fixture(name), { registry }); if (!result.ok) throw new Error( `${name} unexpectedly failed:\n${JSON.stringify(result.issues, null, 2)}`, ); return result.config; } function firstIssue(text: string): ConfIssue { const result = parseConf(text, { registry }); expect(result.ok).toBe(false); if (result.ok) throw new Error("expected parse to fail"); expect(result.issues.length).toBe(1); return result.issues[0]!; } describe("parseConf — grammar goldens", () => { test("lexicographic fixture parses to its expected raw config", () => { expect(parseOk("lexicographic")).toEqual(lexicographicJson); }); test("variadic/nested/disabled fixture parses to its expected raw config", () => { expect(parseOk("variadic-nested-disabled")).toEqual(variadicJson); }); test("minimal fixture parses to its expected raw config", () => { expect(parseOk("minimal")).toEqual(minimalJson); }); test("every golden config is a valid RunConfig", () => { for (const raw of [lexicographicRaw, variadicRaw, minimalRaw]) { expect(() => runConfigSchema.parse(raw)).not.toThrow(); } }); }); describe("parseConf — grammar features", () => { test("sweep axes preserve broadcast, zip, and $enabled targets", () => { const result = parseConf( `[levelset]\nformat = 1\n\n[blocks.cap-a]\ncode = "levelset.MaxConcurrentResource.v1"\nresourceUniqueId = 1\nmax = 2\n\n[blocks.cap-b]\ncode = "levelset.MaxConcurrentResource.v1"\nresourceUniqueId = 2\nmax = 2\n\n[blocks.search]\ncode = "levelset.SerialSGS.v1"\n\n[sweep]\nmaxCombinations = 8\n\n[[sweep.axes]]\nid = "caps"\n\n[[sweep.axes.targets]]\ninstanceId = "cap-a"\nparamPath = "max"\nvalueKey = "A"\n\n[[sweep.axes.targets]]\ninstanceId = "cap-b"\nparamPath = "max"\nvalueKey = "B"\n\n[[sweep.axes.values]]\nA = 2\nB = 3\n\n[[sweep.axes.values]]\nA = 3\nB = 4\n\n[[sweep.axes]]\nid = "enabled"\nvalues = [true, false]\n\n[[sweep.axes.targets]]\ninstanceId = "cap-a"\nparamPath = "$enabled"\n`, ); expect(result.ok).toBe(true); if (!result.ok) throw new Error(JSON.stringify(result.issues)); expect(result.config.sweep).toEqual({ maxCombinations: 8, axes: [ { id: "caps", targets: [ { instanceId: "cap-a", paramPath: "max", valueKey: "A" }, { instanceId: "cap-b", paramPath: "max", valueKey: "B" }, ], values: [ { A: 2, B: 3 }, { A: 3, B: 4 }, ], }, { id: "enabled", targets: [{ instanceId: "cap-a", paramPath: "$enabled" }], values: [true, false], }, ], }); }); test("block document order is preserved", () => { const config = parseOk("lexicographic"); expect(config.pipeline.blocks.map((b) => b.instanceId)).toEqual([ "deadlineA", "lateness", "makespan", "sgs", ]); }); test('the "report" weight sentinel maps to null; a number stays a number', () => { const config = parseOk("lexicographic"); expect(config.weights).toEqual({ makespan: null, lateness: 1 }); }); test("array-of-tables becomes an object array param", () => { const config = parseOk("lexicographic"); const lateness = config.pipeline.blocks.find( (b) => b.instanceId === "lateness", )!; expect(lateness.params).toEqual({ phases: [ { name: "phase-1", taskUniqueIds: [1, 2, 3], targetDay: 80, weight: 2 }, ], }); }); test("dotted keys nest into an object param", () => { const config = parseOk("variadic-nested-disabled"); const peak = config.pipeline.blocks.find((b) => b.instanceId === "peak")!; expect(peak.params).toEqual({ resourceUniqueId: 3, cap: 5, window: { fromDay: 10, toDay: 20 }, }); }); test("a quoted table key is the verbatim instanceId", () => { const config = parseOk("variadic-nested-disabled"); expect( config.pipeline.blocks.some((b) => b.instanceId === "units.open"), ).toBe(true); }); test("reserved flags: enabled=false, candidate provenance, shadow=true", () => { const config = parseOk("variadic-nested-disabled"); const wip = config.pipeline.blocks.find((b) => b.instanceId === "wip")!; expect(wip.enabled).toBe(false); const open = config.pipeline.blocks.find( (b) => b.instanceId === "units.open", )!; expect(open.provenance).toBe("candidate"); expect(open.shadow).toBe(true); }); test("format maps to both runConfig and pipeline formatVersion; version is stamped back", () => { const config = parseOk("minimal"); expect(config.formatVersion).toBe(1); expect(config.pipeline.formatVersion).toBe(1); expect(config.levelset).toBe("0.1.0"); }); test("parsing without a registry skips graph validation (raw config)", () => { // No search block — validateGraph would reject, but registry-less parse // returns the raw config for the caller to fill/validate. const result = parseConf( `[levelset]\nformat = 1\n[blocks.m]\ncode = "levelset.Makespan.v1"`, ); expect(result.ok).toBe(true); }); }); describe("parseConf — error fixtures (one per code)", () => { test("E301 TOML syntax carries line and column", () => { const issue = firstIssue(`[levelset]\nformat = = 1`); expect(issue.code).toBe("LEVELSET_E301"); expect(issue.pos).toEqual({ line: 2, column: 10 }); }); test("E302 unknown top-level section", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[nonsense]\nx = 1\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E302"); expect(issue.path).toBe("nonsense"); }); test("E302 config shape error carries a conf-oriented path", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[resolve]\nhorizonDays = "ninety"\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E302"); expect(issue.path).toBe("resolve.horizonDays"); }); test("E303 missing format", () => { const issue = firstIssue(`[blocks.s]\ncode = "levelset.SerialSGS.v1"`); expect(issue.code).toBe("LEVELSET_E303"); expect(issue.path).toBe("levelset.format"); }); test("E303 unsupported format", () => { const issue = firstIssue( `[levelset]\nformat = 2\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E303"); }); test("E304 rejects a bare TOML datetime literal", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[resolve]\nepoch = 2026-01-01\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E304"); expect(issue.path).toBe("resolve.epoch"); }); test("E305 a reserved-named param collision", () => { // A synthetic block whose schema declares a `shadow` param — unreachable // through the conf format. const collidingRegistry = standardRegistry([ { code: "test.Colliding.v1", family: "constraint", tier: "engineer", block: { id: "Colliding", schema: { input: z.object({ shadow: z.number() }), output: z.unknown(), }, apply: (i: unknown) => i, toMiniZinc: () => ({ text: "" }), doc: { nl: "", pseudocode: "" }, }, defaultShadow: { shadow: 1 }, }, ]); const result = parseConf( `[levelset]\nformat = 1\n[blocks.x]\ncode = "test.Colliding.v1"\nshadow = 3\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, { registry: collidingRegistry }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("expected failure"); const e305 = result.issues.find((i) => i.code === "LEVELSET_E305")!; expect(e305).toBeDefined(); expect(e305.instanceId).toBe("x"); }); test("E306 rejects a non-finite number", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[resolve]\nhorizonDays = inf\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E306"); }); test("E308 unknown block code, attributed to its instanceId", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[blocks.mystery]\ncode = "levelset.Nope.v1"\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E308"); expect(issue.instanceId).toBe("mystery"); }); test("E308 invalid sweep target is attributed to its instanceId", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[blocks.s]\ncode = "levelset.SerialSGS.v1"\n[sweep]\n[[sweep.axes]]\nid = "missing-cap"\nvalues = [1, 2]\n[[sweep.axes.targets]]\ninstanceId = "missing"\nparamPath = "max"`, ); expect(issue.code).toBe("LEVELSET_E308"); expect(issue.instanceId).toBe("missing"); expect(issue.message).toContain('sweep axis "missing-cap"'); }); test("E308 zero searches", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[blocks.m]\ncode = "levelset.Makespan.v1"`, ); expect(issue.code).toBe("LEVELSET_E308"); expect(issue.message).toMatch(/exactly one search/); }); test("E308 invalid params, attributed to its instanceId", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[blocks.cap]\ncode = "levelset.MaxConcurrentResource.v1"\nresourceUniqueId = 1\nmax = -3\n[blocks.s]\ncode = "levelset.SerialSGS.v1"`, ); expect(issue.code).toBe("LEVELSET_E308"); expect(issue.instanceId).toBe("cap"); }); test("E308 a weight referencing a non-scorer instance", () => { const issue = firstIssue( `[levelset]\nformat = 1\n[blocks.s]\ncode = "levelset.SerialSGS.v1"\n[weights]\nghost = 2`, ); expect(issue.code).toBe("LEVELSET_E308"); expect(issue.instanceId).toBe("ghost"); }); });