// Soft-validators for the v2 artifact payloads (contracts.md A/B/C). Pure, no I/O. // A payload without schema_version===2 is NOT an error -- it's legacy, and the caller // falls back to the current (pre-v2) rendering path. const test = require("node:test"); const assert = require("node:assert"); const { validateCtePlanV2, validateCteResultThin, validatePhaseSummaryV2, } = require("../core/artifact-contracts.js"); // --- validateCtePlanV2 (payload A) ------------------------------------------------ test("validateCtePlanV2: legacy (no schema_version) is ok:true, legacy:true", () => { const out = validateCtePlanV2({ some: "legacy shape" }); assert.deepEqual(out, { ok: true, legacy: true }); }); test("validateCtePlanV2: legacy when data is a plain string (markdown)", () => { const out = validateCtePlanV2("### Piano CTE\n1. foo"); assert.deepEqual(out, { ok: true, legacy: true }); }); test("validateCtePlanV2: valid v2 payload", () => { const out = validateCtePlanV2({ schema_version: 2, question: "domanda", strategy: "razionale", ctes: [ { name: "base_pazienti", purpose: "p", rationale: "r", depends_on: [], tables: [], keys: [], filters: [], output_columns: [] }, { name: "eventi", purpose: "p2", rationale: "r2", depends_on: ["base_pazienti"], tables: [], keys: [], filters: [], output_columns: [] }, ], }); assert.equal(out.ok, true); assert.ok(!out.errors || out.errors.length === 0); }); test("validateCtePlanV2: rejects missing ctes array", () => { const out = validateCtePlanV2({ schema_version: 2, question: "q", strategy: "s" }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /ctes/.test(e))); }); test("validateCtePlanV2: rejects a CTE missing name", () => { const out = validateCtePlanV2({ schema_version: 2, question: "q", strategy: "s", ctes: [{ purpose: "p" }], }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /ctes\[0\].*name/.test(e))); }); test("validateCtePlanV2: rejects depends_on referencing an unknown/forward CTE", () => { const out = validateCtePlanV2({ schema_version: 2, question: "q", strategy: "s", ctes: [ { name: "a", depends_on: ["b"] }, // b does not precede a { name: "b", depends_on: [] }, ], }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /depends_on/.test(e) && /'b'/.test(e))); }); test("validateCtePlanV2: depends_on referencing an earlier CTE is fine", () => { const out = validateCtePlanV2({ schema_version: 2, question: "q", strategy: "s", ctes: [ { name: "a", depends_on: [] }, { name: "b", depends_on: ["a"] }, ], }); assert.equal(out.ok, true); }); // --- validateCteResultThin (payload B, model side is THIN) ------------------------ test("validateCteResultThin: legacy (string/markdown) is ok:true, legacy:true", () => { assert.deepEqual(validateCteResultThin("**CTE risultato**: ok"), { ok: true, legacy: true }); assert.deepEqual(validateCteResultThin({ some: "legacy" }), { ok: true, legacy: true }); }); test("validateCteResultThin: valid thin v2 payload (all fields optional)", () => { const out = validateCteResultThin({ schema_version: 2 }); assert.equal(out.ok, true); }); test("validateCteResultThin: valid thin v2 payload with purpose/rationale/note", () => { const out = validateCteResultThin({ schema_version: 2, purpose: "seleziona pazienti", rationale: "base per la catena", note: "ok", }); assert.equal(out.ok, true); }); test("validateCteResultThin: rejects non-string purpose/rationale/note", () => { const out = validateCteResultThin({ schema_version: 2, purpose: 42 }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /purpose/.test(e))); }); test("validateCteResultThin: permissive on extra fields (e.g. a model that also sends sql) -- guidance against this lives in SKILL.md, not the validator", () => { const out = validateCteResultThin({ schema_version: 2, sql: "WITH a AS (SELECT 1)" }); assert.equal(out.ok, true); }); // --- validatePhaseSummaryV2 (payload C) ------------------------------------------- test("validatePhaseSummaryV2: legacy (no schema_version) is ok:true, legacy:true", () => { assert.deepEqual(validatePhaseSummaryV2({ recap: "legacy text" }), { ok: true, legacy: true }); assert.deepEqual(validatePhaseSummaryV2("legacy markdown"), { ok: true, legacy: true }); }); test("validatePhaseSummaryV2: valid minimal v2 payload", () => { const out = validatePhaseSummaryV2({ schema_version: 2, summary: "riepilogo" }); assert.equal(out.ok, true); }); test("validatePhaseSummaryV2: valid full v2 payload", () => { const out = validatePhaseSummaryV2({ schema_version: 2, summary: "riepilogo", checks: [{ label: "schema valido", status: "ok" }], sections: [{ title: "Criteri", items: [{ label: "flag attivo", table: "t", column: "c", value: "TRUE", kind: "filter" }] }], tables: [{ name: "t", role: "promoted", columns: [{ name: "c" }] }], open_questions: [], }); assert.equal(out.ok, true); }); test("validatePhaseSummaryV2: rejects missing summary", () => { const out = validatePhaseSummaryV2({ schema_version: 2 }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /summary/.test(e))); }); test("validatePhaseSummaryV2: rejects a check with an invalid status", () => { const out = validatePhaseSummaryV2({ schema_version: 2, summary: "s", checks: [{ label: "x", status: "maybe" }], }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /checks\[0\].*status/.test(e))); }); test("validatePhaseSummaryV2: rejects a section missing title or items", () => { const out = validatePhaseSummaryV2({ schema_version: 2, summary: "s", sections: [{ items: [] }], }); assert.equal(out.ok, false); assert.ok(out.errors.some((e) => /sections\[0\].*title/.test(e))); }); test("validatePhaseSummaryV2: rejects structured open_questions instead of crashing the UI", () => { const out = validatePhaseSummaryV2({ schema_version: 2, summary: "s", open_questions: [ { label: "pazienti_finale restituisce 0 righe", question: "Verificare se i filtri sono troppo restrittivi", }, ], }); assert.equal(out.ok, false); assert.ok(out.errors.some((error) => /open_questions\[0\]/.test(error))); }); test("validatePhaseSummaryV2: permissive on extra unknown fields", () => { const out = validatePhaseSummaryV2({ schema_version: 2, summary: "s", extra_field_from_model: "whatever" }); assert.equal(out.ok, true); });