- Extract shared statusBadgeClass() helper (src/viewers/statusBadge.ts) so SqlViewer, CteResultViewer and PhaseSummaryViewer can't drift: ok/success/ passed/promoted render green (--success), warn renders amber (--warning), error/failed stay destructive red, everything else stays neutral outline. Previously CteResultViewer/PhaseSummaryViewer mapped "ok"/"promoted" to the default badge variant, which is bg-primary (GSD red) — success states rendered red. - enrich.js: buildCteResultV2 now falls back preview.rows to [] instead of null when last_test.preview_rows is missing (pre-upgrade ok records), and CteResultViewer reads result.preview?.rows?.length with a null-safe fallback so it degrades to the "No preview rows" empty state instead of crashing. - PhaseSummaryViewer: section.items is optional (model-authored sections can be prose-only); render (section.items ?? []) instead of crashing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
9.0 KiB
JavaScript
202 lines
9.0 KiB
JavaScript
// Pure merge/enrichment functions (contracts.md A/B/C). Catalog lookups are injected
|
|
// as a `getColumns` function parameter so these are testable with no `tht` shell-out.
|
|
const test = require("node:test");
|
|
const assert = require("node:assert");
|
|
const {
|
|
enrichCtePlanV2,
|
|
buildCteResultV2,
|
|
enrichPhaseSummaryV2,
|
|
} = require("../enrich.js");
|
|
|
|
// --- enrichCtePlanV2 --------------------------------------------------------------
|
|
|
|
test("enrichCtePlanV2 assigns 1-based index in ctes[] order", () => {
|
|
const data = {
|
|
schema_version: 2, question: "q", strategy: "s",
|
|
ctes: [
|
|
{ name: "a", tables: [], filters: [] },
|
|
{ name: "b", tables: [], filters: [] },
|
|
],
|
|
};
|
|
const out = enrichCtePlanV2(data, () => null);
|
|
assert.equal(out.ctes[0].index, 1);
|
|
assert.equal(out.ctes[1].index, 2);
|
|
});
|
|
|
|
test("enrichCtePlanV2 fills tables[].description from the catalog", () => {
|
|
const getColumns = (table) =>
|
|
table === "dim_patient" ? { description: "Anagrafica pazienti", columns: [{ name: "cod_paz", description: "Codice paziente" }] } : null;
|
|
const data = {
|
|
schema_version: 2, question: "q", strategy: "s",
|
|
ctes: [{ name: "a", tables: [{ name: "dim_patient" }], filters: [] }],
|
|
};
|
|
const out = enrichCtePlanV2(data, getColumns);
|
|
assert.equal(out.ctes[0].tables[0].description, "Anagrafica pazienti");
|
|
});
|
|
|
|
test("enrichCtePlanV2 fills filters[].description by resolving table.column", () => {
|
|
const getColumns = (table) =>
|
|
table === "dim_patient"
|
|
? { description: "Anagrafica", columns: [{ name: "flag_attivo", description: "Flag attivo" }] }
|
|
: null;
|
|
const data = {
|
|
schema_version: 2, question: "q", strategy: "s",
|
|
ctes: [{ name: "a", tables: [], filters: [{ column: "dim_patient.flag_attivo", op: "IS", value: "TRUE" }] }],
|
|
};
|
|
const out = enrichCtePlanV2(data, getColumns);
|
|
assert.equal(out.ctes[0].filters[0].description, "Flag attivo");
|
|
});
|
|
|
|
test("enrichCtePlanV2: catalog miss -> empty description string, never throws", () => {
|
|
const out = enrichCtePlanV2(
|
|
{
|
|
schema_version: 2, question: "q", strategy: "s",
|
|
ctes: [{ name: "a", tables: [{ name: "nonexistent_table" }], filters: [{ column: "nonexistent_table.x", op: "IS", value: "1" }] }],
|
|
},
|
|
() => null,
|
|
);
|
|
assert.equal(out.ctes[0].tables[0].description, "");
|
|
assert.equal(out.ctes[0].filters[0].description, "");
|
|
});
|
|
|
|
test("enrichCtePlanV2: getColumns throwing is tolerated by the caller contract (function itself does not catch)", () => {
|
|
// enrich.js documents that getColumns must itself be safe (try/catch -> null on miss);
|
|
// enrichCtePlanV2 does not need its own try/catch around calls, since getColumns never throws.
|
|
const getColumns = () => null; // simulates the safe wrapper's miss behavior
|
|
const out = enrichCtePlanV2(
|
|
{ schema_version: 2, question: "q", strategy: "s", ctes: [{ name: "a", tables: [{ name: "t" }], filters: [] }] },
|
|
getColumns,
|
|
);
|
|
assert.equal(out.ctes[0].tables[0].description, "");
|
|
});
|
|
|
|
test("enrichCtePlanV2 preserves model-authored fields (purpose/rationale/depends_on/keys/output_columns)", () => {
|
|
const data = {
|
|
schema_version: 2, question: "q", strategy: "s",
|
|
ctes: [{
|
|
name: "a", purpose: "scopo", rationale: "motivo", depends_on: [],
|
|
keys: ["cod_paz"], output_columns: ["cod_paz"], tables: [], filters: [],
|
|
}],
|
|
};
|
|
const out = enrichCtePlanV2(data, () => null);
|
|
assert.equal(out.ctes[0].purpose, "scopo");
|
|
assert.equal(out.ctes[0].rationale, "motivo");
|
|
assert.deepEqual(out.ctes[0].keys, ["cod_paz"]);
|
|
assert.deepEqual(out.ctes[0].output_columns, ["cod_paz"]);
|
|
});
|
|
|
|
// --- buildCteResultV2 --------------------------------------------------------------
|
|
|
|
test("buildCteResultV2 merges name/index/total/sql from cteInfo", () => {
|
|
const thin = { schema_version: 2, note: "nota" };
|
|
const cteInfo = {
|
|
name: "base_pazienti", index: 1, total: 3, sql: "WITH base_pazienti AS (SELECT 1)",
|
|
doc: null, last_test: { status: "ok", execution_ms: 42, row_sample: 5, warnings: [], sql_hash: "h1", columns: ["x"], preview_rows: [[1]] },
|
|
};
|
|
const out = buildCteResultV2(thin, cteInfo);
|
|
assert.equal(out.name, "base_pazienti");
|
|
assert.equal(out.index, 1);
|
|
assert.equal(out.total, 3);
|
|
assert.equal(out.sql, "WITH base_pazienti AS (SELECT 1)");
|
|
assert.equal(out.note, "nota");
|
|
});
|
|
|
|
test("buildCteResultV2 prefers doc purpose/rationale/depends_on, falls back to thin data", () => {
|
|
const cteInfoWithDoc = {
|
|
name: "a", index: 1, total: 1, sql: "WITH a AS (SELECT 1)",
|
|
doc: { purpose: "scopo dal doc", rationale: "motivo dal doc", depends_on: ["x"] },
|
|
last_test: { status: "ok", execution_ms: 1, row_sample: 0, warnings: [], sql_hash: "h", columns: [], preview_rows: [] },
|
|
};
|
|
const out1 = buildCteResultV2({ schema_version: 2, purpose: "scopo thin" }, cteInfoWithDoc);
|
|
assert.equal(out1.purpose, "scopo dal doc");
|
|
assert.equal(out1.rationale, "motivo dal doc");
|
|
assert.deepEqual(out1.depends_on, ["x"]);
|
|
|
|
const cteInfoNoDoc = { ...cteInfoWithDoc, doc: null };
|
|
const out2 = buildCteResultV2({ schema_version: 2, purpose: "scopo thin", rationale: "motivo thin" }, cteInfoNoDoc);
|
|
assert.equal(out2.purpose, "scopo thin");
|
|
assert.equal(out2.rationale, "motivo thin");
|
|
});
|
|
|
|
test("buildCteResultV2 builds preview from last_test.columns/preview_rows", () => {
|
|
const cteInfo = {
|
|
name: "a", index: 1, total: 1, sql: "WITH a AS (SELECT 1)", doc: null,
|
|
last_test: { status: "ok", execution_ms: 10, row_sample: 2, warnings: [], sql_hash: "h1", columns: ["cod_paz", "eta"], preview_rows: [[1, 30], [2, 45]] },
|
|
};
|
|
const out = buildCteResultV2({ schema_version: 2 }, cteInfo);
|
|
assert.deepEqual(out.preview, { columns: ["cod_paz", "eta"], rows: [[1, 30], [2, 45]] });
|
|
assert.equal(out.status, "ok");
|
|
assert.equal(out.execution_ms, 10);
|
|
assert.equal(out.row_sample, 2);
|
|
assert.equal(out.sql_hash, "h1");
|
|
assert.deepEqual(out.warnings, []);
|
|
});
|
|
|
|
test("buildCteResultV2: last_test.preview_rows null (pre-upgrade ok records) -> preview.rows is []", () => {
|
|
const cteInfo = {
|
|
name: "a", index: 1, total: 1, sql: "WITH a AS (SELECT 1)", doc: null,
|
|
last_test: { status: "ok", execution_ms: 1, row_sample: 1, warnings: [], sql_hash: "h", columns: ["cod_paz"], preview_rows: null },
|
|
};
|
|
const out = buildCteResultV2({ schema_version: 2 }, cteInfo);
|
|
assert.deepEqual(out.preview.rows, []);
|
|
});
|
|
|
|
test("buildCteResultV2: columns[] carries name, with description left as an empty placeholder (filled separately by the gate)", () => {
|
|
const cteInfo = {
|
|
name: "a", index: 1, total: 1, sql: "WITH a AS (SELECT 1)", doc: null,
|
|
last_test: { status: "ok", execution_ms: 1, row_sample: 1, warnings: [], sql_hash: "h", columns: ["cod_paz"], preview_rows: [[1]] },
|
|
};
|
|
const out = buildCteResultV2({ schema_version: 2 }, cteInfo);
|
|
assert.equal(out.columns.length, 1);
|
|
assert.equal(out.columns[0].name, "cod_paz");
|
|
assert.equal(out.columns[0].description, "");
|
|
});
|
|
|
|
// --- enrichPhaseSummaryV2 -----------------------------------------------------------
|
|
|
|
test("enrichPhaseSummaryV2 fills phase from phaseMeta", () => {
|
|
const data = { schema_version: 2, summary: "riepilogo" };
|
|
const phaseMeta = { id: "F5", num: 5, name: "sintesi" };
|
|
const out = enrichPhaseSummaryV2(data, phaseMeta, () => null);
|
|
assert.deepEqual(out.phase, { id: "F5", num: 5, name: "sintesi" });
|
|
});
|
|
|
|
test("enrichPhaseSummaryV2 enriches tables[]/tables[].columns[]/sections[].items[] descriptions", () => {
|
|
const getColumns = (table) =>
|
|
table === "dim_patient"
|
|
? { description: "Anagrafica", columns: [{ name: "cod_paz", description: "Codice paziente" }] }
|
|
: null;
|
|
const data = {
|
|
schema_version: 2, summary: "s",
|
|
tables: [{ name: "dim_patient", role: "promoted", columns: [{ name: "cod_paz" }] }],
|
|
sections: [{ title: "Criteri", items: [{ label: "id paziente", table: "dim_patient", column: "cod_paz" }] }],
|
|
};
|
|
const out = enrichPhaseSummaryV2(data, { id: "F5", num: 5, name: "sintesi" }, getColumns);
|
|
assert.equal(out.tables[0].description, "Anagrafica");
|
|
assert.equal(out.tables[0].columns[0].description, "Codice paziente");
|
|
assert.equal(out.sections[0].items[0].description, "Codice paziente");
|
|
});
|
|
|
|
test("enrichPhaseSummaryV2: catalog miss -> empty description, never throws", () => {
|
|
const data = {
|
|
schema_version: 2, summary: "s",
|
|
tables: [{ name: "nope", role: "excluded", columns: [{ name: "x" }] }],
|
|
sections: [{ title: "t", items: [{ label: "l", table: "nope", column: "x" }] }],
|
|
};
|
|
const out = enrichPhaseSummaryV2(data, { id: "F5", num: 5, name: "sintesi" }, () => null);
|
|
assert.equal(out.tables[0].description, "");
|
|
assert.equal(out.tables[0].columns[0].description, "");
|
|
assert.equal(out.sections[0].items[0].description, "");
|
|
});
|
|
|
|
test("enrichPhaseSummaryV2 preserves open_questions/checks untouched", () => {
|
|
const data = {
|
|
schema_version: 2, summary: "s",
|
|
checks: [{ label: "x", status: "ok" }],
|
|
open_questions: ["domanda aperta"],
|
|
};
|
|
const out = enrichPhaseSummaryV2(data, { id: "F1", num: 1, name: "chiarimento" }, () => null);
|
|
assert.deepEqual(out.checks, [{ label: "x", status: "ok" }]);
|
|
assert.deepEqual(out.open_questions, ["domanda aperta"]);
|
|
});
|