// L1 tests for the pure widget-descriptor builders (spec D2/D4, §4.1). // Each builder turns plain params into a ui_request descriptor object -- no Pi // context, no I/O. Golden files pin the exact shape; fuzzy tests check that bad // params throw clearly rather than silently produce a broken widget. // Run: npm test (node --test) const test = require("node:test"); const assert = require("node:assert"); const fs = require("node:fs"); const path = require("node:path"); const { buildSelectRequest, buildMultiselectRequest, buildArtifactGate, buildInfoRequest, buildFreetextRequest, buildSchemaLinkingRequest, buildJoinReviewRequest, withChildLinkage, } = require("../core/builders.js"); const GOLDEN = path.join(__dirname, "golden"); const golden = (name) => JSON.parse(fs.readFileSync(path.join(GOLDEN, name))); // --- golden shape tests ------------------------------------------------------- test("select F1 matches golden shape", () => { const result = buildSelectRequest({ id: "u1", phase: "F1", title: "Disambigua 'ablazione'", options: [{ id: "o1", label: "procedura" }, { id: "o2", label: "patologia" }], }); const g = golden("select_F1.json"); assert.equal(result.widget, "select"); assert.equal(result.type, "ui_request"); assert.equal(result.schema_version, 1); assert.deepEqual(result.reserved, g.reserved); // back/exit/other framework hatches assert.deepEqual(result.options, g.options); }); test("select no longer injects an Altro option (free-text is the reserved 'Other' control)", () => { const result = buildSelectRequest({ id: "u", phase: "F1", title: "x", options: [{ id: "a", label: "A" }], }); assert.equal(result.options.find((o) => o.id === "other"), undefined, "no injected Altro option"); assert.ok(!("allow_other" in result), "the unused allow_other flag is gone"); assert.deepEqual(result.reserved, ["back", "exit", "other"], "Altro still offered via the reserved control"); }); test("select surfaces a recommended marker when given", () => { const result = buildSelectRequest({ id: "u", phase: "F1", title: "x", options: [{ id: "a", label: "A" }, { id: "b", label: "B" }], recommended: "b", }); assert.equal(result.recommended, "b"); }); test("multiselect F4 matches golden + carries content + allowEmpty", () => { // a multiselect with allow_empty:false MUST have options (zero options is a // broken widget); the golden uses allow_empty:true for the empty-options case // and allow_empty:false when options are present. const result = buildMultiselectRequest({ id: "u3", phase: "F4", title: "Tabelle", options: [{ id: "dim_pazienti", label: "dim_pazienti" }], content: { candidates: [] }, allowEmpty: false, }); const g = golden("multiselect_F4.json"); assert.equal(result.widget, "multiselect"); assert.equal(result.allow_empty, false); assert.ok(!("allow_other" in result), "the unused allow_other flag is gone"); assert.ok("content" in result); assert.deepEqual(result.reserved, g.reserved); }); test("multiselect with allow_empty:true accepts zero options", () => { const result = buildMultiselectRequest({ id: "u", phase: "F4", title: "x", options: [], allowEmpty: true, }); assert.equal(result.allow_empty, true); assert.deepEqual(result.options, []); }); test("multiselect with allowEmpty:false and zero options throws", () => { assert.throws( () => buildMultiselectRequest({ id: "u", phase: "F4", title: "x", options: [], allowEmpty: false }), /allow_empty|options/i, ); }); test("artifact-gate F5 schema matches golden", () => { const result = buildArtifactGate({ id: "u2", phase: "F5", title: "Schema-linking", artifact: { kind: "schema_linking", data: { candidates: [] }, version: 1 }, action: { kind: "confirm", prompt: "Confermi?" }, }); const g = golden("artifact_gate_F5.json"); assert.equal(result.widget, "artifact-gate"); assert.equal(result.artifact.kind, "schema_linking"); assert.equal(result.action.kind, "confirm"); assert.deepEqual(result.reserved, g.reserved); }); // --- the other widgets -------------------------------------------------------- test("info request is fire-and-forget (no reserved, no options)", () => { const result = buildInfoRequest({ phase: "F2", level: "info", text: "sto per proporti lo schema-linking" }); assert.equal(result.widget, "info"); assert.equal(result.level, "info"); assert.equal(result.type, "ui_request"); assert.equal(result.id, undefined, "info is not correlated like a blocking widget"); assert.ok(!("reserved" in result)); }); test("freetext request carries a title and no options", () => { const result = buildFreetextRequest({ id: "u9", phase: "F1", title: "Specifica…" }); assert.equal(result.widget, "freetext"); assert.equal(result.title, "Specifica…"); assert.deepEqual(result.reserved, ["back", "exit", "other"]); }); test("buildSchemaLinkingRequest carries tables + reserved", () => { const out = buildSchemaLinkingRequest({ id: "u7", phase: "F4", title: "F4 — Schema linking", tables: [{ id: "t1", name: "dim_patient", kind: "promote", columns: [] }], }); assert.equal(out.widget, "schema-linking"); assert.equal(out.id, "u7"); assert.equal(out.tables.length, 1); assert.deepEqual(out.reserved, ["back", "exit", "other"]); }); test("buildJoinReviewRequest exposes joins as read-only review items", () => { const out = buildJoinReviewRequest({ id: "u8", phase: "F4", title: "F4 — Proposed joins", options: [ { id: "patient-events", label: "Patient to events", detail: "JOIN dim_patient p ON p.cod_paz = e.cod_paz", rationale: "Standard patient key", }, ], }); assert.equal(out.widget, "join-review"); assert.equal(out.confirm_label, "Continue"); assert.deepEqual(out.options, [ { id: "patient-events", label: "Patient to events", detail: "JOIN dim_patient p ON p.cod_paz = e.cod_paz", rationale: "Standard patient key", }, ]); assert.deepEqual(out.reserved, ["back", "exit", "other"]); }); test("withChildLinkage sets option.opens and returns the option", () => { const child = { widget: "freetext", title: "Motivazione del rifiuto" }; const option = withChildLinkage({ id: "reject", label: "Rifiuta" }, child); assert.deepEqual(option.opens, child); }); // --- fuzzy tests (bad params must throw clearly, never silently break) -------- test("select with missing title throws clearly", () => { assert.throws( () => buildSelectRequest({ id: "u", phase: "F1", options: [] }), /title/i, ); }); test("select with non-array options throws clearly", () => { assert.throws( () => buildSelectRequest({ id: "u", phase: "F1", title: "x", options: "notalist" }), /options/i, ); }); test("artifact-gate with missing artifact throws clearly", () => { assert.throws( () => buildArtifactGate({ id: "u", phase: "F5", title: "x", action: { kind: "confirm" } }), /artifact/i, ); }); test("artifact-gate with unknown action kind throws clearly", () => { assert.throws( () => buildArtifactGate({ id: "u", phase: "F5", title: "x", artifact: { kind: "cte", data: {}, version: 1 }, action: { kind: "bogus" }, }), /action/i, ); }); test("buildArtifactGate derives approve/reject options from action.kind", () => { const w = buildArtifactGate({ id: "u1", phase: "F1", title: "Chiudi fase", artifact: { kind: "phase", data: {} }, action: { kind: "approve_reject" }, }); assert.deepEqual(w.options, [ { id: "approve", label: "Salva e procedi", recommended: true }, { id: "reject", label: "Rifiuta" }, ]); }); test("buildArtifactGate: confirm -> single approve; view_only -> no options", () => { const confirm = buildArtifactGate({ id: "u1", phase: "F1", title: "t", artifact: { kind: "phase", data: {} }, action: { kind: "confirm" }, }); assert.deepEqual(confirm.options, [{ id: "approve", label: "Salva e procedi", recommended: true }]); const viewOnly = buildArtifactGate({ id: "u1", phase: "F1", title: "t", artifact: { kind: "phase", data: {} }, action: { kind: "view_only" }, }); assert.deepEqual(viewOnly.options, []); });