@diviops/mcp-server 1.5.14 → 1.5.17
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.
- package/README.md +10 -1
- package/data/verified-attrs-backlog.json +5 -5
- package/data/verified-attrs.json +8 -8
- package/dist/compatibility.d.ts +25 -0
- package/dist/index.js +349 -4
- package/dist/preset-cli/button-emitter.d.ts +1 -1
- package/dist/preset-cli/button-emitter.js +1 -1
- package/dist/preset-cli/cli.d.ts +4 -3
- package/dist/preset-cli/cli.js +11 -10
- package/dist/preset-cli/heading-font-emitter.d.ts +1 -1
- package/dist/preset-cli/heading-font-emitter.js +3 -3
- package/dist/preset-cli/spacing-emitter.d.ts +13 -13
- package/dist/preset-cli/spacing-emitter.js +23 -23
- package/dist/preset-cli/write-path.d.ts +3 -3
- package/dist/preset-cli/write-path.js +3 -3
- package/dist/wp-cli-fs-validator.d.ts +1 -1
- package/dist/wp-cli-fs-validator.js +1 -1
- package/dist/wp-cli.js +1 -2
- package/dist/wp-client.js +17 -0
- package/package.json +3 -2
- package/dist/preset-cli/__tests__/button-emitter.test.d.ts +0 -8
- package/dist/preset-cli/__tests__/button-emitter.test.js +0 -188
- package/dist/preset-cli/__tests__/cli.test.d.ts +0 -9
- package/dist/preset-cli/__tests__/cli.test.js +0 -791
- package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +0 -12
- package/dist/preset-cli/__tests__/heading-font-emitter.test.js +0 -249
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.d.ts +0 -13
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.js +0 -64
- package/dist/preset-cli/__tests__/registry.test.d.ts +0 -5
- package/dist/preset-cli/__tests__/registry.test.js +0 -175
- package/dist/preset-cli/__tests__/spacing-emitter.test.d.ts +0 -20
- package/dist/preset-cli/__tests__/spacing-emitter.test.js +0 -409
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.d.ts +0 -14
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.js +0 -191
- package/dist/preset-cli/__tests__/write-path.test.d.ts +0 -8
- package/dist/preset-cli/__tests__/write-path.test.js +0 -287
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `divi/spacing` section emitter shape + gating coverage (Track 7b).
|
|
3
|
-
*
|
|
4
|
-
* Pins the load-bearing shape contract from the Track 7a canonical
|
|
5
|
-
* capture (`round-5-spacing-section.json`):
|
|
6
|
-
* - Sparse-emit per axis (only user-touched corners emit; others absent);
|
|
7
|
-
* - Paired sync flags per axis (BOTH `syncVertical` AND `syncHorizontal`
|
|
8
|
-
* emit when an axis is touched; default `"off"` unless explicitly set);
|
|
9
|
-
* - Padding and margin are INDEPENDENT bags (passing only padding flags
|
|
10
|
-
* omits the margin bag entirely, and vice versa);
|
|
11
|
-
* - `groupId: "designSpacing"` + `primaryAttrName: "module"` baked in
|
|
12
|
-
* (NOT inferred from a dotted attr path — Track 7a load-bearing finding);
|
|
13
|
-
* - `attrs`-only entry + request body (no `styleAttrs` / `renderAttrs` —
|
|
14
|
-
* mirror happens at the route layer per Tracks 4/5/6 contract);
|
|
15
|
-
* - Variable-token rejection on length flags (`$variable(...)` and bare
|
|
16
|
-
* `gvid-*` / `gcid-*` forms) — deferred until canonical capture lands;
|
|
17
|
-
* - Registry gate refusal for every non-section module (heading, text,
|
|
18
|
-
* button — all SCHEMA_OBSERVED today).
|
|
19
|
-
*/
|
|
20
|
-
import { test } from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { readFileSync } from "node:fs";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
25
|
-
import { emitSpacingGroupPreset, composeSpacingAttrs, buildSpacingPresetCreateBody, SPACING_GROUP_NAME, SPACING_GROUP_ID, SPACING_PRIMARY_ATTR_NAME, } from "../spacing-emitter.js";
|
|
26
|
-
import { EvidenceGateError, loadRegistry, } from "../registry.js";
|
|
27
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const REPO_ROOT = join(__dirname, "..", "..", "..", "..");
|
|
29
|
-
const FIXTURE_DIR = join(REPO_ROOT, "docs/verification/evidence/canonical-shape-dumps-2026-05-23");
|
|
30
|
-
const FIXTURE_ROUND_5 = join(FIXTURE_DIR, "round-5-spacing-section.json");
|
|
31
|
-
const registry = loadRegistry();
|
|
32
|
-
test("emitter byte-matches round-5 (Track 7a) padding capture attrs", () => {
|
|
33
|
-
const fixture = JSON.parse(readFileSync(FIXTURE_ROUND_5, "utf8"));
|
|
34
|
-
const canonicalAttrs = fixture.preset_entry.attrs;
|
|
35
|
-
const entry = emitSpacingGroupPreset({
|
|
36
|
-
name: "Track 7a Section Spacing Capture v2",
|
|
37
|
-
module: "divi/section",
|
|
38
|
-
padding: { top: "40px" },
|
|
39
|
-
}, registry);
|
|
40
|
-
assert.deepEqual(entry.attrs, canonicalAttrs, "emitted attrs must byte-match the Track 7a padding canonical capture");
|
|
41
|
-
assert.equal(entry.type, "group");
|
|
42
|
-
assert.equal(entry.group_name, SPACING_GROUP_NAME);
|
|
43
|
-
assert.equal(entry.group_id, SPACING_GROUP_ID);
|
|
44
|
-
assert.equal(entry.primary_attr_name, SPACING_PRIMARY_ATTR_NAME);
|
|
45
|
-
assert.equal(entry.module_name, "divi/section");
|
|
46
|
-
});
|
|
47
|
-
test("emitter byte-matches round-5 (Track 7a) margin capture attrs", () => {
|
|
48
|
-
const fixture = JSON.parse(readFileSync(FIXTURE_ROUND_5, "utf8"));
|
|
49
|
-
const canonicalMarginAttrs = fixture.preset_entry_margin.attrs;
|
|
50
|
-
const entry = emitSpacingGroupPreset({
|
|
51
|
-
name: "Track 7a Section Margin Capture",
|
|
52
|
-
module: "divi/section",
|
|
53
|
-
margin: { top: "30px" },
|
|
54
|
-
}, registry);
|
|
55
|
-
assert.deepEqual(entry.attrs, canonicalMarginAttrs, "emitted attrs must byte-match the Track 7a margin canonical capture");
|
|
56
|
-
});
|
|
57
|
-
test("padding-only single corner: sparse-emit + paired sync flags, no margin bag", () => {
|
|
58
|
-
const entry = emitSpacingGroupPreset({ name: "P", module: "divi/section", padding: { top: "40px" } }, registry);
|
|
59
|
-
const value = entry.attrs.module.decoration.spacing.desktop.value;
|
|
60
|
-
assert.deepEqual(Object.keys(value), ["padding"], "no margin bag emitted");
|
|
61
|
-
assert.deepEqual(value.padding, { top: "40px", syncVertical: "off", syncHorizontal: "off" }, "single-corner padding emits exactly that corner + paired off sync flags");
|
|
62
|
-
});
|
|
63
|
-
test("margin-only single corner: sparse-emit + paired sync flags, no padding bag", () => {
|
|
64
|
-
const entry = emitSpacingGroupPreset({ name: "M", module: "divi/section", margin: { bottom: "80px" } }, registry);
|
|
65
|
-
const value = entry.attrs.module.decoration.spacing.desktop.value;
|
|
66
|
-
assert.deepEqual(Object.keys(value), ["margin"], "no padding bag emitted");
|
|
67
|
-
assert.deepEqual(value.margin, { bottom: "80px", syncVertical: "off", syncHorizontal: "off" });
|
|
68
|
-
});
|
|
69
|
-
test("padding + margin combined: both bags, each with its own paired sync flags", () => {
|
|
70
|
-
const entry = emitSpacingGroupPreset({
|
|
71
|
-
name: "Test",
|
|
72
|
-
module: "divi/section",
|
|
73
|
-
padding: { top: "40px" },
|
|
74
|
-
margin: { bottom: "80px" },
|
|
75
|
-
}, registry);
|
|
76
|
-
const value = entry.attrs.module.decoration.spacing.desktop.value;
|
|
77
|
-
assert.deepEqual(value.padding, { top: "40px", syncVertical: "off", syncHorizontal: "off" });
|
|
78
|
-
assert.deepEqual(value.margin, { bottom: "80px", syncVertical: "off", syncHorizontal: "off" });
|
|
79
|
-
});
|
|
80
|
-
test("all four corners of one axis: sparse-emit holds (all four present)", () => {
|
|
81
|
-
const entry = emitSpacingGroupPreset({
|
|
82
|
-
name: "AllCorners",
|
|
83
|
-
module: "divi/section",
|
|
84
|
-
padding: {
|
|
85
|
-
top: "10px",
|
|
86
|
-
right: "20px",
|
|
87
|
-
bottom: "30px",
|
|
88
|
-
left: "40px",
|
|
89
|
-
},
|
|
90
|
-
}, registry);
|
|
91
|
-
const padding = entry.attrs.module.decoration.spacing.desktop.value
|
|
92
|
-
.padding;
|
|
93
|
-
assert.deepEqual(padding, {
|
|
94
|
-
top: "10px",
|
|
95
|
-
right: "20px",
|
|
96
|
-
bottom: "30px",
|
|
97
|
-
left: "40px",
|
|
98
|
-
syncVertical: "off",
|
|
99
|
-
syncHorizontal: "off",
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
test("caller-passed sync-flag values are honored verbatim", () => {
|
|
103
|
-
const entry = emitSpacingGroupPreset({
|
|
104
|
-
name: "Sync",
|
|
105
|
-
module: "divi/section",
|
|
106
|
-
padding: {
|
|
107
|
-
top: "40px",
|
|
108
|
-
bottom: "40px",
|
|
109
|
-
syncVertical: "on",
|
|
110
|
-
syncHorizontal: "off",
|
|
111
|
-
},
|
|
112
|
-
}, registry);
|
|
113
|
-
const padding = entry.attrs.module.decoration.spacing.desktop.value
|
|
114
|
-
.padding;
|
|
115
|
-
assert.equal(padding.syncVertical, "on");
|
|
116
|
-
assert.equal(padding.syncHorizontal, "off");
|
|
117
|
-
});
|
|
118
|
-
test("entry carries no renderAttrs / styleAttrs key (mirror happens at route layer)", () => {
|
|
119
|
-
const entry = emitSpacingGroupPreset({ name: "R", module: "divi/section", padding: { top: "40px" } }, registry);
|
|
120
|
-
assert.equal("renderAttrs" in entry, false, "renderAttrs must NOT appear on the in-memory entry — the plugin's " +
|
|
121
|
-
"/preset/create route mirrors attrs into styleAttrs/renderAttrs at " +
|
|
122
|
-
"the write layer per Tracks 4/5/6 contract");
|
|
123
|
-
assert.equal("styleAttrs" in entry, false, "same for styleAttrs");
|
|
124
|
-
});
|
|
125
|
-
test("request body carries no renderAttrs / styleAttrs key either", () => {
|
|
126
|
-
const entry = emitSpacingGroupPreset({ name: "R", module: "divi/section", padding: { top: "40px" } }, registry);
|
|
127
|
-
const body = buildSpacingPresetCreateBody(entry);
|
|
128
|
-
assert.equal("renderAttrs" in body, false);
|
|
129
|
-
assert.equal("styleAttrs" in body, false);
|
|
130
|
-
});
|
|
131
|
-
test("compose: omitted corners produce no keys (no pre-populate)", () => {
|
|
132
|
-
const { value } = composeSpacingAttrs({
|
|
133
|
-
name: "x",
|
|
134
|
-
module: "divi/section",
|
|
135
|
-
padding: { top: "40px" },
|
|
136
|
-
});
|
|
137
|
-
const padding = value.padding;
|
|
138
|
-
assert.deepEqual(Object.keys(padding).sort(), ["syncHorizontal", "syncVertical", "top"], "only `top` + the two paired sync flags — right/bottom/left absent");
|
|
139
|
-
assert.equal("right" in padding, false);
|
|
140
|
-
assert.equal("bottom" in padding, false);
|
|
141
|
-
assert.equal("left" in padding, false);
|
|
142
|
-
});
|
|
143
|
-
test("empty preset is refused (no padding or margin corners)", () => {
|
|
144
|
-
assert.throws(() => emitSpacingGroupPreset({ name: "Empty", module: "divi/section" }, registry), /empty preset/);
|
|
145
|
-
});
|
|
146
|
-
test("missing name is refused", () => {
|
|
147
|
-
assert.throws(() => emitSpacingGroupPreset({ name: "", module: "divi/section", padding: { top: "40px" } }, registry), /non-empty `name`/);
|
|
148
|
-
});
|
|
149
|
-
test("missing module is refused", () => {
|
|
150
|
-
assert.throws(() => emitSpacingGroupPreset({ name: "X", module: "", padding: { top: "40px" } }, registry), /requires `module`/);
|
|
151
|
-
});
|
|
152
|
-
test("variable-token in padding length: $variable(...) form is refused", () => {
|
|
153
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
154
|
-
name: "V",
|
|
155
|
-
module: "divi/section",
|
|
156
|
-
padding: {
|
|
157
|
-
top: '$variable({"type":"length","value":{"name":"gvid-space-1","settings":{}}})$',
|
|
158
|
-
},
|
|
159
|
-
}, registry), /Variable-token support deferred/);
|
|
160
|
-
});
|
|
161
|
-
test("variable-token in padding length: bare gvid-* form is refused", () => {
|
|
162
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
163
|
-
name: "V",
|
|
164
|
-
module: "divi/section",
|
|
165
|
-
padding: { top: "gvid-space-1" },
|
|
166
|
-
}, registry), /Variable-token support deferred/);
|
|
167
|
-
});
|
|
168
|
-
test("variable-token in margin length: bare gvid-* form is refused", () => {
|
|
169
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
170
|
-
name: "V",
|
|
171
|
-
module: "divi/section",
|
|
172
|
-
margin: { bottom: "gvid-space-2" },
|
|
173
|
-
}, registry), /Variable-token support deferred/);
|
|
174
|
-
});
|
|
175
|
-
test("registry gate: divi/heading is refused (SCHEMA_OBSERVED cell)", () => {
|
|
176
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
177
|
-
name: "H",
|
|
178
|
-
module: "divi/heading",
|
|
179
|
-
padding: { top: "40px" },
|
|
180
|
-
}, registry), (err) => {
|
|
181
|
-
assert.ok(err instanceof EvidenceGateError, "must throw EvidenceGateError, not a plain Error");
|
|
182
|
-
assert.match(err.message, /divi\/heading/);
|
|
183
|
-
assert.match(err.message, /divi\/spacing/);
|
|
184
|
-
return true;
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
test("registry gate: divi/text is refused (SCHEMA_OBSERVED cell)", () => {
|
|
188
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
189
|
-
name: "T",
|
|
190
|
-
module: "divi/text",
|
|
191
|
-
padding: { top: "40px" },
|
|
192
|
-
}, registry), EvidenceGateError);
|
|
193
|
-
});
|
|
194
|
-
test("registry gate: divi/button is refused (SCHEMA_OBSERVED cell)", () => {
|
|
195
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
196
|
-
name: "B",
|
|
197
|
-
module: "divi/button",
|
|
198
|
-
padding: { top: "40px" },
|
|
199
|
-
}, registry), EvidenceGateError);
|
|
200
|
-
});
|
|
201
|
-
test("registry gate: divi/row is refused (no applicability cell → UNVERIFIED)", () => {
|
|
202
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
203
|
-
name: "R",
|
|
204
|
-
module: "divi/row",
|
|
205
|
-
padding: { top: "40px" },
|
|
206
|
-
}, registry), EvidenceGateError);
|
|
207
|
-
});
|
|
208
|
-
test("registry gate: divi/column is refused (no applicability cell → UNVERIFIED)", () => {
|
|
209
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
210
|
-
name: "C",
|
|
211
|
-
module: "divi/column",
|
|
212
|
-
padding: { top: "40px" },
|
|
213
|
-
}, registry), EvidenceGateError);
|
|
214
|
-
});
|
|
215
|
-
test("registry gate: arbitrary unknown module is refused", () => {
|
|
216
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
217
|
-
name: "U",
|
|
218
|
-
module: "divi/not-a-real-module",
|
|
219
|
-
padding: { top: "40px" },
|
|
220
|
-
}, registry), EvidenceGateError);
|
|
221
|
-
});
|
|
222
|
-
test("real registry: divi/section on divi/spacing clears write threshold (canary)", () => {
|
|
223
|
-
// Sanity guard: if a registry edit ever drops the (divi/spacing,
|
|
224
|
-
// divi/section) cell below VB_PRESET_STORAGE_VERIFIED (4), this test is
|
|
225
|
-
// the canary.
|
|
226
|
-
const entry = emitSpacingGroupPreset({
|
|
227
|
-
name: "canary",
|
|
228
|
-
module: "divi/section",
|
|
229
|
-
padding: { top: "40px" },
|
|
230
|
-
}, registry);
|
|
231
|
-
assert.equal(entry.group_id, SPACING_GROUP_ID);
|
|
232
|
-
assert.equal(entry.primary_attr_name, SPACING_PRIMARY_ATTR_NAME);
|
|
233
|
-
});
|
|
234
|
-
test("buildSpacingPresetCreateBody mirrors the diviops_preset_create body shape", () => {
|
|
235
|
-
const entry = emitSpacingGroupPreset({
|
|
236
|
-
name: "Body",
|
|
237
|
-
module: "divi/section",
|
|
238
|
-
padding: { top: "40px" },
|
|
239
|
-
}, registry);
|
|
240
|
-
const body = buildSpacingPresetCreateBody(entry, { dry_run: true });
|
|
241
|
-
assert.deepEqual(body, {
|
|
242
|
-
module_name: "divi/section",
|
|
243
|
-
name: "Body",
|
|
244
|
-
attrs: entry.attrs,
|
|
245
|
-
type: "group",
|
|
246
|
-
group_name: SPACING_GROUP_NAME,
|
|
247
|
-
group_id: SPACING_GROUP_ID,
|
|
248
|
-
primary_attr_name: SPACING_PRIMARY_ATTR_NAME,
|
|
249
|
-
dry_run: true,
|
|
250
|
-
});
|
|
251
|
-
const noDry = buildSpacingPresetCreateBody(entry);
|
|
252
|
-
assert.equal("dry_run" in noDry, false);
|
|
253
|
-
});
|
|
254
|
-
test("padding axis with sync flags but no corners is refused explicitly", () => {
|
|
255
|
-
// Sync-flag-only input on an axis with no touched corner is a usage
|
|
256
|
-
// error: there is no corner to anchor the sync flags to. The emitter
|
|
257
|
-
// refuses BEFORE the empty-preset check so the operator gets a precise
|
|
258
|
-
// error pointing at the axis, not the generic "empty preset" message.
|
|
259
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
260
|
-
name: "SyncOnly",
|
|
261
|
-
module: "divi/section",
|
|
262
|
-
padding: { syncVertical: "on", syncHorizontal: "off" },
|
|
263
|
-
}, registry), /--padding-sync-vertical.*require at least one --padding-\{top,right,bottom,left\}/s);
|
|
264
|
-
});
|
|
265
|
-
test("margin sync flag with no margin corner is refused even when padding has corners", () => {
|
|
266
|
-
// Cross-axis silent-no-op guard (Copilot finding on PR #754): passing
|
|
267
|
-
// --margin-sync-vertical on alongside --padding-top 40px previously
|
|
268
|
-
// silently dropped the margin sync flag (composeAxis returned undefined
|
|
269
|
-
// for margin, and the padding emission succeeded). The refusal now
|
|
270
|
-
// fires per-axis so the operator catches the misconfiguration.
|
|
271
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
272
|
-
name: "CrossAxis",
|
|
273
|
-
module: "divi/section",
|
|
274
|
-
padding: { top: "40px" },
|
|
275
|
-
margin: { syncVertical: "on" },
|
|
276
|
-
}, registry), /--margin-sync-vertical.*require at least one --margin-/s);
|
|
277
|
-
});
|
|
278
|
-
test("padding sync flag with no padding corner is refused even when margin has corners", () => {
|
|
279
|
-
// Symmetric to the previous test — padding sync flag with no padding
|
|
280
|
-
// corner must refuse even when the margin axis is fully specified.
|
|
281
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
282
|
-
name: "CrossAxisInverse",
|
|
283
|
-
module: "divi/section",
|
|
284
|
-
padding: { syncHorizontal: "on" },
|
|
285
|
-
margin: { bottom: "40px" },
|
|
286
|
-
}, registry), /--padding-sync-horizontal.*require at least one --padding-/s);
|
|
287
|
-
});
|
|
288
|
-
test("variable-token rejection still works on leading-whitespace value", () => {
|
|
289
|
-
// Gemini finding on PR #754: the previous startsWith / regex anchor
|
|
290
|
-
// could be bypassed with leading whitespace (e.g. " gvid-space-1").
|
|
291
|
-
// The validator now trims before pattern-matching.
|
|
292
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
293
|
-
name: "Whitespace",
|
|
294
|
-
module: "divi/section",
|
|
295
|
-
padding: { top: " gvid-space-1" },
|
|
296
|
-
}, registry), /Variable-token support deferred/);
|
|
297
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
298
|
-
name: "Whitespace2",
|
|
299
|
-
module: "divi/section",
|
|
300
|
-
padding: { top: " $variable({\"x\":1})$" },
|
|
301
|
-
}, registry), /Variable-token support deferred/);
|
|
302
|
-
});
|
|
303
|
-
// ------------------------------------------------------------------
|
|
304
|
-
// Literal CSS length grammar — codex re-review finding on PR #754.
|
|
305
|
-
// v1 accepts only `<number><unit>` where unit is one of px / rem / em /
|
|
306
|
-
// % / vw / vh. Free-form strings, var(), calc(), and other unverified
|
|
307
|
-
// grammars are refused before emission.
|
|
308
|
-
// ------------------------------------------------------------------
|
|
309
|
-
test("rejects free-form non-length string (e.g. 'banana')", () => {
|
|
310
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
311
|
-
name: "B",
|
|
312
|
-
module: "divi/section",
|
|
313
|
-
padding: { top: "banana" },
|
|
314
|
-
}, registry), /not a literal CSS length/);
|
|
315
|
-
});
|
|
316
|
-
test("rejects var(--x) — unverified grammar in v1", () => {
|
|
317
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
318
|
-
name: "V",
|
|
319
|
-
module: "divi/section",
|
|
320
|
-
padding: { top: "var(--space-1)" },
|
|
321
|
-
}, registry), /not a literal CSS length/);
|
|
322
|
-
});
|
|
323
|
-
test("rejects calc() — unverified grammar in v1", () => {
|
|
324
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
325
|
-
name: "C",
|
|
326
|
-
module: "divi/section",
|
|
327
|
-
margin: { bottom: "calc(8px + 2vw)" },
|
|
328
|
-
}, registry), /not a literal CSS length/);
|
|
329
|
-
});
|
|
330
|
-
test("rejects bare numeric without unit (e.g. '40')", () => {
|
|
331
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
332
|
-
name: "N",
|
|
333
|
-
module: "divi/section",
|
|
334
|
-
padding: { top: "40" },
|
|
335
|
-
}, registry), /not a literal CSS length/);
|
|
336
|
-
});
|
|
337
|
-
test("rejects unsupported unit (e.g. 'pt')", () => {
|
|
338
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
339
|
-
name: "U",
|
|
340
|
-
module: "divi/section",
|
|
341
|
-
padding: { top: "40pt" },
|
|
342
|
-
}, registry), /not a literal CSS length/);
|
|
343
|
-
});
|
|
344
|
-
test("accepts the full v1 unit set (px / rem / em / % / vw / vh)", () => {
|
|
345
|
-
// Smoke: ensure the validator is positive — each supported unit + a
|
|
346
|
-
// representative numeric form lands in attrs as-emitted.
|
|
347
|
-
for (const v of ["40px", "1.5rem", "2em", "100%", "10vw", "5.25vh", "0px"]) {
|
|
348
|
-
const entry = emitSpacingGroupPreset({ name: "v", module: "divi/section", padding: { top: v } }, registry);
|
|
349
|
-
const padding = entry.attrs.module.decoration.spacing.desktop
|
|
350
|
-
.value.padding;
|
|
351
|
-
assert.equal(padding.top, v, `expected ${v} to pass and be emitted verbatim`);
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
// ------------------------------------------------------------------
|
|
355
|
-
// SPACING_SUPPORTED_MODULES second guard — codex re-review finding on
|
|
356
|
-
// PR #754. Even if a future registry-only PR promotes another cell to
|
|
357
|
-
// VB_PRESET_STORAGE_VERIFIED, this emitter must NOT silently start
|
|
358
|
-
// emitting it — heading/text/button cells need their own per-module
|
|
359
|
-
// wrapper (today the emitter hard-codes wrapper="module").
|
|
360
|
-
// ------------------------------------------------------------------
|
|
361
|
-
function buildElevatedRegistryForHeading() {
|
|
362
|
-
// Clone the real registry and elevate the divi/heading cell on the
|
|
363
|
-
// divi/spacing pattern to VB_PRESET_STORAGE_VERIFIED. This simulates a
|
|
364
|
-
// future registry-only promotion landing without the corresponding
|
|
365
|
-
// implementation PR.
|
|
366
|
-
const real = loadRegistry();
|
|
367
|
-
const cloned = JSON.parse(JSON.stringify(real));
|
|
368
|
-
const spacing = (cloned.tier1 ?? []).find((e) => e.pattern_family === "divi/spacing") ?? (cloned.tier2 ?? []).find((e) => e.pattern_family === "divi/spacing");
|
|
369
|
-
if (!spacing || !spacing.applicability) {
|
|
370
|
-
throw new Error("fixture precondition failed: registry has no divi/spacing pattern entry");
|
|
371
|
-
}
|
|
372
|
-
const cell = spacing.applicability["divi/heading"];
|
|
373
|
-
if (!cell) {
|
|
374
|
-
throw new Error("fixture precondition failed: divi/spacing pattern has no divi/heading cell");
|
|
375
|
-
}
|
|
376
|
-
cell.cell_evidence_level = "VB_PRESET_STORAGE_VERIFIED";
|
|
377
|
-
return cloned;
|
|
378
|
-
}
|
|
379
|
-
test("divi/heading is refused by SPACING_SUPPORTED_MODULES guard even if registry elevates the cell", () => {
|
|
380
|
-
// Codex finding on PR #754: registry-only promotion must not bypass
|
|
381
|
-
// implementation. The constant `SPACING_SUPPORTED_MODULES` acts as a
|
|
382
|
-
// second guard after `gateWriteAttr`, so a future registry-only PR
|
|
383
|
-
// promoting divi/heading would still land on this refusal until an
|
|
384
|
-
// explicit code change extends the constant.
|
|
385
|
-
const elevated = buildElevatedRegistryForHeading();
|
|
386
|
-
assert.throws(() => emitSpacingGroupPreset({
|
|
387
|
-
name: "H",
|
|
388
|
-
module: "divi/heading",
|
|
389
|
-
padding: { top: "40px" },
|
|
390
|
-
}, elevated), (err) => {
|
|
391
|
-
// NOT an EvidenceGateError — that gate was cleared by the elevated
|
|
392
|
-
// registry. The refusal comes from the implementation-supported-
|
|
393
|
-
// modules guard with a distinct message naming the constant.
|
|
394
|
-
assert.ok(!(err instanceof EvidenceGateError), "with an elevated registry the EvidenceGateError must NOT fire — the implementation guard does");
|
|
395
|
-
assert.match(err.message, /does not yet implement module/, "refusal cites the implementation guard, not the registry gate");
|
|
396
|
-
assert.match(err.message, /divi\/heading/, "refusal names the module the caller asked for");
|
|
397
|
-
assert.match(err.message, /Supported modules: divi\/section/, "refusal names the currently-supported module set");
|
|
398
|
-
return true;
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
test("divi/section is still allowed under the elevated-registry fixture (sanity)", () => {
|
|
402
|
-
// Sanity guard: the registry clone + elevation logic does not break
|
|
403
|
-
// the section path. The section cell stays at VB_PRESET_STORAGE_VERIFIED
|
|
404
|
-
// both before and after the clone, and the implementation guard
|
|
405
|
-
// includes it in SPACING_SUPPORTED_MODULES.
|
|
406
|
-
const elevated = buildElevatedRegistryForHeading();
|
|
407
|
-
const entry = emitSpacingGroupPreset({ name: "S", module: "divi/section", padding: { top: "40px" } }, elevated);
|
|
408
|
-
assert.equal(entry.module_name, "divi/section");
|
|
409
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,191 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Apply-mode coverage — mocked only (no live substrate write, per #725 AC #8).
|
|
3
|
-
*
|
|
4
|
-
* Asserts: the capability gate (storage_multipath_probe_v1 present → proceed,
|
|
5
|
-
* absent → fail fast), the POST route + request body, and credential
|
|
6
|
-
* handling. The HTTP client is a stub; nothing touches the network.
|
|
7
|
-
*/
|
|
8
|
-
export {};
|