import { test } from "node:test"; import assert from "node:assert/strict"; import { validateDeck } from "../src/validate.js"; import { slideCatalog, deckSchema } from "../src/ai/catalog.js"; import { buildOpsSchema } from "../src/ai/ops.js"; const base = { title: "T", slides: [{ type: "title" }] }; // Every one of these is a content type, so the schema now requires a headline: // a slide that reserves a heading band and gets none renders as an empty band // above stretched content. const VALID = { "big-number": { headline: "H", value: "180 GW", label: "capacity by 2030", body: "A long claim about capacity.", sub: "source line" }, agenda: { headline: "H", items: [{ title: "Intro" }, { title: "Method", desc: "how" }, { title: "Results" }] }, milestone: { headline: "H", milestones: [{ when: "2021", title: "First plant", body: "opened" }, { when: "2026", title: "Scale-up" }] }, "pros-cons": { headline: "H", pros: ["Cheap to run", "Clean output"], cons: ["Costly build", "Needs grid"] }, emphasis: { headline: "H", body: "The one point that matters.", label: "Key point" }, definition: { headline: "H", term: "Overpotential", definition: "Extra voltage beyond the ideal.", example: "e.g. 1.23 V ideal, ~1.8 V real" }, }; for (const [type, payload] of Object.entries(VALID)) { test(`${type}: a valid slide passes, wrong field types are rejected`, async () => { const ok = await validateDeck({ ...base, slides: [...base.slides, { type, ...payload }] }); assert.equal(ok.ok, true, JSON.stringify(ok.errors)); // The same field with the wrong shape must fail with a type message. const wrong = await validateDeck({ ...base, slides: [...base.slides, { type, ...payload, ...badField(type) }], }); assert.equal(wrong.ok, false); assert.ok(wrong.errors.some((e) => /must be/.test(e)), wrong.errors.join("; ")); }); } function badField(type) { const scalar = "not-an-array"; switch (type) { case "big-number": return { value: 180 }; // value is a string case "agenda": return { items: [{ title: 5 }] }; // title is a string case "milestone": return { milestones: [{ when: "2021", title: 5 }] }; case "pros-cons": return { pros: [5] }; // strings only case "emphasis": return { body: 5 }; case "definition": return { term: 5 }; default: return { items: scalar }; } } test("new types are missing required fields and fail clearly", async () => { const r = await validateDeck({ ...base, slides: [...base.slides, { type: "big-number" }] }); assert.equal(r.ok, false); assert.ok(r.errors.some((e) => e.includes("missing required field \"value\""))); assert.ok(r.errors.some((e) => e.includes("missing required field \"label\""))); }); test("the catalog picks up the new types and their families", async () => { const cat = await slideCatalog(); const schema = await deckSchema(); const enumTypes = schema.definitions.slide.properties.type.enum; for (const t of Object.keys(VALID)) assert.ok(enumTypes.includes(t)); // Family categorisation is present and groups the new arrivals. assert.ok(cat.includes("DATA & STATS: stats, big-number")); assert.ok(cat.includes("COMPARISON: compare, pros-cons")); assert.ok(cat.includes("DEFINITIONS & FAQ: definition")); // Per-type detail auto-derives the fields from the schema. assert.ok(cat.includes("- big-number: value")); assert.ok(cat.includes("- milestone: milestones")); }); test("the per-slide ops schema constrains to one new type's fields", async () => { const schema = await deckSchema(); const ops = buildOpsSchema(schema, { slideCount: 2, onlyTypes: ["pros-cons"] }); const slide = ops.properties.ops.items.properties.slide; // `headline` joins every content type's required set, so the grammar a model // writes through demands one rather than leaving it optional. assert.deepEqual(slide.required.sort(), ["cons", "headline", "pros", "type"]); assert.ok(slide.properties.pros.maxItems === 8); });