import { test, expect, describe } from "bun:test"; import { z } from "zod"; import { deriveBlockMeta, dumpBlockMeta, ISO_DATE_PATTERN, type BlockFieldSpec, } from "../../src/level-blocks/meta.ts"; import type { BlockDef } from "../../src/level-blocks/registry.ts"; import { standardBlockDefs, standardRegistry, STANDARD_CODES, } from "../../src/level-blocks/standard.ts"; import { DEFAULT_MAX_COMBINATIONS, HARD_MAX_COMBINATIONS, SWEEP_ENABLED_TARGET, } from "../../src/level-blocks/sweep.ts"; const defsByCode = new Map(standardBlockDefs().map((d) => [d.code, d])); function meta(code: string) { return deriveBlockMeta(defsByCode.get(code)!); } function field( fields: BlockFieldSpec[], path: string, ): BlockFieldSpec | undefined { return fields.find((f) => f.path === path); } describe("deriveBlockMeta — scalar fields", () => { test("integer field carries bounds, integer flag, and shadow default", () => { const m = meta(STANDARD_CODES.maxConcurrentResource); const max = field(m.fields, "max")!; expect(max.kind).toBe("number"); expect(max.integer).toBe(true); expect(max.min).toBe(1); // exclusiveMinimum 0 → inclusive 1 for integers expect(max.max).toBeUndefined(); // INT_SENTINEL stripped expect(max.defaultValue).toBe(1); // from defaultShadow expect(max.label).toBe("max"); }); test("number field with a schema default surfaces that default", () => { const m = meta(STANDARD_CODES.concurrentResourceCost); const threshold = field(m.fields, "threshold")!; expect(threshold.kind).toBe("number"); // defaultShadow omits threshold, so the schema default (0) fills in. expect(threshold.defaultValue).toBe(0); const growth = field(m.fields, "growthBase")!; expect(growth.min).toBe(1); // exclusiveMinimum 1, non-integer → stays 1 expect(growth.integer).toBe(false); }); test("boolean field becomes a checkbox", () => { const m = meta(STANDARD_CODES.unimodalDeviation); const flag = field(m.fields, "allowSecondPeak")!; expect(flag.kind).toBe("checkbox"); expect(flag.defaultValue).toBe(false); }); test("enum field becomes a dropdown with string options", () => { const m = meta(STANDARD_CODES.gate); const fallback = field(m.fields, "fallback")!; expect(fallback.kind).toBe("dropdown"); expect(fallback.options).toEqual(["leastBad", "none"]); }); }); describe("deriveBlockMeta — variadic scalar arrays", () => { test("integer array → number variadic (ConcurrentUnitsLimit.unitIds)", () => { const m = meta(STANDARD_CODES.concurrentUnitsLimit); const v = m.variadic.find((p) => p.path === "unitIds")!; expect(v.itemKind).toBe("number"); expect(v.integer).toBe(true); expect(v.minItems).toBe(0); expect(v.emptySeed).toBe(0); expect(v.defaultValues).toEqual([]); // unitIds is a variadic, not a complex param. expect(m.complexParams).not.toContain("unitIds"); }); test("string array → text variadic (KBest.by)", () => { const m = meta(STANDARD_CODES.kBest); const v = m.variadic.find((p) => p.path === "by")!; expect(v.itemKind).toBe("text"); expect(v.integer).toBe(false); expect(v.minItems).toBe(1); expect(v.emptySeed).toBe(""); expect(v.defaultValues).toEqual(["makespan"]); }); }); describe("deriveBlockMeta — complex (object-array) params", () => { test("ModeSelection.modes is complex, taskUniqueId is a field", () => { const m = meta(STANDARD_CODES.modeSelection); expect(m.complexParams).toContain("modes"); expect(field(m.fields, "taskUniqueId")).toBeDefined(); // No nested field is emitted for the object-array's inner scalars. expect(m.fields.some((f) => f.path.startsWith("modes."))).toBe(false); }); test("PhaseLateness.phases and Gate.limits are complex", () => { expect(meta(STANDARD_CODES.phaseLateness).complexParams).toContain( "phases", ); expect(meta(STANDARD_CODES.gate).complexParams).toContain("limits"); }); }); describe("deriveBlockMeta — nested objects flatten to dotted fields", () => { test("PeakCap.window flattens to window.fromDay / window.toDay", () => { const m = meta(STANDARD_CODES.peakCap); expect(field(m.fields, "window.fromDay")).toBeDefined(); expect(field(m.fields, "window.toDay")).toBeDefined(); // The container object itself is neither a field nor complex. expect(m.fields.some((f) => f.path === "window")).toBe(false); expect(m.complexParams).not.toContain("window"); }); }); describe("deriveBlockMeta — empty-schema blocks", () => { test("Makespan and SerialSGS have no fields, variadic, or complex params", () => { for (const code of [STANDARD_CODES.makespan, STANDARD_CODES.serialSGS]) { const m = meta(code); expect(m.fields).toEqual([]); expect(m.variadic).toEqual([]); expect(m.complexParams).toEqual([]); } }); }); describe("deriveBlockMeta — registry facts", () => { test("carries family, tier, doc, defaultShadow, and inputSchema", () => { const m = meta(STANDARD_CODES.makespan); expect(m.family).toBe("scorer"); expect(m.tier).toBe("sales"); expect(m.doc.nl.length).toBeGreaterThan(0); expect(m.defaultShadow).toEqual({}); expect((m.inputSchema as { type?: string }).type).toBe("object"); }); test("assumptionTemplate is included when present, omitted otherwise", () => { expect( meta(STANDARD_CODES.maxConcurrentResource).assumptionTemplate, ).toContain("concurrent"); expect(meta(STANDARD_CODES.makespan).assumptionTemplate).toBeUndefined(); }); }); describe("deriveBlockMeta — panelBinding-driven kinds (date, slider)", () => { // The standard registry declares no panelBindings and no date params, so a // synthetic def exercises the slider and date branches. const schema = z.object({ start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), level: z.number().min(0).max(10), }); const def: BlockDef = { code: "Synthetic", family: "constraint", tier: "engineer", block: { id: "Synthetic", schema: { input: schema, output: z.unknown() }, apply: (i: unknown) => i, toMiniZinc: () => ({ text: "" }), doc: { nl: "synthetic", pseudocode: "" }, }, defaultShadow: { start: "2026-01-01", level: 5 }, panelBindings: [ { paramPath: "level", label: "Aggressiveness", control: "slider", options: { min: 1, max: 8, step: 0.5 }, }, ], }; test("date pattern → date field", () => { const m = deriveBlockMeta(def); const start = field(m.fields, "start")!; expect(start.kind).toBe("date"); expect(start.defaultValue).toBe("2026-01-01"); }); test("slider binding → slider field with binding bounds and label", () => { const m = deriveBlockMeta(def); const level = field(m.fields, "level")!; expect(level.kind).toBe("slider"); expect(level.label).toBe("Aggressiveness"); expect(level.slider).toEqual({ min: 1, max: 8, step: 0.5 }); expect(level.defaultValue).toBe(5); }); test("ISO_DATE_PATTERN matches the block-author date convention", () => { expect(new RegExp(ISO_DATE_PATTERN).test("2026-07-16")).toBe(true); expect(new RegExp(ISO_DATE_PATTERN).test("not-a-date")).toBe(false); }); }); describe("dumpBlockMeta", () => { test("dumps every registered block with the composition grammar", () => { const dump = dumpBlockMeta(standardRegistry()); expect(dump.formatVersion).toBe(1); expect(dump.blocks.length).toBe(standardBlockDefs().length); expect(dump.composition.order).toEqual([ "constraint", "scorer", "search", "collector", ]); expect(dump.composition.cardinality.search).toEqual({ min: 1, max: 1 }); expect(dump.levelsetVersion.length).toBeGreaterThan(0); }); test("derives canvas authoring contracts from the instance and sweep schemas", () => { const dump = dumpBlockMeta(standardRegistry()); expect(dump.authoring.instance.reservedParamNames).toEqual([ "code", "provenance", "shadow", "enabled", ]); expect(dump.authoring.instance.enabledWhenOmitted).toBe(true); expect(dump.authoring.instance.schema).toMatchObject({ type: "object", required: ["instanceId", "code", "params"], }); expect(dump.authoring.sweep).toMatchObject({ enabledTarget: SWEEP_ENABLED_TARGET, defaultMaxCombinations: DEFAULT_MAX_COMBINATIONS, hardMaxCombinations: HARD_MAX_COMBINATIONS, schema: { type: "object", required: ["axes"] }, }); expect(dump.authoring.objective.lexicographicTierSchema).toMatchObject({ anyOf: [ { type: "string" }, { type: "object", required: ["weightedSum"], properties: { weightedSum: { type: "object" } }, }, ], }); }); });