@bongos/core 1.21.182 → 1.22.1

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.
Files changed (135) hide show
  1. package/.bongos-core.json +157 -137
  2. package/docs/copy-inventory.md +165 -165
  3. package/docs/copy-registry.json +184 -184
  4. package/docs/module-api-changelog.md +10 -0
  5. package/docs/page-inventory.json +35 -26
  6. package/docs/page-readings.json +389 -388
  7. package/modules/hall-ui/public/approval-queue.js +3 -3
  8. package/modules/hall-ui/public/atlas.html +2 -1
  9. package/modules/hall-ui/public/atlas.js +1 -1
  10. package/modules/hall-ui/public/blockers.html +3 -4
  11. package/modules/hall-ui/public/board-room.html +2 -2
  12. package/modules/hall-ui/public/brand-holes.js +3 -3
  13. package/modules/hall-ui/public/city.html +2 -1
  14. package/modules/hall-ui/public/collab.html +2 -2
  15. package/modules/hall-ui/public/commands.html +2 -2
  16. package/modules/hall-ui/public/commands.js +1 -2
  17. package/modules/hall-ui/public/copy-desk.html +2 -2
  18. package/modules/hall-ui/public/core-move-notice.js +1 -5
  19. package/modules/hall-ui/public/craft-mode.js +1 -5
  20. package/modules/hall-ui/public/criterion-uat-ui.js +3 -3
  21. package/modules/hall-ui/public/deploy.html +2 -2
  22. package/modules/hall-ui/public/diagrams.html +2 -1
  23. package/modules/hall-ui/public/dom-utils.js +15 -6
  24. package/modules/hall-ui/public/drachmae.html +2 -1
  25. package/modules/hall-ui/public/fleet-costs.js +3 -2
  26. package/modules/hall-ui/public/fleet.html +2 -2
  27. package/modules/hall-ui/public/founding-mark.js +1 -6
  28. package/modules/hall-ui/public/gate.html +2 -2
  29. package/modules/hall-ui/public/goal-map.html +2 -1
  30. package/modules/hall-ui/public/goals.html +2 -2
  31. package/modules/hall-ui/public/government.html +2 -2
  32. package/modules/hall-ui/public/hall-ask.js +3 -3
  33. package/modules/hall-ui/public/hall-kit.js +3 -5
  34. package/modules/hall-ui/public/hall-visual.js +3 -5
  35. package/modules/hall-ui/public/idea.html +2 -2
  36. package/modules/hall-ui/public/ideas.html +2 -2
  37. package/modules/hall-ui/public/index.html +2 -2
  38. package/modules/hall-ui/public/lightbulb.js +1 -5
  39. package/modules/hall-ui/public/md-reader.js +1 -8
  40. package/modules/hall-ui/public/modules.html +2 -2
  41. package/modules/hall-ui/public/my-agents.html +2 -2
  42. package/modules/hall-ui/public/my-agents.js +3 -1
  43. package/modules/hall-ui/public/page-notes.js +1 -5
  44. package/modules/hall-ui/public/paintbrush.js +1 -5
  45. package/modules/hall-ui/public/palette.js +1 -5
  46. package/modules/hall-ui/public/primer.html +2 -1
  47. package/modules/hall-ui/public/primer.js +1 -1
  48. package/modules/hall-ui/public/profile-crafts.js +3 -5
  49. package/modules/hall-ui/public/profile.html +2 -3
  50. package/modules/hall-ui/public/project-settings.html +2 -2
  51. package/modules/hall-ui/public/ranks.html +2 -3
  52. package/modules/hall-ui/public/roadmap.html +2 -2
  53. package/modules/hall-ui/public/roster.html +3 -4
  54. package/modules/hall-ui/public/sessions.html +2 -3
  55. package/modules/hall-ui/public/settings.html +2 -3
  56. package/modules/hall-ui/public/shell.js +1 -5
  57. package/modules/hall-ui/public/sky-data.js +1 -1
  58. package/modules/hall-ui/public/street.html +2 -1
  59. package/modules/hall-ui/public/studio.html +2 -2
  60. package/modules/hall-ui/public/task-detail.js +1 -6
  61. package/modules/hall-ui/public/task-lib.js +3 -8
  62. package/modules/hall-ui/public/task.html +2 -3
  63. package/modules/hall-ui/public/thinking.html +2 -1
  64. package/modules/hall-ui/public/tweak-editor.html +2 -2
  65. package/modules/hall-ui/public/vote-control.js +1 -1
  66. package/modules/hall-ui/public/watch.html +2 -3
  67. package/modules/hall-ui/public/welcome-boot.js +3 -5
  68. package/modules/hall-ui/public/work.html +2 -3
  69. package/modules/lifecycle/ship-card.js +1 -4
  70. package/modules/npm-release/preview/proxy.js +2 -3
  71. package/modules/npm-release/public/task-widget.js +1 -1
  72. package/modules/platform-identity/routes/sso.js +1 -3
  73. package/modules/provisioning/routes/callback-page.js +2 -6
  74. package/modules/public-landing/public/projects.html +3 -5
  75. package/modules/raise/raise.js +1 -1
  76. package/package-lock.json +2 -2
  77. package/package.json +1 -1
  78. package/release-notes.json +20 -12
  79. package/scripts/bongos/escape-helper-guard.js +147 -0
  80. package/scripts/bongos/fitness.js +3 -2
  81. package/scripts/bongos/key-card.js +1 -4
  82. package/scripts/bongos/review-sheet.js +1 -3
  83. package/scripts/bongos/start.js +1 -1
  84. package/scripts/bongos/status.js +1 -1
  85. package/scripts/gds/escape-helper-guard.js +6 -0
  86. package/scripts/status-mirror-sync.js +2 -3
  87. package/src/bongos/module-scope-map.js +1 -0
  88. package/src/bongos/platform-visibility-gate.js +2 -3
  89. package/src/bongos/prelaunch-gate.js +2 -3
  90. package/src/bongos/routes/auth.js +1 -5
  91. package/src/bongos/serve-internal.js +2 -6
  92. package/src/html-escape.js +26 -0
  93. package/src/module-api.js +9 -1
  94. package/tests/blockers_kit_pager.mjs +2 -2
  95. package/tests/blockers_page_ui.mjs +2 -2
  96. package/tests/core_move_notice.mjs +3 -1
  97. package/tests/core_update_banner.mjs +2 -0
  98. package/tests/core_upgrade_owner_door.mjs +2 -0
  99. package/tests/craft_mode.mjs +2 -0
  100. package/tests/craft_mode_nav.mjs +3 -0
  101. package/tests/craft_review_mode.mjs +3 -2
  102. package/tests/criterion_ref_status.mjs +7 -2
  103. package/tests/curve_kind_overlay.mjs +2 -1
  104. package/tests/founding_mode.mjs +10 -2
  105. package/tests/github_install_nudge.mjs +4 -1
  106. package/tests/goal_map_page.mjs +2 -2
  107. package/tests/hall_approval_queue.mjs +2 -2
  108. package/tests/hall_city.mjs +1 -1
  109. package/tests/hall_fullscreen.mjs +2 -0
  110. package/tests/hall_gov_real_site.mjs +2 -0
  111. package/tests/hall_kit_charts.mjs +2 -1
  112. package/tests/hall_my_agents.mjs +3 -2
  113. package/tests/hall_nav.mjs +2 -0
  114. package/tests/hall_palette.mjs +2 -2
  115. package/tests/hall_reading_world.mjs +3 -1
  116. package/tests/hall_reference_world.mjs +2 -1
  117. package/tests/hall_signout.mjs +2 -0
  118. package/tests/hall_street.mjs +1 -1
  119. package/tests/hall_studio_world.mjs +3 -1
  120. package/tests/hall_tweak_editor.mjs +2 -1
  121. package/tests/helpers.mjs +17 -2
  122. package/tests/html_escape.mjs +149 -0
  123. package/tests/lightbulb.mjs +2 -0
  124. package/tests/module_api.mjs +1 -0
  125. package/tests/module_store_howto.mjs +2 -1
  126. package/tests/nav_permission_atoms.mjs +1 -1
  127. package/tests/npm_release_where.mjs +2 -1
  128. package/tests/page_notes_panel.mjs +4 -3
  129. package/tests/paintbrush.mjs +2 -1
  130. package/tests/profile_connections_ui.mjs +2 -1
  131. package/tests/profile_role_headline.mjs +2 -2
  132. package/tests/profile_role_tabs.mjs +2 -2
  133. package/tests/projects_hub_repo_private.mjs +4 -0
  134. package/tests/terms_acceptance.mjs +1 -0
  135. package/tests/welcome_boot_browser.mjs +1 -1
