import { describe, expect, it } from "vitest"; import { createProject } from "../src/domain/project"; import type { PhyFile, KeyNode, VariantGroupNode } from "../src/domain/phy"; import { generateMockTarget } from "../src/domain/generator"; import { validateProject } from "../src/domain/validate"; import { assignKey, type MapFile } from "../src/domain/map"; import { setKeyWiring, setProvider, type ContactWiring, type ElcFile } from "../src/domain/elc"; import { listDeleteReferences, deleteNodeCascade } from "../src/domain/deleteCascade"; import { loadProject, saveProject } from "../src/io/projectFile"; import { makePhyReference } from "../src/domain/phyReference"; import { ProjectHistory } from "../src/domain/history"; const emptyPhy = (): PhyFile => ({ version: "0.1.0", metadata: {}, profiles: [{ id: "d", name: "D", unitSize: { x: 1, y: 1 } }], root: [], }); const key = (id: string, decal = false): KeyNode => ({ type: "key", id, profile: "d", transform: { x: 0, y: 0, rotation: 0 }, size: { w: 1, h: 1 }, ...(decal ? { decal: true } : {}), }); const variantGroup = ( id: string, defaultId: string, variants: Array<[string, string, (KeyNode | VariantGroupNode)[]]>, ): VariantGroupNode => ({ type: "variantGroup", id, name: id, transform: { x: 0, y: 0, rotation: 0 }, default: defaultId, children: variants.map(([variantId, name, children]) => ({ type: "variant", id: variantId, name, children, })), }); describe("review regressions", () => { it("keeps sibling Variant references when deleting a Variant and moves the selection", async () => { const phy = emptyPhy(); phy.root = [ variantGroup("vg", "va", [ ["va", "A", [key("ka")]], ["vb", "B", [key("kb")]], ["vc", "C", [key("kc")]], ]), ]; const p = await createProject(phy); p.idx.order = ["ka", "kb", "kc"]; p.output.variantSelection = { vg: "va" }; p.elc.keys = ["ka", "kb", "kc"].map((id) => ({ key: id, detect: "contact", terminals: { a: "VCC", b: "GND" }, })); p.elc.ledChains = [{ id: "chain", data: "VCC", order: ["ka", "kb", "kc"] }]; p.map.layers[0]!.keymap = ["ka", "kb", "kc"].map((keyId) => ({ keyId, keycode: "KC_A" })); expect(listDeleteReferences(p, "va").map((ref) => ref.id)).toEqual(["ka", "ka", "ka", "ka"]); const result = deleteNodeCascade(p, "va"); expect( result.phy.root[0]!.type === "variantGroup" && result.phy.root[0]!.children.map((v) => v.id), ).toEqual(["vb", "vc"]); expect(result.phy.root[0]!.type === "variantGroup" && result.phy.root[0]!.default).toBe("vb"); expect(result.output.variantSelection.vg).toBe("vb"); expect(result.idx.order).toEqual(["kb", "kc"]); expect(result.elc.keys.map((w) => w.key)).toEqual(["kb", "kc"]); expect(result.elc.ledChains?.[0]?.order).toEqual(["kb", "kc"]); expect(result.map.layers[0]!.keymap.map((assignment) => assignment.keyId)).toEqual(["kb", "kc"]); }); it("returns schema issues without running semantic validation on malformed input", async () => { const p = await createProject(emptyPhy()); const badOutput = await loadProject(JSON.stringify({ ...p, output: {} })); expect(badOutput.errors.some((issue) => issue.path === "/output/variantSelection")).toBe(true); expect(badOutput.errors.some((issue) => issue.code === "positive-unit-size")).toBe(false); const badPhy = await loadProject( JSON.stringify({ ...p, phy: { version: "0.1.0", profiles: null, root: [] } }), ); expect(badPhy.errors.every((issue) => issue.code === "schema")).toBe(true); expect(badPhy.errors.length).toBeGreaterThan(0); const blockedSave = await saveProject({ ...p, phy: { ...p.phy, profiles: [] } }); expect(blockedSave.json).toBeUndefined(); expect(blockedSave.errors.some((issue) => issue.severity === "blocksSave")).toBe(true); const semanticBlock = await saveProject({ ...p, phy: { ...p.phy, profiles: [{ ...p.phy.profiles[0]!, unitSize: { x: 0, y: 1 } }] }, }); expect(semanticBlock.json).toBeUndefined(); expect(semanticBlock.errors.some((issue) => issue.code === "positive-unit-size")).toBe(true); }); it("rejects unsupported project versions and allows other blocksSave projects to load", async () => { const p = await createProject(emptyPhy()); const unsupported = await loadProject(JSON.stringify({ ...p, version: "0.2.0" })); expect(unsupported.project).toBeUndefined(); expect(unsupported.errors.some((x) => x.code === "version")).toBe(true); p.idx.order = ["missing"]; const blocked = await loadProject(JSON.stringify(p)); expect(blocked.project).toBeDefined(); expect(blocked.errors.some((x) => x.severity === "blocksSave")).toBe(true); }); it("blocks saving duplicate IDX keys", async () => { const phy = emptyPhy(); phy.root = [key("k")]; const p = await createProject(phy); p.idx.order = ["k", "k"]; expect(validateProject(p).some((x) => x.code === "schema" && x.severity === "blocksSave")).toBe(true); const result = await saveProject(p); expect(result.json).toBeUndefined(); expect(result.errors.some((x) => x.code === "schema")).toBe(true); }); it("does not retain a no-op grouped history entry", async () => { const p = await createProject(emptyPhy()); const history = new ProjectHistory(p); history.begin(); history.commit(p); expect(history.canUndo).toBe(false); const changed = { ...p, projectId: "changed" }; history.begin(); history.commit(changed); expect(history.canUndo).toBe(true); expect(history.undo().projectId).toBe(p.projectId); }); it("ignores decals when requiring index registration, wiring, and keymap assignments", async () => { const phy = emptyPhy(); phy.root = [key("decal", true)]; const p = await createProject(phy); const relevant = validateProject(p).filter((issue) => issue.ids?.includes("decal")); expect( relevant.filter((issue) => ["idx-unregistered", "unwired-key", "unassigned-key"].includes(issue.code)), ).toEqual([]); }); it("preserves extension fields while editing existing MAP and ELC entries", async () => { const p = await createProject(emptyPhy()); const map = p.map as MapFile; map.layers[0]!.keymap = [{ keyId: "k", keycode: "KC_A", vendorAssignment: { keep: true } }]; const editedMap = assignKey(map, "Base", "k", "KC_B"); expect(editedMap.layers[0]!.keymap[0]).toEqual({ keyId: "k", keycode: "KC_B", vendorAssignment: { keep: true }, }); expect(() => assignKey(map, "Base", "k", "VENDOR_ACTION")).toThrow(); expect(() => assignKey(map, "Base", "k", "MO(Missing)")).toThrow(); const elc = p.elc as ElcFile; elc.providers = [{ id: "m", kind: "mcu", part: "RP2040", vendorProvider: 9 }]; const editedProvider = setProvider(elc, { id: "m", kind: "mcu", part: "RP2040B" }); expect(editedProvider.providers[0]).toMatchObject({ part: "RP2040B", vendorProvider: 9 }); elc.keys = [ { key: "k", detect: "contact", terminals: { a: "m.GP0", b: "m.GP1", vendorTerminal: "x" }, vendorWiring: true, } as ContactWiring, ]; const editedElc = setKeyWiring(elc, { key: "k", detect: "contact", terminals: { a: "m.GP2", b: "m.GP3" }, }); expect(editedElc.keys[0]).toMatchObject({ vendorWiring: true, terminals: { a: "m.GP2", b: "m.GP3", vendorTerminal: "x" }, }); }); it("validates pin ownership, provider pin ranges, duplicate ELC IDs and MAP layer references", async () => { const phy = emptyPhy(); phy.root = [key("k")]; const p = await createProject(phy); p.elc.providers = [ { id: "m", kind: "mcu", part: "RP2040" }, { id: "m", kind: "mcu", part: "RP2040" }, { id: "mux", kind: "analog-mux", channels: 2, select: ["missing.X"], common: "m.GP30" }, ]; p.elc.keys = [ { key: "k", detect: "contact", terminals: { a: "mux.2", b: "missing.P" } }, { key: "k", detect: "contact", terminals: { a: "VCC", b: "GND" } }, ]; p.elc.ledChains = [ { id: "chain", data: "m.GP0", order: [] }, { id: "chain", data: "m.GP1", order: [] }, ]; p.map.layers[0]!.keymap = [{ keyId: "k", keycode: "MO(Missing)" }]; const codes = new Set(validateProject(p).map((issue) => issue.code)); for (const code of [ "duplicate-provider-id", "duplicate-key-wiring", "duplicate-led-chain-id", "missing-pin-provider", "unknown-provider-pin", "missing-map-layer-reference", ]) expect(codes.has(code), code).toBe(true); const unknownPart = validateProject({ ...p, elc: { ...p.elc, providers: [{ id: "custom", kind: "mcu", part: "UnknownPart" }], keys: [{ key: "k", detect: "contact", terminals: { a: "custom.any_pin", b: "VCC" } }], ledChains: [], }, }); expect(unknownPart.some((issue) => issue.code === "unknown-provider-pin")).toBe(false); expect(() => setProvider(p.elc, { id: "bad.id", kind: "mcu", part: "RP2040" })).toThrow(); }); it("selects nested variants and treats sibling alternatives as exclusive", async () => { const phy = emptyPhy(); phy.root = [ variantGroup("outer", "outer-a", [ [ "outer-a", "A", [ key("common"), variantGroup("inner", "inner-a", [ ["inner-a", "IA", [key("ia")]], ["inner-b", "IB", [key("ib")]], ]), ], ], ["outer-b", "B", [key("other")]], ]), ]; const p = await createProject(phy); p.idx.order = ["common", "ia", "ib", "other"]; p.output.variantSelection = { outer: "outer-a", inner: "inner-b" }; p.elc.providers = [{ id: "m", kind: "mcu", part: "RP2040" }]; p.output.matrix = { rowPins: ["m.GP0", "m.GP2"], colPins: ["m.GP1", "m.GP3"] }; p.elc.keys = ["common", "ia", "ib", "other"].map((id) => ({ key: id, detect: "contact", terminals: id === "common" || id === "other" ? { a: "m.GP2", b: "m.GP3" } : { a: "m.GP0", b: "m.GP1" }, diode: "b-to-a", })); p.map.layers[0]!.keymap = ["common", "ia", "ib", "other"].map((keyId) => ({ keyId, keycode: "KC_A" })); expect(validateProject(p).some((issue) => issue.code === "wiring-collision")).toBe(false); const generated = generateMockTarget(p); expect(generated.target?.idx).toEqual(["common", "ib"]); expect(p.idx.order).toEqual(["common", "ia", "ib", "other"]); }); it("uses the independently calculated JCS checksum and reports stale references", async () => { const phy = emptyPhy(); expect(await makePhyReference(phy)).toEqual({ source: "project:phy", checksum: "sha256-jcs:277e45f2cae4b3883480599c5569e73fbeec9e1a22bfb9bc323aecc4cb91d843", }); const p = await createProject(phy); p.phy.metadata.name = "changed after reference creation"; const loaded = await loadProject(JSON.stringify(p)); expect( loaded.errors.filter((issue) => issue.code === "phy-reference-mismatch").map((issue) => issue.path), ).toEqual(["/idx/phyReference", "/elc/phyReference", "/map/phyReference"]); }); });