@diviops/mcp-server 1.5.11 → 1.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,249 @@
1
+ /**
2
+ * `divi/font` heading emitter shape + gating coverage:
3
+ * - fixture-based shape assertion against round-1a (Pattern A) and
4
+ * round-1b (Pattern B) canonical captures;
5
+ * - Pattern B no-weight discriminator: omitted `weight` produces no key;
6
+ * - Pattern B + explicit weight refused as unverified/out-of-scope;
7
+ * - variant-aware registry gating: missing applicability or under-
8
+ * verified evidence on the chosen variant throws with attr / pattern /
9
+ * effective-level / source in the message; Pattern A evidence does NOT
10
+ * vouch for Pattern B and vice versa.
11
+ */
12
+ import { test } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { readFileSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+ import { dirname, join } from "node:path";
17
+ import { emitHeadingFontGroupPreset, composeHeadingFontAttrs, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, HEADING_FONT_MODULE, HEADING_FONT_GROUP_NAME, HEADING_FONT_GROUP_ID, HEADING_FONT_PATTERN_VARIANTS, } from "../heading-font-emitter.js";
18
+ import { loadRegistry, EvidenceGateError, } from "../registry.js";
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const REPO_ROOT = join(__dirname, "..", "..", "..", "..");
21
+ const FIXTURE_DIR = join(REPO_ROOT, "docs/verification/evidence/canonical-shape-dumps-2026-05-18");
22
+ const FIXTURE_1A = join(FIXTURE_DIR, "round-1a-heading-h1-pattern-a-google.json");
23
+ const FIXTURE_1B = join(FIXTURE_DIR, "round-1b-heading-h1-pattern-b-local.json");
24
+ const registry = loadRegistry();
25
+ test("emitter byte-matches round-1a (Pattern A — Google Fonts) fixture attrs", () => {
26
+ const fixture = JSON.parse(readFileSync(FIXTURE_1A, "utf8"));
27
+ const canonicalAttrs = fixture.preset_entry.attrs;
28
+ const canonicalStyleAttrs = fixture.preset_entry.styleAttrs;
29
+ const entry = emitHeadingFontGroupPreset({
30
+ name: "canonical-heading-h1-vb-google-2026-05-18",
31
+ pattern: "google",
32
+ family: "Inter",
33
+ weight: "700",
34
+ color: '$variable({"type":"color","value":{"name":"gcid-heading-color","settings":{}}})$',
35
+ size: "48px",
36
+ }, registry);
37
+ assert.deepEqual(entry.attrs, canonicalAttrs, "emitted attrs must byte-match the Pattern A canonical capture");
38
+ // Round 1a observation: attrs and styleAttrs are byte-identical. The
39
+ // emitter only emits `attrs`; the plugin's /preset/create route mirrors
40
+ // into styleAttrs. So `attrs` must equal the fixture's styleAttrs too.
41
+ assert.deepEqual(entry.attrs, canonicalStyleAttrs, "attrs must equal styleAttrs (mirror at /preset/create layer)");
42
+ assert.equal(entry.type, "group");
43
+ assert.equal(entry.group_name, HEADING_FONT_GROUP_NAME);
44
+ assert.equal(entry.group_id, HEADING_FONT_GROUP_ID);
45
+ assert.equal(entry.module_name, HEADING_FONT_MODULE);
46
+ assert.equal(entry.pattern_variant, HEADING_FONT_PATTERN_VARIANTS.google);
47
+ });
48
+ test("emitter byte-matches round-1b (Pattern B — local-hosted) fixture attrs", () => {
49
+ const fixture = JSON.parse(readFileSync(FIXTURE_1B, "utf8"));
50
+ const canonicalAttrs = fixture.preset_entry.attrs;
51
+ const canonicalStyleAttrs = fixture.preset_entry.styleAttrs;
52
+ const entry = emitHeadingFontGroupPreset({
53
+ name: "canonical-heading-h1-vb-local-2026-05-18",
54
+ pattern: "local",
55
+ family: "Sora 700",
56
+ // weight intentionally absent — Pattern B encodes the weight in the
57
+ // family string itself.
58
+ color: '$variable({"type":"color","value":{"name":"gcid-heading-color","settings":{}}})$',
59
+ size: "48px",
60
+ }, registry);
61
+ assert.deepEqual(entry.attrs, canonicalAttrs, "emitted attrs must byte-match the Pattern B canonical capture");
62
+ assert.deepEqual(entry.attrs, canonicalStyleAttrs, "attrs must equal styleAttrs (mirror at /preset/create layer)");
63
+ assert.equal(entry.pattern_variant, HEADING_FONT_PATTERN_VARIANTS.local);
64
+ });
65
+ test("Pattern B discriminator: omitted weight produces NO weight key", () => {
66
+ const attrs = composeHeadingFontAttrs({
67
+ name: "B",
68
+ pattern: "local",
69
+ family: "Sora 700",
70
+ color: "#666666",
71
+ size: "48px",
72
+ });
73
+ const value = attrs.title.decoration.font.font.desktop.value;
74
+ assert.equal("weight" in value, false, "Pattern B canonical shape has no `weight` key in attrs.title.decoration.font.font.desktop.value");
75
+ // The exact set of structural keys, no more, no less. Order is not
76
+ // semantically meaningful (deep-equal is order-insensitive on objects)
77
+ // — sort here for a stable assertion.
78
+ assert.deepEqual(Object.keys(value).sort(), ["color", "family", "size"], "Pattern B emits ONLY the keys the user specified — no defaulted weight");
79
+ });
80
+ test("Pattern A discriminator: weight emitted when specified, absent when not", () => {
81
+ const withWeight = composeHeadingFontAttrs({
82
+ name: "A+w",
83
+ pattern: "google",
84
+ family: "Inter",
85
+ weight: "700",
86
+ });
87
+ const withoutWeight = composeHeadingFontAttrs({
88
+ name: "A-w",
89
+ pattern: "google",
90
+ family: "Inter",
91
+ });
92
+ const wv = withWeight.title.decoration.font.font.desktop.value;
93
+ const wov = withoutWeight.title.decoration.font.font.desktop.value;
94
+ assert.equal(wv.weight, "700");
95
+ assert.equal("weight" in wov, false, "weight absent when not specified");
96
+ });
97
+ test("Pattern B + explicit --font-weight is refused as out-of-scope", () => {
98
+ assert.throws(() => emitHeadingFontGroupPreset({
99
+ name: "B-with-weight",
100
+ pattern: "local",
101
+ family: "Sora 700",
102
+ weight: "700",
103
+ }, registry), (err) => {
104
+ assert.ok(err instanceof UnsupportedVariantCombinationError);
105
+ assert.match(err.message, /Pattern B/);
106
+ assert.match(err.message, /weight/);
107
+ return true;
108
+ });
109
+ });
110
+ test("emit-on-specification: only specified keys produce keys (Pattern A)", () => {
111
+ const attrs = composeHeadingFontAttrs({
112
+ name: "minimal",
113
+ pattern: "google",
114
+ family: "Inter",
115
+ });
116
+ const value = attrs.title.decoration.font.font.desktop.value;
117
+ assert.deepEqual(Object.keys(value), ["family"], "ONLY the touched key emitted — no defaulted weight/color/size/lineHeight");
118
+ });
119
+ test("variable color tokens get the {name,settings} object shape + trailing )$", () => {
120
+ const attrs = composeHeadingFontAttrs({
121
+ name: "v",
122
+ pattern: "google",
123
+ color: "gcid-heading-color",
124
+ });
125
+ const color = attrs.title.decoration.font.font.desktop.value.color;
126
+ assert.ok(color.endsWith(")$"));
127
+ assert.match(color, /^\$variable\(/);
128
+ const payload = JSON.parse(color.slice("$variable(".length, -2));
129
+ assert.deepEqual(payload, {
130
+ type: "color",
131
+ value: { name: "gcid-heading-color", settings: {} },
132
+ });
133
+ });
134
+ test("literal hex color is emitted verbatim, not wrapped", () => {
135
+ const attrs = composeHeadingFontAttrs({
136
+ name: "h",
137
+ pattern: "google",
138
+ color: "#666666",
139
+ });
140
+ assert.equal(attrs.title.decoration.font.font.desktop.value.color, "#666666");
141
+ });
142
+ test("lineHeight is emitted only when specified", () => {
143
+ const withLH = composeHeadingFontAttrs({
144
+ name: "lh+",
145
+ pattern: "google",
146
+ family: "Inter",
147
+ lineHeight: "1.1",
148
+ });
149
+ assert.equal(withLH.title.decoration.font.font.desktop.value.lineHeight, "1.1");
150
+ const without = composeHeadingFontAttrs({
151
+ name: "lh-",
152
+ pattern: "google",
153
+ family: "Inter",
154
+ });
155
+ assert.equal("lineHeight" in without.title.decoration.font.font.desktop.value, false);
156
+ });
157
+ test("emitter rejects an invalid `pattern` value", () => {
158
+ assert.throws(() => emitHeadingFontGroupPreset(
159
+ // @ts-expect-error — exercising runtime validation
160
+ { name: "x", pattern: "auto", family: "Inter" }, registry), /pattern.*google.*local/i);
161
+ });
162
+ test("emitter rejects an empty preset (no styling specified)", () => {
163
+ assert.throws(() => emitHeadingFontGroupPreset({ name: "empty", pattern: "google" }, registry), /empty preset/);
164
+ });
165
+ test("emitter rejects a missing name", () => {
166
+ assert.throws(() => emitHeadingFontGroupPreset({ name: "", pattern: "google", family: "Inter" }, registry), /requires a non-empty `name`/);
167
+ });
168
+ test("variant-aware gate: missing applicability throws with attr+variant+source", () => {
169
+ // Build a synthetic registry where the Pattern A entry's applicability
170
+ // for divi/heading is REMOVED — Pattern B still verified. Confirms a
171
+ // missing applicability cell on the chosen variant is caught at the
172
+ // gate (not silently inherited from the pattern level), and that the
173
+ // error message names the variant and the registry source.
174
+ const real = loadRegistry();
175
+ const synthetic = JSON.parse(JSON.stringify(real));
176
+ const t2 = synthetic.tier2 ?? [];
177
+ const a = t2.find((e) => e.pattern_family === "divi/font" &&
178
+ e.pattern_variant === HEADING_FONT_PATTERN_VARIANTS.google);
179
+ assert.ok(a, "test prereq: Pattern A entry must exist in the real registry");
180
+ // Strip the divi/heading applicability cell from the Pattern A variant.
181
+ delete a.applicability["divi/heading"];
182
+ assert.throws(() => emitHeadingFontGroupPreset({
183
+ name: "missing-applic",
184
+ pattern: "google",
185
+ family: "Inter",
186
+ weight: "700",
187
+ }, synthetic), (err) => {
188
+ assert.ok(err instanceof EvidenceGateError);
189
+ assert.match(err.message, /divi\/font/);
190
+ assert.match(err.message, /google_fonts_pattern_a/);
191
+ assert.match(err.message, /UNVERIFIED \(0\)/);
192
+ assert.match(err.message, /absent from the registry entry/);
193
+ return true;
194
+ });
195
+ });
196
+ test("variant isolation: Pattern A evidence does NOT vouch for Pattern B (and vice versa)", () => {
197
+ // Build a synthetic registry containing ONLY the Pattern A divi/font
198
+ // entry (Pattern B variant removed). A Pattern-B caller must be
199
+ // refused — its variant is entirely absent — even though a Pattern A
200
+ // entry under the same `pattern_family` is fully verified.
201
+ const real = loadRegistry();
202
+ const synthetic = JSON.parse(JSON.stringify(real));
203
+ synthetic.tier2 = (synthetic.tier2 ?? []).filter((e) => !(e.pattern_family === "divi/font" &&
204
+ e.pattern_variant === HEADING_FONT_PATTERN_VARIANTS.local));
205
+ assert.throws(() => emitHeadingFontGroupPreset({
206
+ name: "B-without-its-entry",
207
+ pattern: "local",
208
+ family: "Sora 700",
209
+ }, synthetic), (err) => {
210
+ // Variant absent entirely → resolveEvidence throws the
211
+ // "absent from verified-attrs.json" error (not EvidenceGateError).
212
+ assert.match(err.message, /absent from verified-attrs\.json/, "missing variant must NOT silently fall back to the other variant's evidence");
213
+ assert.match(err.message, /local_hosted_pattern_b/);
214
+ return true;
215
+ });
216
+ // The reverse: stripping Pattern A and asking for Pattern A.
217
+ const synthetic2 = JSON.parse(JSON.stringify(real));
218
+ synthetic2.tier2 = (synthetic2.tier2 ?? []).filter((e) => !(e.pattern_family === "divi/font" &&
219
+ e.pattern_variant === HEADING_FONT_PATTERN_VARIANTS.google));
220
+ assert.throws(() => emitHeadingFontGroupPreset({ name: "A-without-its-entry", pattern: "google", family: "Inter" }, synthetic2), /google_fonts_pattern_a/);
221
+ });
222
+ test("buildHeadingFontPresetCreateBody mirrors the diviops_preset_create body shape", () => {
223
+ const entry = emitHeadingFontGroupPreset({ name: "H1", pattern: "google", family: "Inter", weight: "700" }, registry);
224
+ const body = buildHeadingFontPresetCreateBody(entry, { dry_run: true });
225
+ assert.deepEqual(body, {
226
+ module_name: HEADING_FONT_MODULE,
227
+ name: "H1",
228
+ attrs: entry.attrs,
229
+ type: "group",
230
+ group_name: HEADING_FONT_GROUP_NAME,
231
+ group_id: HEADING_FONT_GROUP_ID,
232
+ dry_run: true,
233
+ });
234
+ // `pattern_variant` is in-memory metadata; it must NOT leak into the wire body.
235
+ assert.equal("pattern_variant" in body, false, "pattern_variant is client-side gating metadata; it must not be sent over the wire");
236
+ const noDry = buildHeadingFontPresetCreateBody(entry);
237
+ assert.equal("dry_run" in noDry, false);
238
+ });
239
+ test("real registry: both divi/font variants on divi/heading clear the write threshold", () => {
240
+ // The verified-attrs.json shipped in-repo MUST keep both Pattern A and
241
+ // Pattern B at effective-evidence VB_PRESET_STORAGE_VERIFIED (4) for
242
+ // divi/heading. If a registry edit ever drops one below 4, this test
243
+ // is the canary.
244
+ const reg = loadRegistry();
245
+ const a = emitHeadingFontGroupPreset({ name: "A", pattern: "google", family: "Inter", weight: "700" }, reg);
246
+ assert.equal(a.pattern_variant, HEADING_FONT_PATTERN_VARIANTS.google);
247
+ const b = emitHeadingFontGroupPreset({ name: "B", pattern: "local", family: "Sora 700" }, reg);
248
+ assert.equal(b.pattern_variant, HEADING_FONT_PATTERN_VARIANTS.local);
249
+ });
@@ -132,3 +132,44 @@ test("real registry: divi/button styling families clear the write threshold", ()
132
132
  gateWriteAttr(reg, "divi/button", fam);
133
133
  }
134
134
  });