@@ -22,6 +22,7 @@ import path from 'node:path';
22
22
  import vm from 'node:vm';
23
23
  import { createRequire } from 'node:module';
24
24
  import { fileURLToPath } from 'node:url';
25
+ import { realOTB } from './helpers.mjs';
25
26
 
26
27
  const require = createRequire(import.meta.url);
27
28
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -159,12 +160,14 @@ test('GET /provisioning/instances and /provisioning/instances/:id carry github_i
159
160
  function grab(head) {
160
161
  const at = HUB.indexOf(`\n ${head}`);
161
162
  assert.ok(at >= 0, `${head} must exist`);
163
+ const line = HUB.slice(at, HUB.indexOf('\n', at + 1));
164
+ if (/\}\s*$/.test(line)) return line; // a one-line declaration (esc, task 1004876)
162
165
  const end = HUB.indexOf('\n }', at + 1);
163
166
  const close = HUB.slice(end, end + 5) === '\n };' ? end + 5 : end + 4;
164
167
  return HUB.slice(at, close);
165
168
  }
166
169
  function page() {
167
- const ctx = { STATE_GROUP: { active: 'live', error: 'attention', torn_down: 'gone' } };
170
+ const ctx = { window: { OTB: realOTB() }, STATE_GROUP: { active: 'live', error: 'attention', torn_down: 'gone' } };
168
171
  vm.createContext(ctx);
169
172
  vm.runInContext(['function esc(', 'function ghInstallRow(', 'function groupFor('].map(grab).join('\n'), ctx);
170
173
  return ctx;
@@ -431,9 +431,9 @@ await test('the page is the sky\'s own surface — the same sheet and the same s
431
431
  assert.match(html, /<main id="hall-main" class="sky-stage">/, 'the sky is drawn in the stage, the skip link\'s target');
432
432
  const scripts = (h) => [...h.matchAll(/<script src="\/builders\/([^"?]+)\?v=([^"]+)"/g)].map((m) => [m[1], m[2]]);
433
433
  // criterion-uat-ui.js (task 1004402): the rings' UAT state words, shared with the goal page.
434
- assert.deepEqual(scripts(html).filter((s) => !['shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
434
+ assert.deepEqual(scripts(html).filter((s) => !['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
435
435
  // Both doors load the shell (tasks 1004327, 1004430); the SKY's files are the same.
436
- const skyScripts = (h) => scripts(h).filter((s) => !['shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0]));
436
+ const skyScripts = (h) => scripts(h).filter((s) => !['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0]));
437
437
  assert.deepEqual(skyScripts(html), skyScripts(thinking), 'the two doors load the same sky files at the same stamp');
438
438
  assert.equal(html.match(/thinking\.css\?v=([^"]+)/)[1], thinking.match(/thinking\.css\?v=([^"]+)/)[1]);
439
439
  assert.match(read('thinking.css'), /body\[data-page\] \.sky-stage \{/, 'the sheet dresses the one stage both doors draw in inside the hall');
@@ -18,7 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
- import { makeRunner, makeEl, walk, settle } from './helpers.mjs';
21
+ import { makeRunner, makeEl, walk, settle, realOTB } from './helpers.mjs';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -105,7 +105,7 @@ function bootPanel({ answers = {}, writes = null, mode = 'light' } = {}) {
105
105
  return val.__status ? { ok: false, status: val.__status, data: val.data } : { ok: true, status: 200, data: val };
106
106
  },
107
107
  };
108
- const win = { matchMedia: () => ({ matches: false }), __BRANDING__: { identity: { productName: 'Cloud Bongos', worldName: 'the studio' } } };
108
+ const win = { OTB: realOTB(), matchMedia: () => ({ matches: false }), __BRANDING__: { identity: { productName: 'Cloud Bongos', worldName: 'the studio' } } };
109
109
  const ctx = vm.createContext({ window: win, document: documentObj, console, setTimeout, clearTimeout, encodeURIComponent });
110
110
  win.window = win;
111
111
  vm.runInContext(read('brand-holes.js'), ctx, { filename: 'brand-holes.js' });
@@ -288,7 +288,7 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
288
288
  // task 1004685 (the living city): gov-living.js, the model under the drawing, and
289
289
  // gov-feeds.js, its one reader per page (shared with the shell's tally), load before it
290
290
  // task 1004702: the card answers its first ask in place, so the deck's card and sender (and the desk rows under them) load before the page
291
- assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, as on every hall page');
291
+ assert.deepEqual(scripts, ['dom-utils', 'shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, as on every hall page');
292
292
  const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
293
293
  assert.match(css, /\.city-main \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
294
294
  assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
@@ -18,6 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { realOTB } from './helpers.mjs';
21
22
 
22
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
24
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -104,6 +105,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
104
105
  };
105
106
  let resizes = 0;
106
107
  const windowObj = {
108
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
107
109
  __BRANDING__: {}, __MODULES__: { enabled: {} },
108
110
  // `meId` answers /me once with that builder; otherwise the read never settles.
109
111
  BongosClient: { createClient: () => ({ request: () => (meId == null ? new Promise(() => {})
@@ -31,6 +31,7 @@ import path from 'node:path';
31
31
  import vm from 'node:vm';
32
32
  import { createRequire } from 'node:module';
33
33
  import { fileURLToPath } from 'node:url';
34
+ import { realOTB } from './helpers.mjs';
34
35
 
35
36
  const require = createRequire(import.meta.url);
36
37
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -300,6 +301,7 @@ function baseWindow(hash = '') {
300
301
  const store = new Map();
301
302
  const ls = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) };
302
303
  return {
304
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
303
305
  innerWidth: 1440, innerHeight: 900, location: { hash, href: '', search: '', pathname: '/' },
304
306
  localStorage: ls, sessionStorage: ls,
305
307
  matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {}, removeEventListener() {},
@@ -20,6 +20,7 @@ import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import vm from 'node:vm';
22
22
  import { fileURLToPath } from 'node:url';
23
+ import { realOTB } from './helpers.mjs';
23
24
 
24
25
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
26
  const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
@@ -299,7 +300,7 @@ function runPage(file, extraWindow = {}) {
299
300
  { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
300
301
  const sandbox = {
301
302
  window: {
302
- OTB: { escapeHtml: esc, $: () => null, fmtNum: (n) => String(n ?? 0), fmtDate: (s) => String(s ?? '') },
303
+ OTB: { ...realOTB(), escapeHtml: esc, $: () => null, fmtNum: (n) => String(n ?? 0), fmtDate: (s) => String(s ?? '') },
303
304
  __BRANDING__: { currency: { label: 'drachmae' } },
304
305
  ...extraWindow,
305
306
  },
@@ -26,6 +26,7 @@ import path from 'node:path';
26
26
  import vm from 'node:vm';
27
27
  import { createRequire } from 'node:module';
28
28
  import { fileURLToPath } from 'node:url';
29
+ import { realOTB } from './helpers.mjs';
29
30
 
30
31
  const require = createRequire(import.meta.url);
31
32
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -378,7 +379,7 @@ function bootCraft({ session = {} } = {}) {
378
379
  getElementById: () => null, querySelector: () => null, querySelectorAll: () => [],
379
380
  createElement: () => el(), addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
380
381
  };
381
- const window = { location: { search: '' }, matchMedia: () => ({ matches: false }), sessionStorage: mk(session), localStorage: mk({}) };
382
+ const window = { OTB: realOTB(), location: { search: '' }, matchMedia: () => ({ matches: false }), sessionStorage: mk(session), localStorage: mk({}) };
382
383
  const sandbox = { window, document, console, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
383
384
  sandbox.globalThis = sandbox;
384
385
  vm.createContext(sandbox);
@@ -441,7 +442,7 @@ test('PAGE: in the hall family, with no prototype key', () => {
441
442
  const sheets = [...html.matchAll(/href="\/builders\/([a-z-]+\.css)/g)].map((m) => m[1]);
442
443
  assert.deepEqual(sheets, ['style.css', 'oversight.css', 'my-agents.css'], 'rides the oversight archetype, linked before its own sheet');
443
444
  const scripts = [...html.matchAll(/src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
444
- assert.deepEqual(scripts.slice(0, 3), ['shell-trail', 'craft-mode', 'shell']);
445
+ assert.deepEqual(scripts.slice(0, 4), ['dom-utils', 'shell-trail', 'craft-mode', 'shell']); // dom-utils first: the shell escapes through it (task 1004876)
445
446
  for (const s of ['gov-living', 'gov-positions', 'my-agents']) assert.ok(scripts.includes(s), `loads ${s}.js`);
446
447
  assert.doesNotMatch(html, /ma-key|data-proto|Prototype data/, 'nothing on the page is prototype data, so there is no key');
447
448
  assert.doesNotMatch(read('my-agents.js'), /governor-overlay/, 'and it reads no overlay');
@@ -20,6 +20,7 @@ import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import vm from 'node:vm';
22
22
  import { fileURLToPath } from 'node:url';
23
+ import { realOTB } from './helpers.mjs';
23
24
 
24
25
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
26
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -136,6 +137,7 @@ function boot({ page = 'settings', hash = '', pathname = '/settings', search = '
136
137
  };
137
138
 
138
139
  const windowObj = {
140
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
139
141
  __BRANDING__: {}, __MODULES__: { enabled: {} },
140
142
  // Index pages never reach probeMe (builders.js owns /me there); a non-index
141
143
  // boot would, so hand back a never-settling promise rather than fake auth —
@@ -18,7 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
- import { hallSource } from './helpers.mjs';
21
+ import { hallSource, realOTB } from './helpers.mjs';
22
22
 
23
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
24
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -79,7 +79,7 @@ function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work',
79
79
  addEventListener() {},
80
80
  activeElement: null,
81
81
  };
82
- const windowObj = { location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
82
+ const windowObj = { OTB: realOTB(), location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
83
83
  const sandbox = {
84
84
  window: windowObj, document: documentObj, navigator: { platform: 'Win32', userAgent: 'test' },
85
85
  console, setTimeout, clearTimeout, Promise, JSON, Number, String, Array, Object,
@@ -59,6 +59,7 @@ import fs from 'node:fs';
59
59
  import path from 'node:path';
60
60
  import vm from 'node:vm';
61
61
  import { fileURLToPath } from 'node:url';
62
+ import { realOTB } from './helpers.mjs';
62
63
 
63
64
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
64
65
  const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
@@ -429,7 +430,7 @@ test('roadmap\'s two sub-24px controls clear the floor, on both axes', () => {
429
430
  // md-reader.js is a browser IIFE that only touches `window` and `location`, so
430
431
  // a two-property sandbox is the whole DOM it needs.
431
432
  function loadReader() {
432
- const sandbox = { window: {}, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/primer' } };
433
+ const sandbox = { window: { OTB: realOTB() }, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/primer' } };
433
434
  vm.createContext(sandbox);
434
435
  vm.runInContext(mdReaderJs, sandbox, { filename: 'md-reader.js' });
435
436
  return sandbox.window.OTBMdReader;
@@ -644,6 +645,7 @@ async function bootRoadmap({ progress, versions } = {}) {
644
645
  },
645
646
  };
646
647
  const windowObj = {
648
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
647
649
  BongosClient: { createClient: () => client },
648
650
  __BRANDING__: {}, __MODULES__: { enabled: {} },
649
651
  localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
@@ -41,6 +41,7 @@ import fs from 'node:fs';
41
41
  import path from 'node:path';
42
42
  import vm from 'node:vm';
43
43
  import { fileURLToPath } from 'node:url';
44
+ import { realOTB } from './helpers.mjs';
44
45
 
45
46
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
46
47
  const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
@@ -67,7 +68,7 @@ const rule = (css, selector) => {
67
68
 
68
69
  // md-reader.js is a browser IIFE that only touches `window` and `location`.
69
70
  function loadReader() {
70
- const sandbox = { window: {}, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/drachmae' } };
71
+ const sandbox = { window: { OTB: realOTB() }, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/drachmae' } };
71
72
  vm.createContext(sandbox);
72
73
  vm.runInContext(mdReaderJs, sandbox, { filename: 'md-reader.js' });
73
74
  return sandbox.window.OTBMdReader;
@@ -20,6 +20,7 @@ import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import vm from 'node:vm';
22
22
  import { fileURLToPath } from 'node:url';
23
+ import { realOTB } from './helpers.mjs';
23
24
 
24
25
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
26
  const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/shell.js'), 'utf8');
@@ -66,6 +67,7 @@ function driveClick({ page = 'settings', href } = {}) {
66
67
  };
67
68
 
68
69
  const windowObj = {
70
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
69
71
  __BRANDING__: {}, __MODULES__: { enabled: {} },
70
72
  BongosClient: { createClient: () => api },
71
73
  matchMedia: () => ({ matches: false, addEventListener() {} }),
@@ -487,7 +487,7 @@ test('the page holds the prototype\'s header, the street, the readings and the c
487
487
  assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
488
488
  for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
489
489
  const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
490
- assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-depth', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the living model and its one reader (task 1004685: city-draw.js counts with it, so it loads first), the shared kit, then the card (task 1004528), the interiors (task 1004526), the living street\'s exterior (street-depth.js), the floor desk (task 1004558) and the drafted amendment (task 1004530); the deck rides beside them');
490
+ assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['dom-utils', 'shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-depth', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the living model and its one reader (task 1004685: city-draw.js counts with it, so it loads first), the shared kit, then the card (task 1004528), the interiors (task 1004526), the living street\'s exterior (street-depth.js), the floor desk (task 1004558) and the drafted amendment (task 1004530); the deck rides beside them');
491
491
  assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
492
492
  });
493
493
 
@@ -17,6 +17,7 @@ import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import vm from 'node:vm';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { realOTB } from './helpers.mjs';
20
21
 
21
22
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
23
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -271,7 +272,7 @@ async function bootStudio({ reviews = [], shipped = {}, rank = 'metic', flags =
271
272
  const handlers = [];
272
273
  const window = {
273
274
  BongosClient: { createClient: () => ({ request }) },
274
- OTB: {
275
+ OTB: { ...realOTB(),
275
276
  // task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
276
277
  // the same reason categoryChip is: an undefined helper throws mid-render.
277
278
  roleAxisLabel: () => 'Role',
@@ -574,6 +575,7 @@ function bootShell() {
574
575
  };
575
576
  const location = { hostname: 'builders.cloudbongos.com', origin: 'https://builders.cloudbongos.com', pathname: '/studio', hash: '', assign() {}, replace() {} };
576
577
  const window = {
578
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
577
579
  __BRANDING__: {}, __MODULES__: { enabled: {} },
578
580
  BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
579
581
  matchMedia: () => ({ matches: false, addEventListener() {} }),
@@ -19,7 +19,7 @@ import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import vm from 'node:vm';
21
21
  import { fileURLToPath } from 'node:url';
22
- import { makeRunner, makeEl, parseInto, walk, settle } from './helpers.mjs';
22
+ import { makeRunner, makeEl, parseInto, walk, settle, realOTB } from './helpers.mjs';
23
23
 
24
24
  const require = createRequire(import.meta.url);
25
25
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -133,6 +133,7 @@ function bootEditor({ search = '?page=landing:index', answers = {}, stored = {}
133
133
  },
134
134
  };
135
135
  const windowObj = {
136
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
136
137
  __BRANDING__: { identity: { productName: 'Cloud Bongos' } }, __MODULES__: { enabled: {} },
137
138
  BongosClient: { createClient: () => client },
138
139
  location: { host: 'builders.cloudbongos.com', hostname: 'builders.cloudbongos.com', protocol: 'https:', origin: 'https://builders.cloudbongos.com', href: 'https://builders.cloudbongos.com/builders/tweak-editor', search, pathname: '/builders/tweak-editor', assign: (u) => { out.navigated = u; } },
package/tests/helpers.mjs CHANGED
@@ -79,6 +79,20 @@ import { Readable } from 'node:stream';
79
79
 
80
80
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
81
81
 
82
+ // realOTB — the REAL window.OTB from modules/hall-ui/public/dom-utils.js, built in
83
+ // its own sandbox (task 1004876). Hall scripts escape user text through
84
+ // window.OTB.escapeHtml at call time, so a test that loads one of them without the
85
+ // page's dom-utils.js hands it this: `window.OTB = realOTB()` (or spread
86
+ // `escapeHtml: realOTB().escapeHtml` into a partial stub). The real one, never a
87
+ // re-typed escaper, so a test cannot pass against a weaker copy.
88
+ export const DOM_UTILS_JS = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js'), 'utf8');
89
+ export function realOTB() {
90
+ const ctx = { window: {}, document: { querySelector: () => null, createElement: () => ({}) } };
91
+ vm.createContext(ctx);
92
+ vm.runInContext(DOM_UTILS_JS, ctx, { filename: 'dom-utils.js' });
93
+ return ctx.window.OTB;
94
+ }
95
+
82
96
  export const SKIP_MARKER = 'HOST-TOPOLOGY-SKIP';
83
97
 
84
98
  function emitSkip(missing, note) {
@@ -617,7 +631,7 @@ export function bootProfilePage({ signedIn = true, trail = [], roles = ['ideator
617
631
  roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
618
632
  roleIsUnset: (d) => !d || d === 'unclassified',
619
633
  ROLE_UNSET: 'unclassified',
620
- escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
634
+ escapeHtml: realOTB().escapeHtml, // the real one (task 1004876)
621
635
  $: (sel) => documentObj.querySelector(sel),
622
636
  fmtNum: (n) => String(n ?? 0),
623
637
  fmtDate: (iso) => String(iso ?? '').slice(0, 10),
@@ -729,7 +743,7 @@ export function bootIdeaPage({ idea = null, status = 200, template = null, viewe
729
743
  __BRANDING__: {}, __MODULES__: { enabled: {} },
730
744
  BongosClient: { createClient: () => client },
731
745
  OTB: {
732
- escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
746
+ escapeHtml: realOTB().escapeHtml, // the real one (task 1004876)
733
747
  $: (sel) => documentObj.querySelector(sel),
734
748
  fmtDate: (iso) => String(iso ?? '').slice(0, 10),
735
749
  toast() {},
@@ -931,6 +945,7 @@ export function bootSkyPage({
931
945
  const windowObj = {
932
946
  __BRANDING__: {}, __MODULES__: { enabled: {} },
933
947
  BongosClient: { createClient: () => client },
948
+ OTB: realOTB(), // the page loads dom-utils.js first; sky-data.js escapes through it (task 1004876)
934
949
  innerWidth, innerHeight, devicePixelRatio: 1,
935
950
  matchMedia: () => ({ matches: reduce }),
936
951
  localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
@@ -0,0 +1,149 @@
1
+ // tests/html_escape.mjs — the ONE HTML escaper per side (task 1004876, goal 1000135).
2
+ //
3
+ // The escaper is what keeps a builder's own words (a task title, an idea) from
4
+ // becoming live markup on another builder's screen. ~45 private copies were
5
+ // collapsed onto two: window.OTB.escapeHtml (modules/hall-ui/public/dom-utils.js)
6
+ // in the browser and escapeHtml (src/html-escape.js, re-exported by the module
7
+ // doorway) on the server. These tests pin:
8
+ // 1. both shared escapers escape all five characters, map null/undefined to '',
9
+ // and agree with each other on every input;
10
+ // 2. the module doorway hands modules the very same function;
11
+ // 3. pages that used to carry a private copy now render user text through the
12
+ // shared one (a title with <, & and a quote stays text);
13
+ // 4. the fitness check goes red on a third definition, and on a hall page that
14
+ // loads dom-utils.js after (or deferred behind) the scripts that need it.
15
+
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import vm from 'node:vm';
22
+ import { createRequire } from 'node:module';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
28
+
29
+ const { escapeHtml: serverEscape } = require('../src/html-escape.js');
30
+ const guard = require('../scripts/bongos/escape-helper-guard.js');
31
+
32
+ // Load the real dom-utils.js into a sandbox and hand back its window.OTB.
33
+ function browserOTB() {
34
+ const sandbox = { window: {}, document: { querySelector: () => null } };
35
+ vm.createContext(sandbox);
36
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'dom-utils.js'), 'utf8'), sandbox, { filename: 'dom-utils.js' });
37
+ return sandbox.window.OTB;
38
+ }
39
+
40
+ const HOSTILE = `<img src=x onerror="alert('x')"> & "quoted" 'single' a&amp;b`;
41
+ const CASES = [HOSTILE, '', 'plain', 0, 42, -1.5, true, false, null, undefined, '&lt;', '<<>>', "it's", '"'];
42
+
43
+ test('server escaper escapes all five HTML characters', () => {
44
+ assert.equal(serverEscape(`<a href="x" title='y'>&</a>`),
45
+ '&lt;a href=&quot;x&quot; title=&#39;y&#39;&gt;&amp;&lt;/a&gt;');
46
+ });
47
+
48
+ test('server escaper: null/undefined render as empty text, numbers and booleans as their string', () => {
49
+ assert.equal(serverEscape(null), '');
50
+ assert.equal(serverEscape(undefined), '');
51
+ assert.equal(serverEscape(0), '0');
52
+ assert.equal(serverEscape(42), '42');
53
+ assert.equal(serverEscape(false), 'false');
54
+ });
55
+
56
+ test('server escaper escapes an ampersand first, so an existing entity is shown literally', () => {
57
+ assert.equal(serverEscape('&lt;'), '&amp;lt;');
58
+ });
59
+
60
+ test('browser and server escapers agree on every input', () => {
61
+ const OTB = browserOTB();
62
+ for (const c of CASES) assert.equal(OTB.escapeHtml(c), serverEscape(c), `input ${JSON.stringify(c)}`);
63
+ });
64
+
65
+ test('the module doorway exports the shared server escaper', () => {
66
+ const api = require('../src/module-api.js');
67
+ assert.equal(api.escapeHtml, serverEscape);
68
+ });
69
+
70
+ test('a hall helper that dropped its private copy renders user text through window.OTB', () => {
71
+ // hall-kit.js used to carry its own esc() (exported as OTBKit.esc); it must now delegate, at call time.
72
+ const sandbox = { window: {}, document: { querySelector: () => null } };
73
+ vm.createContext(sandbox);
74
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'dom-utils.js'), 'utf8'), sandbox);
75
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'hall-kit.js'), 'utf8'), sandbox);
76
+ const lib = sandbox.window.OTBKit;
77
+ assert.ok(lib && typeof lib.esc === 'function', 'hall-kit exposes esc');
78
+ assert.equal(lib.esc('Fix <b> & "q" \'s\''), 'Fix &lt;b&gt; &amp; &quot;q&quot; &#39;s&#39;');
79
+ assert.equal(lib.esc(null), '');
80
+ });
81
+
82
+ test('fitness: a third escape table anywhere in the code is a hard fail', () => {
83
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'esc-guard-'));
84
+ try {
85
+ fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });
86
+ fs.mkdirSync(path.join(tmp, 'modules', 'x', 'public'), { recursive: true });
87
+ fs.mkdirSync(path.join(tmp, 'modules', 'x', 'tests'), { recursive: true });
88
+ fs.writeFileSync(path.join(tmp, 'src', 'html-escape.js'), "const T = { '<': '&lt;' };\n");
89
+ fs.writeFileSync(path.join(tmp, 'modules', 'x', 'tests', 't.mjs'), "const T = { '<': '&lt;' };\n");
90
+ let r = guard.checkOneHtmlEscaper({ root: tmp });
91
+ assert.equal(r.ok, true, r.violations.join('\n'));
92
+ // A renamed copy is still caught: the detector reads the table, not the name.
93
+ fs.writeFileSync(path.join(tmp, 'modules', 'x', 'public', 'page.js'),
94
+ "const hesc = (s) => String(s).replace(/</g, '&lt;');\n");
95
+ r = guard.checkOneHtmlEscaper({ root: tmp });
96
+ assert.equal(r.ok, false);
97
+ assert.equal(r.hardFail, true);
98
+ assert.match(r.violations[0], /modules\/x\/public\/page\.js:1/);
99
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
100
+ });
101
+
102
+ test('fitness: a delegate to the shared escaper is not a copy', () => {
103
+ assert.deepEqual(guard.findEscapeTables('function esc(s) { return window.OTB.escapeHtml(s); }'), []);
104
+ assert.deepEqual(guard.findEscapeTables("s.replace(/&lt;/g, '<')"), [], 'a DECODER is not an escaper');
105
+ assert.deepEqual(guard.findEscapeTables("x\n{ '<': \"&lt;\" }"), [2]);
106
+ });
107
+
108
+ test('fitness: a local escaper name that does not delegate is a hard fail (the old primer fallback)', () => {
109
+ const browser = { browser: true };
110
+ assert.deepEqual(guard.findFakeDelegates(" const escapeHtml = (MD && MD.escapeHtml) || ((s) => String(s));", browser), [],
111
+ 'names the shared one — but the line-level rule cannot see the fallback, so primer.js no longer has one');
112
+ assert.deepEqual(guard.findFakeDelegates(' const escapeHtml = (s) => String(s);', browser), [1]);
113
+ assert.deepEqual(guard.findFakeDelegates(' function esc(s) { return s; }', browser), [1]);
114
+ assert.deepEqual(guard.findFakeDelegates(' function esc(s) { return window.OTB.escapeHtml(s); }', browser), []);
115
+ assert.deepEqual(guard.findFakeDelegates(' var esc = kit.esc;', browser), []);
116
+ // server: `esc` is also a regex/markdown escaper name there, so only the HTML names are held
117
+ assert.deepEqual(guard.findFakeDelegates("const esc = (k) => k.replace(/[.*]/g, '\\\\$&');"), []);
118
+ assert.deepEqual(guard.findFakeDelegates('const hesc = (s) => String(s);'), [1]);
119
+ assert.deepEqual(guard.findFakeDelegates("const { escapeHtml: hesc } = require('../../src/html-escape');"), []);
120
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'esc-guard-'));
121
+ try {
122
+ fs.mkdirSync(path.join(tmp, 'modules', 'x', 'public'), { recursive: true });
123
+ fs.writeFileSync(path.join(tmp, 'modules', 'x', 'public', 'p.js'), 'const esc = (s) => String(s);\n');
124
+ const r = guard.checkOneHtmlEscaper({ root: tmp });
125
+ assert.equal(r.ok, false);
126
+ assert.match(r.violations[0], /p\.js:1 — defines a local HTML escaper name/);
127
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
128
+ });
129
+
130
+ test('fitness: the guard is registered in fitness.js CHECKS, so CI runs it', () => {
131
+ const fitness = require('../scripts/bongos/fitness.js');
132
+ const checks = fitness.CHECKS || fitness.checks;
133
+ assert.ok(Array.isArray(checks), 'fitness.js exports its CHECKS list');
134
+ assert.ok(checks.includes(guard.checkOneHtmlEscaper), 'checkOneHtmlEscaper is one of the fitness checks');
135
+ });
136
+
137
+ test('fitness: a hall page must load dom-utils.js first and synchronously', () => {
138
+ const ok = '<script src="/builders/dom-utils.js?v=1"></script><script src="/builders/shell.js"></script>';
139
+ assert.equal(guard.hallPageOrderProblem(ok), null);
140
+ assert.equal(guard.hallPageOrderProblem('<p>no scripts</p>'), null);
141
+ assert.match(guard.hallPageOrderProblem('<script src="/builders/shell.js"></script><script src="/builders/dom-utils.js"></script>'), /before/);
142
+ assert.match(guard.hallPageOrderProblem('<script src="/builders/dom-utils.js" defer></script><script src="/builders/shell.js"></script>'), /defer/);
143
+ assert.match(guard.hallPageOrderProblem('<script src="/builders/shell.js"></script>'), /never/);
144
+ });
145
+
146
+ test('fitness: the real tree holds exactly the two shared escapers', () => {
147
+ const r = guard.checkOneHtmlEscaper();
148
+ assert.equal(r.ok, true, r.violations.join('\n'));
149
+ });
@@ -17,6 +17,7 @@ import path from 'node:path';
17
17
  import vm from 'node:vm';
18
18
  import { createRequire } from 'node:module';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { realOTB } from './helpers.mjs';
20
21
 
21
22
  const require = createRequire(import.meta.url);
22
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -265,6 +266,7 @@ function boot({ storage = {}, throwing = false, respond } = {}) {
265
266
  ? { ok: true, status: 200, json: { ideas: [{ id: '5', title: '<b>Bold</b> idea', author_name: 'Bo', captured_at: '2026-10-01T00:00:00Z' }] } }
266
267
  : { ok: true, status: 201, json: { ideas: [{ id: '801', title: 'Sort drafts' }, { id: '802', title: 'Name writers' }], split_by: 'ai' } }));
267
268
  const win = {
269
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
268
270
  location: { pathname: '/studio' },
269
271
  localStorage: store,
270
272
  fetch: (url, opts) => {
@@ -20,6 +20,7 @@ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
20
20
  // `withTx` was added; no enabled module at its pinned coreVersion lost a capability.
21
21
  const PUBLISHED_SURFACE = [
22
22
  'CORE_VERSION',
23
+ 'escapeHtml', // task 1004876: THE server-side HTML escaper (1.22.0) — modules render user text through it, never a private copy
23
24
  'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
24
25
  'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
25
26
  'requireActAlone', // added by task 1004657 (ADR 0363 D5): a building's own page asks its decision-rules row before one builder acts alone. On the doorway because eight buildings' pages live in six modules and core, and each must refuse the same way People does
@@ -16,6 +16,7 @@ import os from 'node:os';
16
16
  import path from 'node:path';
17
17
  import vm from 'node:vm';
18
18
  import { fileURLToPath } from 'node:url';
19
+ import { realOTB } from './helpers.mjs';
19
20
 
20
21
  const require = createRequire(import.meta.url);
21
22
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -173,7 +174,7 @@ test('a delisted module\'s how-to still reads for people who hold it, and only f
173
174
  // ---- the page: the author's markdown is untrusted input ----
174
175
 
175
176
  function loadReader() {
176
- const ctx = { window: {}, location: { href: 'https://builders.example.com/builders/modules', hostname: 'builders.example.com' }, URL };
177
+ const ctx = { window: { OTB: realOTB() }, location: { href: 'https://builders.example.com/builders/modules', hostname: 'builders.example.com' }, URL };
177
178
  vm.createContext(ctx);
178
179
  vm.runInContext(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/md-reader.js'), 'utf8'), ctx);
179
180
  return ctx.window.OTBMdReader;
@@ -93,7 +93,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
93
93
  // says a held core version was tried and rolled back; task 1004731: the one-time
94
94
  // "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
95
95
  // task 1004846: the new logo (its bump landed late, in task 1004852).
96
- const SHELL_SHA = '670b2f71085c13b6';
96
+ const SHELL_SHA = 'cea38eeab6b234cc';
97
97
 
98
98
  test('shell.js has not changed without its cache-bust being bumped', () => {
99
99
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -18,7 +18,7 @@ import path from 'node:path';
18
18
  import vm from 'node:vm';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { createRequire } from 'node:module';
21
- import { npmTarball, npmPackument, fakeHttpsGet } from './helpers.mjs';
21
+ import { npmTarball, npmPackument, fakeHttpsGet, realOTB } from './helpers.mjs';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -193,6 +193,7 @@ function bootWidget(answer, { shown = null } = {}) {
193
193
  const listeners = {};
194
194
  const asked = [];
195
195
  const windowObj = {
196
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
196
197
  OTBTaskShown: shown,
197
198
  BongosClient: { createClient: () => ({ request: async (m, url) => { asked.push(url); return answer(url); } }) },
198
199
  };
@@ -13,6 +13,7 @@ import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import vm from 'node:vm';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { realOTB } from './helpers.mjs';
16
17
 
17
18
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
19
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -59,7 +60,7 @@ function boot({ session = {}, search = '' } = {}) {
59
60
  };
60
61
  const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
61
62
  const opened = [];
62
- const window = { localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }), location: { search } };
63
+ const window = { OTB: realOTB(), localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }), location: { search } };
63
64
  // "Loading" page-notes.js: record the tag, then define the API it would define.
64
65
  head.onAppend = (tag) => {
65
66
  loaded.push(tag.src);
@@ -115,7 +116,7 @@ test('a ?note= link opens that doc on arrival, in any mode', async () => {
115
116
 
116
117
  // ---------- the reader ----------
117
118
  function reader() {
118
- const window = { location: { pathname: '/studio' } };
119
+ const window = { OTB: realOTB(), location: { pathname: '/studio' } };
119
120
  const document = { body: { dataset: { page: 'studio', pageTitle: 'Studio' } }, title: 'Studio — hall' };
120
121
  const sandbox = { window, document };
121
122
  vm.createContext(sandbox);
@@ -206,7 +207,7 @@ function dockedPanel({ reviewing = null, opened = false } = {}) {
206
207
  experiences: () => [{ id: 'ideating', label: 'Ideator experience', short: 'Ideating' }, { id: 'building', label: 'Building experience', short: 'Building' }],
207
208
  iconHtml: () => '<svg></svg>',
208
209
  };
209
- const window = { location: { pathname: '/studio' }, OTBCraftMode: craft };
210
+ const window = { OTB: realOTB(), location: { pathname: '/studio' }, OTBCraftMode: craft };
210
211
  const sandbox = { window, document, fetch, setTimeout, clearTimeout, Promise };
211
212
  vm.createContext(sandbox);
212
213
  vm.runInContext(NOTES, sandbox, { filename: 'page-notes.js' });
@@ -17,6 +17,7 @@ import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import vm from 'node:vm';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { realOTB } from './helpers.mjs';
20
21
 
21
22
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
23
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -157,7 +158,7 @@ function boot({ round = null, draft = null, claim = { status: 200, body: { ok: t
157
158
  return respond(404, {});
158
159
  };
159
160
  const winListeners = {};
160
- const window = { location: { pathname: '/roadmap' }, addEventListener: (t, fn) => { (winListeners[t] ||= []).push(fn); }, removeEventListener: (t, fn) => { winListeners[t] = (winListeners[t] || []).filter((f) => f !== fn); } };
161
+ const window = { OTB: realOTB(), location: { pathname: '/roadmap' }, addEventListener: (t, fn) => { (winListeners[t] ||= []).push(fn); }, removeEventListener: (t, fn) => { winListeners[t] = (winListeners[t] || []).filter((f) => f !== fn); } };
161
162
  const sandbox = { window, document, fetch, Promise, setTimeout, clearTimeout, JSON, Math, String, Array, Object };
162
163
  window.window = window;
163
164
  vm.createContext(sandbox);