@bongos/core 1.19.116 → 1.19.118

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,79 @@
1
+ // tests/design_tokens_sync.mjs — config/design-tokens.neutral.json mirrors the
2
+ // branding pack, never a palette of its own (task 1002752).
3
+ //
4
+ // The repo has two files describing the neutral look: config/branding.neutral.json
5
+ // (the RUNTIME pack — themeMarkup() paints every served page with it) and
6
+ // config/design-tokens.neutral.json (the DESIGN-side tokens feeding the Figma /
7
+ // Claude Design round-trip). Nothing tied them together, so the design file kept
8
+ // the pre-1002749 palette — including the exact accents removed for failing WCAG
9
+ // AA — and every prototype started from colours the product no longer paints.
10
+ // Model: the fallback-equals-pack assertions in tests/devbox_dmg_backdrop.mjs.
11
+ //
12
+ // Run: node tests/design_tokens_sync.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { test } from 'node:test';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const readJson = (...p) => JSON.parse(fs.readFileSync(path.join(REPO_ROOT, ...p), 'utf8'));
22
+
23
+ const pack = readJson('config', 'branding.neutral.json');
24
+ const tokens = readJson('config', 'design-tokens.neutral.json');
25
+
26
+ // Strip comments so rationale prose — which must be free to name a removed
27
+ // colour — never trips a scan of the CODE (the stripCommentsForIdentity
28
+ // contract, as applied in tests/devbox_dmg_backdrop.mjs).
29
+ function codeOnly(text) {
30
+ return text
31
+ .replace(/\/\*[\s\S]*?\*\//g, '')
32
+ .split(/\r?\n/)
33
+ .map((line) => line.replace(/(^|[^:"'])\/\/.*$/, '$1'))
34
+ .join('\n');
35
+ }
36
+
37
+ test('every design-token colour is the branding pack value — a divergent token is a fork of the brand', () => {
38
+ const packColors = pack.theme.ui.colors;
39
+ assert.deepEqual(
40
+ Object.keys(tokens.colors || {}).sort(), Object.keys(packColors).sort(),
41
+ 'the design file must carry exactly the pack colour keys — a missing or extra key is drift too, ' +
42
+ 'running through the key set instead of the values');
43
+ for (const [key, val] of Object.entries(tokens.colors)) {
44
+ assert.equal(String(val).toLowerCase(), String(packColors[key]).toLowerCase(),
45
+ `design-token "${key}" must equal the pack's value — a drifted token hands every prototype ` +
46
+ 'a palette the product no longer paints');
47
+ }
48
+ });
49
+
50
+ test('the fonts the two files share are the same fonts', () => {
51
+ const packFonts = pack.theme.ui.fonts;
52
+ for (const [key, val] of Object.entries(tokens.fonts || {})) {
53
+ if (!(key in packFonts)) continue; // mono is design-side only — the pack declares no mono surface
54
+ assert.deepEqual(val, packFonts[key],
55
+ `design-token font "${key}" must equal the pack's — same drift class as the colours`);
56
+ }
57
+ });
58
+
59
+ test('the claude-design adapter falls back to the pack accent, never a brand literal of its own', () => {
60
+ const source = fs.readFileSync(
61
+ path.join(REPO_ROOT, 'src', 'ui', 'adapters', 'claude-design', 'index.js'), 'utf8');
62
+ const block = source.match(/const FALLBACK_ACCENT =[\s\S]*?\}\)\(\);/);
63
+ assert.ok(block, 'the adapter declares one resolved fallback accent instead of inline literals');
64
+ assert.match(block[0], /design-tokens\.neutral\.json/,
65
+ 'the fallback resolves from the neutral token file, never a bare literal');
66
+ for (const hex of block[0].match(/#[0-9a-fA-F]{6}\b/g) || []) {
67
+ assert.equal(hex.toLowerCase(), String(pack.theme.ui.colors.accent).toLowerCase(),
68
+ 'the catch literal must be the pack ACCENT, not merely some pack colour');
69
+ }
70
+ // Every colour literal left in the adapter must be the neutral pack's own value
71
+ // (i.e. a fallback, not a brand). The preview-card chrome is 3-digit grays, so
72
+ // a 6-digit literal is exactly the brand-value class.
73
+ const packValues = new Set(Object.values(pack.theme.ui.colors).map((h) => String(h).toLowerCase()));
74
+ const hexes = codeOnly(source).match(/#[0-9a-fA-F]{6}\b/g) || [];
75
+ for (const hex of hexes) {
76
+ assert.ok(packValues.has(hex.toLowerCase()),
77
+ `hardcoded colour ${hex} in the claude-design adapter — resolve it from the tokens instead`);
78
+ }
79
+ });
@@ -228,11 +228,37 @@ test('/create is absorbed — the wizard lives here and the route only redirects
228
228
  const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
229
229
  assert.match(server, /res\.redirect\(302, '\/projects\?view=new'/,
230
230
  '/create must 302 into the hub (302, not 301: the URL is an OAuth return target in the wild)');
231
- for (const id of ['panel-gate', 'panel-name', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
231
+ for (const id of ['panel-gate', 'panel-name', 'panel-type', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
232
232
  assert.match(HUB, new RegExp(`id="${id}"`), `the wizard's ${id} ships as real markup (C7)`);
233
233
  }
234
234
  });
235
235
 
236
+ test('the wizard step indices stay coupled — data-next lands one past its own panel (task 1002772)', () => {
237
+ // The whole renumbering turns on validateStep(next - 1): a form's data-next
238
+ // must sit inside panels[next - 1], or the validator guards the wrong step.
239
+ const pm = SCRIPT.match(/var panels = \{([^}]*)\}/);
240
+ assert.ok(pm, 'the panels map must exist');
241
+ const map = {};
242
+ for (const m of pm[1].matchAll(/(\d+):\s*'([a-z-]+)'/g)) map[Number(m[1])] = m[2];
243
+ const nexts = [...HUB.matchAll(/data-next="(\d+)"/g)];
244
+ assert.ok(nexts.length >= 4, 'every wizard form declares its next step');
245
+ for (const m of nexts) {
246
+ const before = HUB.slice(0, m.index);
247
+ const sec = before.lastIndexOf('id="panel-');
248
+ assert.ok(sec !== -1, 'a data-next form lives inside a panel');
249
+ const id = HUB.slice(sec + 4).match(/^(panel-[a-z]+)/)[1];
250
+ assert.equal(id, map[Number(m[1]) - 1],
251
+ `data-next="${m[1]}" must live in panels[${Number(m[1]) - 1}] — validateStep(next-1) guards that panel`);
252
+ }
253
+ for (const m of HUB.matchAll(/data-back="(\d+)"/g)) {
254
+ assert.ok(map[Number(m[1])], `data-back="${m[1]}" must point at a mapped panel`);
255
+ }
256
+ // the rail numbers every step except the gate and the done panel
257
+ const beats = [...HUB.matchAll(/data-step="(\d+)"/g)].map((x) => Number(x[1]));
258
+ assert.equal(Math.max.apply(null, beats), Object.keys(map).length - 2,
259
+ 'the rail shows every numbered step except gate and done');
260
+ });
261
+
236
262
  test('the absorbed wizard sheds its two URL landmines — /gate/ and the whole-query strip', () => {
237
263
  // create.html carried a document-level Escape that navigated to the retired
238
264
  // /gate/ (a 404 the href-only guard above cannot see: it was a
@@ -475,18 +501,21 @@ test('the row keeps its own Open link working underneath the card', () => {
475
501
  The mock's columns shipped whole — Project / Builders / Type / Tags / Status
476
502
  — but three of them rendered an em dash for every row, forever, because the
477
503
  public projection carries four fields and Type/Tags are not in the catalog
478
- schema at all. A header promising a column is a claim like any other. ── */
504
+ schema at all. A header promising a column is a claim like any other.
505
+ Builders and Runs came back with a source in task 1002771; Type came back in
506
+ task 1002772 when a project began DECLARING it at creation — a nullable
507
+ declared field, so an em dash now means "not declared", never a guess. ── */
479
508
 
480
- test('the table declares no column it has no source for', () => {
509
+ test('every table column has a source — Type is declared, never guessed', () => {
481
510
  const thead = (HUB.match(/<thead>([\s\S]*?)<\/thead>/) || [, ''])[1];
482
- // Type is still absent: the owner's call is that a project DECLARES its type
483
- // at creation, so until that field exists the hub must not guess one.
484
- assert.doesNotMatch(thead, />Type</, 'Type has no source until a project declares it');
485
- // Builders and Runs came back in task 1002771 WITH a source behind them
511
+ assert.match(thead, />Type</, 'Type has a source now: the project declares it at creation (task 1002772)');
512
+ assert.match(SCRIPT, /typeHtml\(p\.type\)/, 'the Type cell must read the declared field through the one formatter');
486
513
  assert.match(thead, />Builders</);
487
514
  assert.match(thead, />Runs</);
488
515
  assert.match(SCRIPT, /p\.builders/, 'the Builders cell must read real data');
489
516
  assert.match(SCRIPT, /tagsHtml\(p\.tags\)/, 'the Runs cell must read real data');
517
+ // the wizard files the declaration with the create — the source end of the pipe
518
+ assert.match(SCRIPT, /type: state\.projType/, 'the create request carries the declared type');
490
519
  });
491
520
 
492
521
  test('header and body agree on the column count', () => {
@@ -509,11 +538,13 @@ test('no sort control is offered for a column that cannot sort', () => {
509
538
  }
510
539
  });
511
540
 
512
- test('Type left no styling behind, and the live columns kept theirs', () => {
513
- assert.ok(!COSMOS.includes('.colType'), '.colType is dead — the column is gone');
514
- for (const live of ['.tbl .num', '.tbl .tags', '.colTags']) {
515
- assert.ok(COSMOS.includes(live), `${live} styles a column that now renders real data`);
541
+ test('every live column keeps its styling — .colType included since task 1002772', () => {
542
+ for (const live of ['.tbl .num', '.tbl .tags', '.colTags', '.colType', '.typeTag']) {
543
+ assert.ok(COSMOS.includes(live), `${live} styles a column that renders real data`);
516
544
  }
545
+ // the type tag is its own vocabulary: not the relationship .pill, not the
546
+ // liveness .chip — two different facts must not wear the same chrome
547
+ assert.doesNotMatch(SCRIPT, /typeHtml[\s\S]{0,80}?class="pill"/, 'type must not wear the relationship pill');
517
548
  });
518
549
 
519
550
  /* ── Real Builders + Runs (task 1002771) ────────────────────────────────────