import { describe, expect, it } from "vitest"; import { normalizeControlsPanelLayout } from "./controls-panel-normalization"; import { resolveToolcraftControlApplicability } from "./control-applicability"; import { defineToolcraft } from "./define-toolcraft"; const opacityControl = { applicability: { mode: "always" as const }, defaultValue: 75, max: 100, min: 0, target: "appearance.opacity", type: "slider", } as const; describe("Toolcraft controls panel normalization idempotency", () => { it("preserves explicit applicability across repeated resolution", () => { const resolved = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { explicit: { applicability: { mode: "always" as const }, target: "appearance.explicit", type: "switch", }, secondary: { applicability: { mode: "always" as const }, target: "appearance.secondary", type: "switch", }, conditional: { target: "appearance.conditional", type: "slider", applicability: { all: [ { equals: true, target: "appearance.explicit", }, ], mode: "conditional" as const, }, }, }, id: "appearance", title: "Appearance", }, ], title: "Controls", }, }, }, modules: [], }); const readApplicability = (schema: typeof resolved) => Object.fromEntries( schema.panels.controls?.sections .filter((section) => section.id.startsWith("appearance")) .flatMap((section) => Object.entries(section.controls)) .map(([id, control]) => [id, control.applicability]) ?? [], ); expect(readApplicability(resolved)).toEqual({ explicit: { mode: "always", origin: "explicit" }, secondary: { mode: "always", origin: "explicit" }, conditional: { all: [{ equals: true, target: "appearance.explicit" }], mode: "conditional", origin: "explicit", }, }); expect( Object.fromEntries( Object.entries(readApplicability(resolved)).map( ([id, applicability]) => [ id, resolveToolcraftControlApplicability({ applicability }), ], ), ), ).toEqual(readApplicability(resolved)); }); it("keeps resolved product and runtime section ids idempotent", () => { const resolved = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { prompt: { applicability: { mode: "always" as const }, target: "generation.prompt", type: "text", }, export: { applicability: { mode: "always" as const }, actions: [{ label: "Export PNG", value: "export" }], target: "output.export", type: "panelActions", }, }, title: "Generation", }, ], title: "Controls", }, }, }, modules: [], }); const roundTripped = normalizeControlsPanelLayout( resolved.panels.controls!, ); const resolvedIds = resolved.panels.controls?.sections.map( (section) => section.id, ); const roundTrippedIds = roundTripped.sections.map((section) => section.id); expect(roundTrippedIds).toEqual(resolvedIds); expect( roundTrippedIds?.filter((id) => id === "runtime.setup"), ).toHaveLength(1); expect( roundTrippedIds?.filter((id) => id === "runtime.export"), ).toHaveLength(1); }); it("derives deterministic child ids when mixed layout splits one authored section", () => { const createPanel = () => normalizeControlsPanelLayout({ sections: [ { controls: { opacity: opacityControl, palette: { applicability: { mode: "always" as const }, target: "appearance.palette", type: "palette", }, }, id: "appearance", title: "Appearance", }, ], title: "Controls", }); const firstIds = createPanel().sections.map((section) => section.id); const secondIds = createPanel().sections.map((section) => section.id); expect(firstIds).toHaveLength(2); expect(new Set(firstIds).size).toBe(2); expect(firstIds).toEqual(secondIds); expect(firstIds.every((id) => id.startsWith("appearance.part-"))).toBe( true, ); }); it("keeps deterministic mixed-layout child ids idempotent", () => { const resolved = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-2", controls: { opacity: opacityControl, palette: { applicability: { mode: "always" as const }, target: "appearance.palette", type: "palette", }, }, title: "Appearance", }, ], title: "Controls", }, }, }, modules: [], }); const productIds = resolved.panels.controls?.sections .map((section) => section.id) .filter((id) => !id.startsWith("runtime.")); const roundTripped = normalizeControlsPanelLayout( resolved.panels.controls!, ); const roundTrippedProductIds = roundTripped.sections .map((section) => section.id) .filter((id) => !id.startsWith("runtime.")); expect(productIds).toHaveLength(2); expect( productIds?.every((id) => id.startsWith("test-section-2.part-")), ).toBe(true); expect(roundTrippedProductIds).toEqual(productIds); }); it("keeps action-extracted mixed-layout child ids idempotent", () => { const resolved = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { controls: { opacity: opacityControl, palette: { applicability: { mode: "always" as const }, target: "appearance.palette", type: "palette", }, export: { applicability: { mode: "always" as const }, actions: [{ label: "Export PNG", value: "export" }], target: "output.export", type: "panelActions", }, }, id: "appearance", title: "Appearance", }, ], title: "Controls", }, }, }, modules: [], }); const productIds = resolved.panels.controls?.sections .map((section) => section.id) .filter((id) => !id.startsWith("runtime.")); expect(productIds).toHaveLength(2); expect( productIds?.every((id) => (id.match(/\.part-/gu) ?? []).length === 2), ).toBe(true); const roundTripped = normalizeControlsPanelLayout( resolved.panels.controls!, ); const roundTrippedProductIds = roundTripped.sections .map((section) => section.id) .filter((id) => !id.startsWith("runtime.")); expect(roundTrippedProductIds).toEqual(productIds); }); });