1import { describe, expect, it } from "vitest";
2import {
3 resolveTemplateValues,
4 substituteSnapshot,
5 substituteTemplateTokens,
6 validateTemplateVars
7} from "./template-vars.js";
8
9describe("template variables (T-142)", () => {
10 it("validates a well-formed variable list", () => {
11 const r = validateTemplateVars([
12 { name: "title", label: "Title", type: "text", required: true },
13 { name: "count", label: "Count", type: "number", defaultValue: "0" },
14 { name: "date", label: "Date", type: "date" },
15 { name: "status", label: "Status", type: "select", options: ["open", "closed"] }
16 ]);
17 expect(r.ok).toBe(true);
18 if (r.ok) expect(r.vars).toHaveLength(4);
19 });
20
21 it("rejects bad names, duplicates, unknown types, empty select options", () => {
22 for (const bad of [
23 [{ name: "bad name!", label: "L", type: "text" }],
24 [{ name: "a", label: "L", type: "text" }, { name: "a", label: "M", type: "text" }],
25 [{ name: "a", label: "L", type: "widget" }],
26 [{ name: "a", label: "L", type: "select" }],
27 "nope"
28 ]) {
29 const r = validateTemplateVars(bad);
30 expect(r.ok).toBe(false);
31 if (!r.ok) expect(r.errors.length).toBeGreaterThan(0);
32 }
33 });
34
35 it("resolves values: defaults, required, coercion, select membership, length cap", () => {
36 const r = validateTemplateVars([
37 { name: "title", label: "Title", type: "text", required: true },
38 { name: "count", label: "Count", type: "number", defaultValue: "3" },
39 { name: "date", label: "Date", type: "date" },
40 { name: "status", label: "Status", type: "select", options: ["open", "closed"] }
41 ]);
42 expect(r.ok).toBe(true);
43 const vars = (r as { ok: true; vars: import("./template-vars.js").TemplateVar[] }).vars;
44
45
46 expect(resolveTemplateValues(vars, {}).ok).toBe(false);
47
48 const good = resolveTemplateValues(vars, { title: "T", count: "5", date: "2026-10-11", status: "open" });
49 expect(good.ok).toBe(true);
50 if (good.ok) {
51 expect(good.values.count).toBe("5");
52 expect(good.values.status).toBe("open");
53 }
54
55 const def = resolveTemplateValues(vars, { title: "T" });
56 expect(def.ok).toBe(true);
57 if (def.ok) expect(def.values.count).toBe("3");
58
59 for (const v of [
60 { title: "T", status: "maybe" },
61 { title: "T", date: "tomorrow" },
62 { title: "T", count: "abc" }
63 ]) {
64 expect(resolveTemplateValues(vars, v).ok).toBe(false);
65 }
66
67 expect(resolveTemplateValues(vars, { title: "T", count: "x".repeat(2001) }).ok).toBe(false);
68 });
69
70 it("substitutes tokens (plain text; unknown tokens → empty)", () => {
71 expect(substituteTemplateTokens("Hi {{name}}, due {{date}}", { name: "Ada", date: "2026-10-11" })).toBe(
72 "Hi Ada, due 2026-10-11"
73 );
74 expect(substituteTemplateTokens("{{ missing }} stays", {})).toBe(" stays");
75 expect(substituteTemplateTokens("no tokens", { a: "b" })).toBe("no tokens");
76 });
77
78 it("substitutes across the snapshot (page + blocks + children) without mutating input", () => {
79 const snapshot = {
80 page: { title: "{{title}} draft", icon: "📄", description: "owner: {{owner}}" },
81 blocks: [
82 { id: "b1", type: "PARAGRAPH", content: { text: "Count: {{count}}" }, properties: {}, parentId: null, position: "A" },
83 { id: "b2", type: "TODO", content: { text: "ship" }, properties: {}, parentId: null, position: "B" }
84 ],
85 children: [{ title: "{{title}} child", blocks: [{ content: { text: "{{owner}} owns" } }] }]
86 };
87 const out = substituteSnapshot(snapshot, { title: "Q3", owner: "Ada", count: "7" });
88 expect(out.page.title).toBe("Q3 draft");
89 expect(out.page.description).toBe("owner: Ada");
90 const firstBlock = out.blocks[0] as { content: { text: string } } | undefined;
91 expect(firstBlock?.content.text).toBe("Count: 7");
92 const firstChild = out.children[0];
93 expect(firstChild?.title).toBe("Q3 child");
94 const childBlock = firstChild?.blocks[0] as { content: { text: string } } | undefined;
95 expect(childBlock?.content.text).toBe("Ada owns");
96
97 expect(snapshot.page.title).toBe("{{title}} draft");
98 });
99});
100