@bongos/core 1.24.33 → 1.24.34

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,114 @@
1
+ // tests/craft_mode_home.mjs — choosing a craft leaves another craft's page for Home
2
+ // (task 1005067, goal 1000095).
3
+ //
4
+ // Owner, 2026-11-04: a governor on the city who picked Artistry, Ideating or Building
5
+ // stayed in the governor's city. Each craft's home is its own dashboard (Home draws it;
6
+ // the street is Governing's for now), so a page that belongs ONLY to other crafts is
7
+ // left for Home, while a shared page, a page no craft owns, and Home itself stay put.
8
+ // These cases run the REAL craft-mode.js and drive its own choose(), recording where
9
+ // the hop would go instead of navigating.
10
+ import assert from 'node:assert/strict';
11
+ import { test } from 'node:test';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import vm from 'node:vm';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { realOTB } from './helpers.mjs';
17
+
18
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ const CRAFT = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/craft-mode.js'), 'utf8');
20
+
21
+ function el() {
22
+ const attrs = {};
23
+ return {
24
+ id: '', dataset: {}, children: [],
25
+ classList: { toggle() {}, add() {}, remove() {} },
26
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
27
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
28
+ appendChild(c) { this.children.push(c); return c; },
29
+ querySelector() { return null; }, querySelectorAll() { return []; },
30
+ };
31
+ }
32
+
33
+ // One page: `page` is body[data-page], `active` the nav item the shell marked.
34
+ function boot({ page, active = null, govCity = false }) {
35
+ const hops = [];
36
+ const body = el();
37
+ body.setAttribute('data-page', page);
38
+ const sidebar = el();
39
+ sidebar.querySelector = (sel) => {
40
+ if (sel !== '.nav-item--active' || !active) return null;
41
+ const a = el(); a.id = 'nav-' + active; return a;
42
+ };
43
+ const document = {
44
+ body, head: el(), documentElement: el(), readyState: 'complete',
45
+ getElementById: (id) => (id === 'app-sidebar' ? sidebar : null),
46
+ querySelector: () => null, querySelectorAll: () => [],
47
+ createElement: () => el(), createComment: () => el(),
48
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
49
+ };
50
+ const store = () => { const o = {}; return { getItem: (k) => (k in o ? o[k] : null), setItem: (k, v) => { o[k] = String(v); }, removeItem: (k) => { delete o[k]; } }; };
51
+ const window = {
52
+ OTB: realOTB(), sessionStorage: store(), localStorage: store(),
53
+ OTBCraftHop: (h) => hops.push(h),
54
+ matchMedia: () => ({ matches: false }),
55
+ location: { search: '' },
56
+ };
57
+ if (govCity) window.GovCity = { STREET_HREF: '/builders/street' };
58
+ window.window = window;
59
+ vm.runInNewContext(CRAFT, { window, document, CustomEvent: function () {}, Promise, console });
60
+ return { CM: window.OTBCraftMode, hops };
61
+ }
62
+
63
+ test('leaving the governor\'s city for any other craft goes to Home', () => {
64
+ for (const mode of ['building', 'artistry', 'ideating']) {
65
+ const { CM, hops } = boot({ page: 'city', active: 'city' });
66
+ CM.choose(mode);
67
+ assert.deepEqual(hops, ['/'], `${mode} from the city goes Home`);
68
+ }
69
+ });
70
+
71
+ test('the street is Governing\'s own page, so other crafts leave it too', () => {
72
+ const { CM, hops } = boot({ page: 'street' });
73
+ assert.equal(CM.strandedBy('governing'), false);
74
+ CM.choose('ideating');
75
+ assert.deepEqual(hops, ['/']);
76
+ });
77
+
78
+ test('choosing Governing on another craft\'s page lands on the street', () => {
79
+ const { CM, hops } = boot({ page: 'studio', active: 'studio', govCity: true });
80
+ CM.choose('governing');
81
+ assert.deepEqual(hops, ['/builders/street']);
82
+ // without the street's script on the page, Home itself hops to the street
83
+ assert.equal(boot({ page: 'studio' }).CM.homeHref('governing'), '/');
84
+ });
85
+
86
+ test('a page the chosen craft also shows stays put', () => {
87
+ // goals is in Building, Artistry and Ideating alike
88
+ const { CM, hops } = boot({ page: 'goals', active: 'goals' });
89
+ CM.choose('ideating');
90
+ CM.choose('artistry');
91
+ assert.deepEqual(hops, []);
92
+ });
93
+
94
+ test('a page every craft keeps, and a page no craft owns, stay put', () => {
95
+ for (const [page, active] of [['roster', 'roster'], ['my-agents', 'my-agents'], ['task', null], ['idea', null]]) {
96
+ const { CM, hops } = boot({ page, active });
97
+ CM.choose('governing');
98
+ CM.choose('artistry');
99
+ assert.deepEqual(hops, [], `${page} stays`);
100
+ }
101
+ });
102
+
103
+ test('Home redraws itself for the craft, so it never hops from here', () => {
104
+ const { CM, hops } = boot({ page: 'home', active: 'home' });
105
+ CM.choose('artistry');
106
+ CM.choose('governing');
107
+ assert.deepEqual(hops, []);
108
+ });
109
+
110
+ test('the nav item wins over the page name (Economy is drachmae.html)', () => {
111
+ const { CM } = boot({ page: 'drachmae', active: 'economy' });
112
+ assert.equal(CM.strandedBy('artistry'), true);
113
+ assert.equal(CM.strandedBy('building'), false);
114
+ });
@@ -0,0 +1,70 @@
1
+ // tests/hall_sitemap.mjs — the builders hall's generated sitemap stays fresh, and no hall
2
+ // link is broken (task 1005068, goal 1000095).
3
+ //
4
+ // docs/hall-sitemap.md is written by scripts/bongos/gen-hall-sitemap.js from the real
5
+ // sources: the page files, shell.js NAV, craft-mode.js's tabs (run, not read) and every
6
+ // in-hall link. It is maintained by regeneration, never by hand, so this fails the moment
7
+ // a page, a nav item, a craft's tabs or a link changes without a regenerate, and the
8
+ // moment any link points at a URL the hall does not serve.
9
+ import assert from 'node:assert/strict';
10
+ import { test } from 'node:test';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const require = createRequire(import.meta.url);
18
+ const gen = require('../scripts/bongos/gen-hall-sitemap.js');
19
+ const model = gen.build();
20
+
21
+ test('the parse found the hall, so nothing below passes against an empty map', () => {
22
+ const files = fs.readdirSync(path.join(ROOT, 'modules/hall-ui/public')).filter((f) => f.endsWith('.html'));
23
+ assert.equal(model.pages.length, files.length, 'one row per page file');
24
+ assert.ok(model.nav.length >= 25, `expected the shell's nav, found ${model.nav.length} items`);
25
+ for (const c of ['building', 'artistry', 'ideating', 'governing']) {
26
+ assert.ok(model.crafts.tabs[c].length > model.crafts.always.length, `${c} lists tabs of its own`);
27
+ }
28
+ assert.ok(model.chrome.includes('shell.js') && model.chrome.includes('craft-mode.js'), 'the chrome is the shell');
29
+ assert.ok(model.pages.reduce((n, p) => n + p.out.length, 0) >= 40, 'the pages link to each other');
30
+ });
31
+
32
+ test('docs/hall-sitemap.md is what the generator writes now', () => {
33
+ const committed = fs.readFileSync(path.join(ROOT, gen.OUT), 'utf8');
34
+ assert.ok(committed === gen.render(model),
35
+ `${gen.OUT} is stale — run: node scripts/bongos/gen-hall-sitemap.js`);
36
+ });
37
+
38
+ test('no hall link points at a URL the hall does not serve', () => {
39
+ assert.deepEqual(model.findings.broken, [],
40
+ 'fix the link, or name the server route in ROUTES in scripts/bongos/gen-hall-sitemap.js');
41
+ });
42
+
43
+ test('every craft tab is a real nav item', () => {
44
+ assert.deepEqual(model.findings.dangling, []);
45
+ });
46
+
47
+ test('a link is read the ways the hall writes one, and an unknown one is broken', () => {
48
+ const urls = new Set(model.pages.map((p) => p.url));
49
+ const found = gen.linksIn([
50
+ '<a href="/goals">', "x.innerHTML = '<a href=\"/builders/roster?id=2\">';",
51
+ "a.href = '/work';", "{ href: '/profile' }", "location.assign('/settings#x')",
52
+ "const STREET_HREF = '/builders/street';", '`<a href="${hallBase()}/idea/${id}">`',
53
+ "function itemHref(id) { return '/market-watch-item?id=' + id; }",
54
+ '`<a href="${hub}/connections">`', "fetch(API + '/goals')",
55
+ ].join('\n')).map((h) => gen.classify(h, urls));
56
+ assert.deepEqual(found.sort(), ['/goals', '/idea', '/market-watch-item', '/profile', '/roster', '/settings', '/street', '/work'].sort(),
57
+ 'each written form is a link to its page; a hub page and an API path are not hall links');
58
+ assert.equal(gen.classify('/no-such-page', urls), false, 'an unknown target is broken');
59
+ assert.equal(gen.classify('/task/12', urls), '/task', 'a task id lands on the task page');
60
+ assert.equal(gen.classify('/people', urls), '/roster', 'a retired URL follows its redirect');
61
+ assert.equal(gen.classify('/builders/style.css?v=1', urls), null, 'an asset is not a page link');
62
+ });
63
+
64
+ test('the street is Governing\'s home: no nav item, reached by clicking from the city', () => {
65
+ const street = model.pages.find((p) => p.url === '/street');
66
+ assert.ok(street, 'the street is a hall page');
67
+ assert.deepEqual(street.nav, []);
68
+ assert.ok(street.reachIn.includes('governing'));
69
+ assert.ok(street.in.includes('/city'), 'the city links to the street');
70
+ });