135
+ test("variant-aware lookup selects the correct entry by pattern_variant", () => {
136
+ // The real registry carries TWO `divi/font` entries that share a
137
+ // pattern_family but differ on pattern_variant. resolveEvidence must
138
+ // honor the variant — a Pattern A query must NOT match the Pattern B
139
+ // entry (and vice versa), even though both share the `divi/font` family.
140
+ const reg = loadRegistry();
141
+ const a = resolveEvidence(reg, "divi/heading", "divi/font", "google_fonts_pattern_a");
142
+ assert.equal(a.patternVariant, "google_fonts_pattern_a");
143
+ assert.equal(a.effectiveLevel, 4, "Pattern A on divi/heading must clear VB_PRESET_STORAGE_VERIFIED in the shipped registry");
144
+ const b = resolveEvidence(reg, "divi/heading", "divi/font", "local_hosted_pattern_b");
145
+ assert.equal(b.patternVariant, "local_hosted_pattern_b");
146
+ assert.equal(b.effectiveLevel, 4, "Pattern B on divi/heading must clear VB_PRESET_STORAGE_VERIFIED in the shipped registry");
147
+ // A bogus variant under a known family throws — no silent fallback.
148
+ assert.throws(() => resolveEvidence(reg, "divi/heading", "divi/font", "no_such_variant"), /absent from verified-attrs\.json/);
149
+ });
150
+ test("variant-aware lookup: divi/font-body Pattern A on divi/text clears the write threshold", () => {
151
+ // Track 6 contract: the shipped registry MUST carry one cleared
152
+ // `divi/font-body` + `google_fonts_pattern_a` entry on `divi/text`
153
+ // (effective level = VB_PRESET_STORAGE_VERIFIED). If a registry edit
154
+ // ever drops it, this canary fires.
155
+ const reg = loadRegistry();
156
+ const a = resolveEvidence(reg, "divi/text", "divi/font-body", "google_fonts_pattern_a");
157
+ assert.equal(a.patternVariant, "google_fonts_pattern_a");
158
+ assert.equal(a.effectiveLevel, 4, "divi/font-body Pattern A on divi/text must clear VB_PRESET_STORAGE_VERIFIED");
159
+ // gateWriteAttr must not throw at this cell.
160
+ gateWriteAttr(reg, "divi/text", "divi/font-body", "google_fonts_pattern_a");
161
+ });
162
+ test("variant-aware lookup: divi/font-body Pattern B is absent from the registry (refused)", () => {
163
+ // Track 6 contract: the shipped registry MUST NOT carry a
164
+ // `divi/font-body` + `local_hosted_pattern_b` entry. Pattern B for
165
+ // body-text was deliberately deferred per round-2 _meta — there is no
166
+ // captured evidence. resolveEvidence MUST throw the standard
167
+ // registry-absence error.
168
+ const reg = loadRegistry();
169
+ assert.throws(() => resolveEvidence(reg, "divi/text", "divi/font-body", "local_hosted_pattern_b"), (err) => {
170
+ assert.match(err.message, /absent from verified-attrs\.json/);
171
+ assert.match(err.message, /divi\/font-body/);
172
+ assert.match(err.message, /local_hosted_pattern_b/);
173
+ return true;
174
+ });
175
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `divi/font-body` text-body-font emitter shape + gating coverage:
3
+ * - fixture-based shape assertion against round-2 (Pattern A) canonical
4
+ * capture for the captured fields;
5
+ * - emit-on-specification: omitted size / lineHeight / weight produce no
6
+ * key in the output;
7
+ * - no renderAttrs on the in-memory entry (mirror happens at route layer);
8
+ * - Pattern B refusal: selecting `--pattern local` resolves to a
9
+ * registry-absence refusal (no `divi/font-body` + `local_hosted_pattern_b`
10
+ * entry exists in verified-attrs.json);
11
+ * - variant-aware registry gating: Pattern A evidence does NOT vouch for
12
+ * Pattern B and vice versa.
13
+ */
14
+ export {};
@@ -0,0 +1,191 @@
1
+ /**
2
+ * `divi/font-body` text-body-font emitter shape + gating coverage:
3
+ * - fixture-based shape assertion against round-2 (Pattern A) canonical
4
+ * capture for the captured fields;
5
+ * - emit-on-specification: omitted size / lineHeight / weight produce no
6
+ * key in the output;
7
+ * - no renderAttrs on the in-memory entry (mirror happens at route layer);
8
+ * - Pattern B refusal: selecting `--pattern local` resolves to a
9
+ * registry-absence refusal (no `divi/font-body` + `local_hosted_pattern_b`
10
+ * entry exists in verified-attrs.json);
11
+ * - variant-aware registry gating: Pattern A evidence does NOT vouch for
12
+ * Pattern B and vice versa.
13
+ */
14
+ import { test } from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import { readFileSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import { dirname, join } from "node:path";
19
+ import { emitTextBodyFontGroupPreset, composeTextBodyFontAttrs, buildTextBodyFontPresetCreateBody, TEXT_BODY_FONT_MODULE, TEXT_BODY_FONT_GROUP_NAME, TEXT_BODY_FONT_GROUP_ID, TEXT_BODY_FONT_PATTERN_VARIANTS, } from "../text-body-font-emitter.js";
20
+ import { loadRegistry } from "../registry.js";
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const REPO_ROOT = join(__dirname, "..", "..", "..", "..");
23
+ const FIXTURE_DIR = join(REPO_ROOT, "docs/verification/evidence/canonical-shape-dumps-2026-05-18");
24
+ const FIXTURE_ROUND_2 = join(FIXTURE_DIR, "round-2-body-text-pattern-a.json");
25
+ const registry = loadRegistry();
26
+ test("emitter byte-matches round-2 (Pattern A — Google Fonts) fixture attrs", () => {
27
+ const fixture = JSON.parse(readFileSync(FIXTURE_ROUND_2, "utf8"));
28
+ const canonicalAttrs = fixture.preset_entry.attrs;
29
+ const entry = emitTextBodyFontGroupPreset({
30
+ name: "canonical-body-text-vb-2026-05-18",
31
+ pattern: "google",
32
+ family: "Inter",
33
+ weight: "400",
34
+ color: '$variable({"type":"color","value":{"name":"gcid-body-color","settings":{}}})$',
35
+ size: "16px",
36
+ }, registry);
37
+ assert.deepEqual(entry.attrs, canonicalAttrs, "emitted attrs must byte-match the Pattern A canonical capture");
38
+ assert.equal(entry.type, "group");
39
+ assert.equal(entry.group_name, TEXT_BODY_FONT_GROUP_NAME);
40
+ assert.equal(entry.group_id, TEXT_BODY_FONT_GROUP_ID);
41
+ assert.equal(entry.module_name, TEXT_BODY_FONT_MODULE);
42
+ assert.equal(entry.pattern_variant, TEXT_BODY_FONT_PATTERN_VARIANTS.google);
43
+ });
44
+ test("emit-on-specification: only specified keys produce keys (no defaults)", () => {
45
+ const attrs = composeTextBodyFontAttrs({
46
+ name: "minimal",
47
+ pattern: "google",
48
+ family: "Inter",
49
+ });
50
+ const value = attrs.content.decoration.bodyFont.body.font.desktop
51
+ .value;
52
+ assert.deepEqual(Object.keys(value), ["family"], "ONLY the touched key emitted — no defaulted weight/color/size/lineHeight");
53
+ });
54
+ test("omitted weight / size / lineHeight produce no keys", () => {
55
+ const attrs = composeTextBodyFontAttrs({
56
+ name: "fc",
57
+ pattern: "google",
58
+ family: "Inter",
59
+ color: "#666666",
60
+ });
61
+ const value = attrs.content.decoration.bodyFont.body.font.desktop
62
+ .value;
63
+ assert.equal("weight" in value, false, "weight omitted → no key");
64
+ assert.equal("size" in value, false, "size omitted → no key");
65
+ assert.equal("lineHeight" in value, false, "lineHeight omitted → no key");
66
+ assert.deepEqual(Object.keys(value).sort(), ["color", "family"]);
67
+ });
68
+ test("entry carries no renderAttrs key (mirror happens at route layer)", () => {
69
+ const entry = emitTextBodyFontGroupPreset({
70
+ name: "no-renderattrs",
71
+ pattern: "google",
72
+ family: "Inter",
73
+ weight: "400",
74
+ }, registry);
75
+ assert.equal("renderAttrs" in entry, false, "renderAttrs must NOT appear on the in-memory entry — the plugin's " +
76
+ "/preset/create route mirrors attrs into styleAttrs/renderAttrs at " +
77
+ "the write layer");
78
+ assert.equal("styleAttrs" in entry, false, "same for styleAttrs");
79
+ });
80
+ test("variable color tokens get the {name,settings} object shape + trailing )$", () => {
81
+ const attrs = composeTextBodyFontAttrs({
82
+ name: "v",
83
+ pattern: "google",
84
+ color: "gcid-body-color",
85
+ });
86
+ const color = attrs.content.decoration.bodyFont.body.font.desktop
87
+ .value.color;
88
+ assert.ok(color.endsWith(")$"));
89
+ assert.match(color, /^\$variable\(/);
90
+ const payload = JSON.parse(color.slice("$variable(".length, -2));
91
+ assert.deepEqual(payload, {
92
+ type: "color",
93
+ value: { name: "gcid-body-color", settings: {} },
94
+ });
95
+ });
96
+ test("literal hex color is emitted verbatim, not wrapped", () => {
97
+ const attrs = composeTextBodyFontAttrs({
98
+ name: "h",
99
+ pattern: "google",
100
+ color: "#666666",
101
+ });
102
+ assert.equal(attrs.content.decoration.bodyFont.body.font.desktop.value.color, "#666666");
103
+ });
104
+ test("lineHeight is emitted only when specified", () => {
105
+ const withLH = composeTextBodyFontAttrs({
106
+ name: "lh+",
107
+ pattern: "google",
108
+ family: "Inter",
109
+ lineHeight: "1.5",
110
+ });
111
+ assert.equal(withLH.content.decoration.bodyFont.body.font.desktop.value
112
+ .lineHeight, "1.5");
113
+ const without = composeTextBodyFontAttrs({
114
+ name: "lh-",
115
+ pattern: "google",
116
+ family: "Inter",
117
+ });
118
+ assert.equal("lineHeight" in
119
+ without.content.decoration.bodyFont.body.font.desktop.value, false);
120
+ });
121
+ test("emitter rejects an invalid `pattern` value", () => {
122
+ assert.throws(() => emitTextBodyFontGroupPreset(
123
+ // @ts-expect-error — exercising runtime validation
124
+ { name: "x", pattern: "auto", family: "Inter" }, registry), /pattern.*google.*local/i);
125
+ });
126
+ test("emitter rejects an empty preset (no styling specified)", () => {
127
+ assert.throws(() => emitTextBodyFontGroupPreset({ name: "empty", pattern: "google" }, registry), /empty preset/);
128
+ });
129
+ test("emitter rejects a missing name", () => {
130
+ assert.throws(() => emitTextBodyFontGroupPreset({ name: "", pattern: "google", family: "Inter" }, registry), /requires a non-empty `name`/);
131
+ });
132
+ test("Pattern B (local) is refused — registry-absence for divi/font-body", () => {
133
+ // Track 6 contract: NO `divi/font-body` + `local_hosted_pattern_b` entry
134
+ // exists in verified-attrs.json. `resolveEvidence`'s standard "absent
135
+ // from verified-attrs.json" throw fires natively — no special-case branch
136
+ // in the emitter. The error message MUST name the family AND the variant
137
+ // so operators can file the gap correctly.
138
+ assert.throws(() => emitTextBodyFontGroupPreset({
139
+ name: "body-local",
140
+ pattern: "local",
141
+ family: "Inter",
142
+ color: "#666666",
143
+ }, registry), (err) => {
144
+ assert.match(err.message, /absent from verified-attrs\.json/, "missing variant must fail with the canonical registry-absence message");
145
+ assert.match(err.message, /divi\/font-body/, "error names the pattern family");
146
+ assert.match(err.message, /local_hosted_pattern_b/, "error names the missing variant");
147
+ return true;
148
+ });
149
+ });
150
+ test("Pattern B refusal fires even with compound input (e.g. --font-weight)", () => {
151
+ // Compound-input parity: passing additional fields alongside
152
+ // --pattern local does NOT change the refusal — the gate fires on the
153
+ // missing registry entry regardless of which other fields are set.
154
+ assert.throws(() => emitTextBodyFontGroupPreset({
155
+ name: "body-local-weight",
156
+ pattern: "local",
157
+ family: "Inter",
158
+ weight: "700",
159
+ color: "#666666",
160
+ size: "16px",
161
+ }, registry), /absent from verified-attrs\.json/);
162
+ });
163
+ test("real registry: Pattern A on divi/text clears the write threshold", () => {
164
+ // Sanity guard: if a registry edit ever drops the (divi/font-body,
165
+ // google_fonts_pattern_a, divi/text) cell below VB_PRESET_STORAGE_VERIFIED
166
+ // (4), this test is the canary.
167
+ const entry = emitTextBodyFontGroupPreset({
168
+ name: "canary",
169
+ pattern: "google",
170
+ family: "Inter",
171
+ weight: "400",
172
+ }, registry);
173
+ assert.equal(entry.pattern_variant, TEXT_BODY_FONT_PATTERN_VARIANTS.google);
174
+ });
175
+ test("buildTextBodyFontPresetCreateBody mirrors the diviops_preset_create body shape", () => {
176
+ const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter", weight: "400" }, registry);
177
+ const body = buildTextBodyFontPresetCreateBody(entry, { dry_run: true });
178
+ assert.deepEqual(body, {
179
+ module_name: TEXT_BODY_FONT_MODULE,
180
+ name: "Body",
181
+ attrs: entry.attrs,
182
+ type: "group",
183
+ group_name: TEXT_BODY_FONT_GROUP_NAME,
184
+ group_id: TEXT_BODY_FONT_GROUP_ID,
185
+ dry_run: true,
186
+ });
187
+ // pattern_variant is in-memory metadata; it must NOT leak into the wire body.
188
+ assert.equal("pattern_variant" in body, false, "pattern_variant is client-side gating metadata; it must not be sent over the wire");
189
+ const noDry = buildTextBodyFontPresetCreateBody(entry);
190
+ assert.equal("dry_run" in noDry, false);
191
+ });
@@ -7,8 +7,10 @@
7
7
  */
8
8
  import { test } from "node:test";
9
9
  import assert from "node:assert/strict";
10
- import { applyButtonPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
10
+ import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
11
11
  import { emitButtonGroupPreset } from "../button-emitter.js";
12
+ import { emitHeadingFontGroupPreset } from "../heading-font-emitter.js";
13
+ import { emitTextBodyFontGroupPreset } from "../text-body-font-emitter.js";
12
14
  import { loadRegistry } from "../registry.js";
13
15
  const registry = loadRegistry();
14
16
  function handshake(capabilities) {
@@ -101,6 +103,113 @@ test("applyButtonPreset threads dry_run into the body when requested", async ()
101
103
  const options = client.calls[0].options;
102
104
  assert.equal(options.body.dry_run, true);
103
105
  });
106
+ // ------------------------------------------------------------------
107
+ // heading-font apply-mode — mocked only.
108
+ // Mirrors the button apply-mode coverage: capability gate first, then a
109
+ // single POST to /preset/create with the canonical body. pattern_variant
110
+ // is in-memory only and must NOT appear in the wire body.
111
+ // ------------------------------------------------------------------
112
+ test("applyHeadingFontPreset gates capability BEFORE issuing the write", async () => {
113
+ const client = mockClient({ capabilities: {} });
114
+ const entry = emitHeadingFontGroupPreset({ name: "H1", pattern: "google", family: "Inter", weight: "700" }, registry);
115
+ await assert.rejects(() => applyHeadingFontPreset(client, entry, {
116
+ serverVersion: TEST_SERVER_VERSION,
117
+ }), CapabilityMissingError);
118
+ assert.equal(client.calls.length, 0, "no write issued when capability is absent");
119
+ });
120
+ test("applyHeadingFontPreset posts to /preset/create with the canonical body", async () => {
121
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
122
+ const entry = emitHeadingFontGroupPreset({
123
+ name: "H1",
124
+ pattern: "google",
125
+ family: "Inter",
126
+ weight: "700",
127
+ color: "gcid-heading-color",
128
+ size: "48px",
129
+ }, registry);
130
+ const result = await applyHeadingFontPreset(client, entry, {
131
+ serverVersion: TEST_SERVER_VERSION,
132
+ });
133
+ assert.equal(client.handshakeVersions[0], TEST_SERVER_VERSION);
134
+ assert.equal(client.calls.length, 1);
135
+ const call = client.calls[0];
136
+ assert.equal(call.endpoint, PRESET_CREATE_ROUTE);
137
+ const options = call.options;
138
+ assert.equal(options.method, "POST");
139
+ assert.equal(options.body.type, "group");
140
+ assert.equal(options.body.module_name, "divi/heading");
141
+ assert.equal(options.body.group_name, "divi/font");
142
+ assert.equal(options.body.group_id, "designTitleText");
143
+ assert.equal(options.body.name, "H1");
144
+ assert.deepEqual(options.body.attrs, entry.attrs);
145
+ assert.equal("pattern_variant" in options.body, false, "pattern_variant is client-side gating metadata; it must not be on the wire");
146
+ assert.equal("dry_run" in options.body, false);
147
+ assert.deepEqual(result, { ok: true, data: { preset_id: "mocked123" } });
148
+ });
149
+ test("applyHeadingFontPreset threads dry_run into the body when requested", async () => {
150
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
151
+ const entry = emitHeadingFontGroupPreset({ name: "H1", pattern: "local", family: "Sora 700" }, registry);
152
+ await applyHeadingFontPreset(client, entry, {
153
+ serverVersion: TEST_SERVER_VERSION,
154
+ dry_run: true,
155
+ });
156
+ const options = client.calls[0].options;
157
+ assert.equal(options.body.dry_run, true);
158
+ });
159
+ // ------------------------------------------------------------------
160
+ // text-body-font apply-mode — mocked only (Track 6).
161
+ // Mirrors the heading-font apply-mode coverage. pattern_variant is
162
+ // in-memory metadata and must NOT appear in the wire body. Pattern B is
163
+ // refused at the emitter level (registry-absence) — apply-mode is never
164
+ // reached for `--pattern local`, so it has no mocked test here.
165
+ // ------------------------------------------------------------------
166
+ test("applyTextBodyFontPreset gates capability BEFORE issuing the write", async () => {
167
+ const client = mockClient({ capabilities: {} });
168
+ const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter", weight: "400" }, registry);
169
+ await assert.rejects(() => applyTextBodyFontPreset(client, entry, {
170
+ serverVersion: TEST_SERVER_VERSION,
171
+ }), CapabilityMissingError);
172
+ assert.equal(client.calls.length, 0, "no write issued when capability is absent");
173
+ });
174
+ test("applyTextBodyFontPreset posts to /preset/create with the canonical body", async () => {
175
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
176
+ const entry = emitTextBodyFontGroupPreset({
177
+ name: "Body",
178
+ pattern: "google",
179
+ family: "Inter",
180
+ weight: "400",
181
+ color: "gcid-body-color",
182
+ size: "16px",
183
+ }, registry);
184
+ const result = await applyTextBodyFontPreset(client, entry, {
185
+ serverVersion: TEST_SERVER_VERSION,
186
+ });
187
+ assert.equal(client.handshakeVersions[0], TEST_SERVER_VERSION);
188
+ assert.equal(client.calls.length, 1);
189
+ const call = client.calls[0];
190
+ assert.equal(call.endpoint, PRESET_CREATE_ROUTE);
191
+ const options = call.options;
192
+ assert.equal(options.method, "POST");
193
+ assert.equal(options.body.type, "group");
194
+ assert.equal(options.body.module_name, "divi/text");
195
+ assert.equal(options.body.group_name, "divi/font-body");
196
+ assert.equal(options.body.group_id, "designText");
197
+ assert.equal(options.body.name, "Body");
198
+ assert.deepEqual(options.body.attrs, entry.attrs);
199
+ assert.equal("pattern_variant" in options.body, false, "pattern_variant is client-side gating metadata; it must not be on the wire");
200
+ assert.equal("dry_run" in options.body, false);
201
+ assert.deepEqual(result, { ok: true, data: { preset_id: "mocked123" } });
202
+ });
203
+ test("applyTextBodyFontPreset threads dry_run into the body when requested", async () => {
204
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
205
+ const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter" }, registry);
206
+ await applyTextBodyFontPreset(client, entry, {
207
+ serverVersion: TEST_SERVER_VERSION,
208
+ dry_run: true,
209
+ });
210
+ const options = client.calls[0].options;
211
+ assert.equal(options.body.dry_run, true);
212
+ });
104
213
  test("buildClientFromEnv throws CredentialsMissingError when env vars are absent", () => {
105
214
  assert.throws(() => buildClientFromEnv({}), (err) => {
106
215
  assert.ok(err instanceof CredentialsMissingError);
@@ -23,6 +23,8 @@
23
23
  * 4 write / network error
24
24
  */
25
25
  import { type ButtonEmitterInput } from "./button-emitter.js";
26
+ import { type HeadingFontEmitterInput } from "./heading-font-emitter.js";
27
+ import { type TextBodyFontEmitterInput } from "./text-body-font-emitter.js";
26
28
  export declare const EXIT: {
27
29
  readonly OK: 0;
28
30
  readonly INVALID_INPUT: 1;
@@ -50,6 +52,10 @@ export declare class UsageError extends Error {
50
52
  }
51
53
  /** Map parsed `button` options into the emitter input shape. */
52
54
  export declare function buildButtonInput(parsed: ParsedArgs): ButtonEmitterInput;
55
+ /** Map parsed `heading-font` options into the heading-font emitter input shape. */
56
+ export declare function buildHeadingFontInput(parsed: ParsedArgs): HeadingFontEmitterInput;
57
+ /** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
58
+ export declare function buildTextBodyFontInput(parsed: ParsedArgs): TextBodyFontEmitterInput;
53
59
  /**
54
60
  * Run the CLI. Returns the structured exit code (does NOT call
55
61
  * `process.exit` — the thin bin wrapper does). `io` is injectable so