/** * Select/status/multi-select option-id normalization (bug: the AI generation * path emitted `config.options` as bare `{ name, color }` — no `id` — and * stored record values as the option NAME. Every id-based reader (web chips, * preselect, board grouping, filters) then failed to resolve the value). * * `normalizeSelectOptions` assigns a DETERMINISTIC id (`optionIdForName`) to * any option that lacks one, at the write boundary (create/update property) * and, defensively, on read. It is pure + idempotent — unit-tested here. */ import { describe, expect, it } from "vitest"; import { optionIdForName } from "@factory/ai"; import { normalizeSelectOptions } from "../database.service.js"; describe("normalizeSelectOptions (select option-id backfill)", () => { it("assigns a deterministic id to options that lack one", () => { const out = normalizeSelectOptions({ options: [ { name: "To Do", color: "default" }, { name: "In Progress", color: "blue" }, { name: "Done", color: "green" } ] }) as { options: { id: string; name: string }[] }; expect(out.options.map((o) => o.id)).toEqual([ optionIdForName("To Do"), optionIdForName("In Progress"), optionIdForName("Done") ]); // The names + colors are preserved verbatim. expect(out.options[0]).toEqual({ name: "To Do", color: "default", id: optionIdForName("To Do") }); }); it("leaves already-id'd options untouched (idempotent)", () => { const id = optionIdForName("To Do"); const cfg = { options: [{ id, name: "To Do", color: "default" }] }; const out = normalizeSelectOptions(cfg) as { options: { id: string }[] }; expect(out.options[0].id).toBe(id); // Second pass is a no-op (no new object). expect(normalizeSelectOptions(cfg)).toBe(cfg); }); it("keeps the ids of PRESENT options on a partial update patch (existing values stay resolvable)", () => { // The stored property already has `To Do` → deterministic id and // `Blocked` → a CUSTOM id. A patch that re-lists BOTH without ids must // reuse each option's existing id (so existing `{ optionId }` values keep // resolving), and a NEW option gets the deterministic name-derived id. const existing = [ { id: optionIdForName("To Do"), name: "To Do" }, { id: "opt-custom-blocked", name: "Blocked" } ]; const out = normalizeSelectOptions( { options: [{ name: "To Do" }, { name: "Blocked" }, { name: "Done" }] }, existing ) as { options: { id: string; name: string }[] }; expect(out.options[0].id).toBe(optionIdForName("To Do")); expect(out.options[1].id).toBe("opt-custom-blocked"); // custom id preserved expect(out.options[2].id).toBe(optionIdForName("Done")); // new → deterministic }); it("passes through non-select configs (no options array) unchanged", () => { const cfg = { format: "currency", currency: "USD" }; expect(normalizeSelectOptions(cfg)).toBe(cfg); expect(normalizeSelectOptions(null)).toBe(null); expect(normalizeSelectOptions(undefined)).toBe(undefined); expect(normalizeSelectOptions({})).toEqual({}); }); it("does not mutate the input", () => { const cfg = { options: [{ name: "To Do", color: "default" }] }; const before = JSON.stringify(cfg); normalizeSelectOptions(cfg); expect(JSON.stringify(cfg)).toBe(before); }); });