import { describe, expect, it } from "vitest"; import { resolveTemplateValues, substituteSnapshot, substituteTemplateTokens, validateTemplateVars } from "./template-vars.js"; describe("template variables (T-142)", () => { it("validates a well-formed variable list", () => { const r = validateTemplateVars([ { name: "title", label: "Title", type: "text", required: true }, { name: "count", label: "Count", type: "number", defaultValue: "0" }, { name: "date", label: "Date", type: "date" }, { name: "status", label: "Status", type: "select", options: ["open", "closed"] } ]); expect(r.ok).toBe(true); if (r.ok) expect(r.vars).toHaveLength(4); }); it("rejects bad names, duplicates, unknown types, empty select options", () => { for (const bad of [ [{ name: "bad name!", label: "L", type: "text" }], [{ name: "a", label: "L", type: "text" }, { name: "a", label: "M", type: "text" }], [{ name: "a", label: "L", type: "widget" }], [{ name: "a", label: "L", type: "select" }], "nope" ]) { const r = validateTemplateVars(bad); expect(r.ok).toBe(false); if (!r.ok) expect(r.errors.length).toBeGreaterThan(0); } }); it("resolves values: defaults, required, coercion, select membership, length cap", () => { const r = validateTemplateVars([ { name: "title", label: "Title", type: "text", required: true }, { name: "count", label: "Count", type: "number", defaultValue: "3" }, { name: "date", label: "Date", type: "date" }, { name: "status", label: "Status", type: "select", options: ["open", "closed"] } ]); expect(r.ok).toBe(true); const vars = (r as { ok: true; vars: import("./template-vars.js").TemplateVar[] }).vars; // required missing → error expect(resolveTemplateValues(vars, {}).ok).toBe(false); // all good const good = resolveTemplateValues(vars, { title: "T", count: "5", date: "2026-10-11", status: "open" }); expect(good.ok).toBe(true); if (good.ok) { expect(good.values.count).toBe("5"); expect(good.values.status).toBe("open"); } // default applied when omitted const def = resolveTemplateValues(vars, { title: "T" }); expect(def.ok).toBe(true); if (def.ok) expect(def.values.count).toBe("3"); // bad select value / bad date / non-number for (const v of [ { title: "T", status: "maybe" }, { title: "T", date: "tomorrow" }, { title: "T", count: "abc" } ]) { expect(resolveTemplateValues(vars, v).ok).toBe(false); } // text length cap expect(resolveTemplateValues(vars, { title: "T", count: "x".repeat(2001) }).ok).toBe(false); }); it("substitutes tokens (plain text; unknown tokens → empty)", () => { expect(substituteTemplateTokens("Hi {{name}}, due {{date}}", { name: "Ada", date: "2026-10-11" })).toBe( "Hi Ada, due 2026-10-11" ); expect(substituteTemplateTokens("{{ missing }} stays", {})).toBe(" stays"); expect(substituteTemplateTokens("no tokens", { a: "b" })).toBe("no tokens"); }); it("substitutes across the snapshot (page + blocks + children) without mutating input", () => { const snapshot = { page: { title: "{{title}} draft", icon: "📄", description: "owner: {{owner}}" }, blocks: [ { id: "b1", type: "PARAGRAPH", content: { text: "Count: {{count}}" }, properties: {}, parentId: null, position: "A" }, { id: "b2", type: "TODO", content: { text: "ship" }, properties: {}, parentId: null, position: "B" } ], children: [{ title: "{{title}} child", blocks: [{ content: { text: "{{owner}} owns" } }] }] }; const out = substituteSnapshot(snapshot, { title: "Q3", owner: "Ada", count: "7" }); expect(out.page.title).toBe("Q3 draft"); expect(out.page.description).toBe("owner: Ada"); const firstBlock = out.blocks[0] as { content: { text: string } } | undefined; expect(firstBlock?.content.text).toBe("Count: 7"); const firstChild = out.children[0]; expect(firstChild?.title).toBe("Q3 child"); const childBlock = firstChild?.blocks[0] as { content: { text: string } } | undefined; expect(childBlock?.content.text).toBe("Ada owns"); // input untouched expect(snapshot.page.title).toBe("{{title}} draft"); }); });