@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.
- package/README.md +20 -2
- package/dist/preset-cli/__tests__/cli.test.js +385 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +12 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.js +249 -0
- package/dist/preset-cli/__tests__/registry.test.js +41 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.d.ts +14 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.js +191 -0
- package/dist/preset-cli/__tests__/write-path.test.js +110 -1
- package/dist/preset-cli/cli.d.ts +6 -0
- package/dist/preset-cli/cli.js +198 -11
- package/dist/preset-cli/heading-font-emitter.d.ts +128 -0
- package/dist/preset-cli/heading-font-emitter.js +166 -0
- package/dist/preset-cli/registry.d.ts +23 -9
- package/dist/preset-cli/registry.js +37 -13
- package/dist/preset-cli/text-body-font-emitter.d.ts +127 -0
- package/dist/preset-cli/text-body-font-emitter.js +169 -0
- package/dist/preset-cli/write-path.d.ts +31 -0
- package/dist/preset-cli/write-path.js +43 -0
- package/package.json +1 -1
|
@@ -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);
|
package/dist/preset-cli/cli.d.ts
CHANGED
|
@@ -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
|