@bongos/core 1.21.22 → 1.21.24
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/.bongos-core.json +62 -62
- package/docs/copy-inventory.md +128 -123
- package/docs/copy-registry.json +222 -187
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +251 -248
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +5 -7
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/record.css +4 -19
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +14 -2
- package/modules/hall-ui/public/sessions.js +111 -3
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +56 -5
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +6 -9
- package/modules/hall-ui/public/task.js +15 -50
- package/modules/hall-ui/public/task.states.json +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.css +4 -0
- package/modules/hall-ui/public/tweak-editor.html +5 -3
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/hall-navigation.md +7 -4
- package/modules/sessions/db.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/hall_fullscreen.mjs +88 -12
- package/tests/hall_record_world.mjs +41 -63
- package/tests/hall_sessions_ledger.mjs +40 -0
- package/tests/hall_tweak_editor.mjs +3 -2
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/session_transcript.mjs +5 -0
|
@@ -306,7 +306,8 @@ const CLAIM_ACTION_SRC = read('claim-action.js');
|
|
|
306
306
|
// all of them, because what these tests are actually about is the REQUEST the
|
|
307
307
|
// page issues — `requests` carries paths only and is left alone so the older
|
|
308
308
|
// assertions on it keep meaning what they meant.
|
|
309
|
-
function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsList = null, patch = null, write = null, status = 200, search = '', referrer = '', withReader = true, grade, sessionLogs, claims } = {}) {
|
|
309
|
+
function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsList = null, patch = null, write = null, status = 200, search = '', referrer = '', withReader = true, grade, sessionLogs, claims, shell = false } = {}) {
|
|
310
|
+
const fills = []; // task 1004477: each time the page asked the shell to fill its exits
|
|
310
311
|
const requests = [];
|
|
311
312
|
const writes = [];
|
|
312
313
|
const toasts = [];
|
|
@@ -400,6 +401,9 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
400
401
|
confirm: () => true,
|
|
401
402
|
navigator: { clipboard: { writeText: async () => {} } },
|
|
402
403
|
};
|
|
404
|
+
// task 1004477: the shell is what fills a [data-shell-exit] link. `shell: true`
|
|
405
|
+
// stands it in so a test can see the page ask; absent, the page must still work.
|
|
406
|
+
if (shell) windowObj.OTBShell = { fillExits(root) { fills.push(root); } };
|
|
403
407
|
class CustomEvent { constructor(type, init) { this.type = type; this.detail = init && init.detail; } }
|
|
404
408
|
const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Date, URL, URLSearchParams, CustomEvent };
|
|
405
409
|
sandbox.globalThis = sandbox;
|
|
@@ -410,6 +414,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
410
414
|
assert.ok(onReady, 'task.js registers its DOMContentLoaded boot');
|
|
411
415
|
return {
|
|
412
416
|
boot: onReady,
|
|
417
|
+
fills,
|
|
413
418
|
html: () => documentObj.getElementById('task-body').innerHTML,
|
|
414
419
|
sub: () => documentObj.getElementById('task-sub').textContent,
|
|
415
420
|
el: (id) => documentObj.getElementById(id),
|
|
@@ -536,77 +541,50 @@ test('the idea grade badge IS the archetype pill, not a private copy of it', ()
|
|
|
536
541
|
'idea.css carried a near-verbatim copy of .rec-pill down to the token triples — a hundred lines waiting to drift');
|
|
537
542
|
});
|
|
538
543
|
|
|
539
|
-
// ---- 10. the way out — the record exits
|
|
544
|
+
// ---- 10. the way out — the record exits through the shell's Back ------------
|
|
540
545
|
//
|
|
541
|
-
// task 1003468
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
assert.match(r.html(), /<a class="td-btn" href="\/goals#\/goal\/1000081">Back to goal 1000081<\/a>/);
|
|
557
|
-
assert.doesNotMatch(r.html(), /Open the work board/, 'the hardcoded exit is gone from the happy path');
|
|
558
|
-
assert.equal(r.el('task-back').href, '/goals#/goal/1000081', 'the crumb points at the goal');
|
|
559
|
-
assert.equal(r.el('task-back-label').textContent, 'Back to goal 1000081');
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
test('opened from the board, the exit is the board — on the tab it was opened from', async () => {
|
|
563
|
-
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent('/work#backlog') });
|
|
564
|
-
await r.boot(); await settle();
|
|
565
|
-
assert.match(r.html(), /href="\/work#backlog">Back to the work board</);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
test('without ?from=, a same-origin referrer is the way back', async () => {
|
|
569
|
-
const r = renderTask(BASE_TASK, { referrer: 'https://builders.cloudbongos.com/sessions?page=2' });
|
|
570
|
-
await r.boot(); await settle();
|
|
571
|
-
assert.match(r.html(), /href="\/sessions\?page=2">Back to sessions</);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
test('a cross-origin referrer, a broken one, or none, falls back to the work board', async () => {
|
|
575
|
-
for (const referrer of ['', 'https://example.org/elsewhere', 'not a url']) {
|
|
576
|
-
const r = renderTask(BASE_TASK, { referrer });
|
|
546
|
+
// task 1003468 taught the record to exit to where it was opened from; task
|
|
547
|
+
// 1004477 moved that decision into shell.js, which now makes it for every hall
|
|
548
|
+
// page (tests/hall_fullscreen.mjs drives the real shell through ?from=, the
|
|
549
|
+
// referrer, the hostile values and the fallback). What stays the record's is
|
|
550
|
+
// PLACING the exit: an actions-row link in every render, the two failures
|
|
551
|
+
// included, marked [data-shell-exit] so the shell fills it with the same
|
|
552
|
+
// address and words as its top-bar Back — and asking the shell to, once the
|
|
553
|
+
// render that placed it has landed.
|
|
554
|
+
|
|
555
|
+
const EXIT_SLOT = /<a class="td-btn" data-shell-exit href="\/work">Back to tasks<\/a>/;
|
|
556
|
+
|
|
557
|
+
test('every render places one shell exit — the record and both honest failures', async () => {
|
|
558
|
+
const cases = [['record', 200], ['not found', 404], ['feed unavailable', 500]];
|
|
559
|
+
for (const [name, status] of cases) {
|
|
560
|
+
const r = renderTask(BASE_TASK, { status, shell: true });
|
|
577
561
|
await r.boot(); await settle();
|
|
578
|
-
assert.
|
|
579
|
-
assert.
|
|
562
|
+
assert.equal((r.html().match(/data-shell-exit/g) || []).length, 1, `${name}: one exit`);
|
|
563
|
+
assert.match(r.html(), EXIT_SLOT, `${name}: the static href is what a reader without the shell still gets`);
|
|
564
|
+
assert.doesNotMatch(r.html(), /Open the work board/, `${name}: the old hardcoded exit stays gone`);
|
|
565
|
+
assert.ok(r.fills.length >= 1, `${name}: the page asked the shell to fill the exit it placed`);
|
|
580
566
|
}
|
|
581
567
|
});
|
|
582
568
|
|
|
583
|
-
test('
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
'
|
|
589
|
-
];
|
|
590
|
-
for (const from of bad) {
|
|
591
|
-
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent(from) });
|
|
592
|
-
await r.boot(); await settle();
|
|
593
|
-
assert.match(r.html(), /href="\/work">Back to the work board</, `from=${JSON.stringify(from)}`);
|
|
594
|
-
assert.equal(r.el('task-back').href, '/work', `the crumb refuses from=${JSON.stringify(from)} too`);
|
|
595
|
-
}
|
|
569
|
+
test('the page no longer decides the way back: ?from= is the shell\'s to read', async () => {
|
|
570
|
+
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent('/goals#/goal/1000081'), shell: true });
|
|
571
|
+
await r.boot(); await settle();
|
|
572
|
+
assert.doesNotMatch(r.html(), /href="\/goals#\/goal\/1000081"/, 'task.js writes no origin of its own into the exit');
|
|
573
|
+
assert.doesNotMatch(TASK_SRC, /\bsafeHallPath\b|\breferrerPath\b|\bexitTarget\b|\bapplyExit\b/,
|
|
574
|
+
'the second copy of the origin logic is gone, so the two cannot drift');
|
|
596
575
|
});
|
|
597
576
|
|
|
598
|
-
test('the
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
assert.doesNotMatch(r.html(), /Open the work board/, `status ${status} still hardcodes the board`);
|
|
604
|
-
}
|
|
577
|
+
test('without a shell the record still renders, and its exit still goes somewhere', async () => {
|
|
578
|
+
const r = renderTask(BASE_TASK);
|
|
579
|
+
await r.boot(); await settle();
|
|
580
|
+
assert.match(r.html(), EXIT_SLOT);
|
|
581
|
+
assert.equal(r.fills.length, 0);
|
|
605
582
|
});
|
|
606
583
|
|
|
607
|
-
test('the crumb is
|
|
608
|
-
assert.
|
|
609
|
-
assert.match(taskHtml, /<
|
|
584
|
+
test('the crumb is the record\'s address only; the fallback rides the body', () => {
|
|
585
|
+
assert.doesNotMatch(taskHtml, /rec-back|task-back/, 'the page-drawn back link is gone');
|
|
586
|
+
assert.match(taskHtml, /<body [^>]*data-back-fallback="\/work"/, 'a deep-linked task still has a way back');
|
|
587
|
+
assert.match(taskHtml, /<p class="rec-sub" id="task-sub">/);
|
|
610
588
|
});
|
|
611
589
|
|
|
612
590
|
// ---- 11. the description is markdown, and renders as it (task 1003465) --------
|
|
@@ -92,3 +92,43 @@ test('the accessibility column is headed in plain language, not "Accessibility"'
|
|
|
92
92
|
assert.match(thead, /Plain speech/, 'the column is headed "Plain speech"');
|
|
93
93
|
assert.ok(!/>Accessibility</.test(thead), 'and does not collide with the zoom control\'s word');
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
// ---- task 1002830: the transcript reader on the Sessions page (ADR 0051) ----
|
|
97
|
+
// Same technique as above: slice the shipped source (canRead/readDoor/turnHtml)
|
|
98
|
+
// and run it, so these exercise the real text.
|
|
99
|
+
function readerFns(viewer) {
|
|
100
|
+
const from = JS.indexOf(' function canRead(s)');
|
|
101
|
+
const to = JS.indexOf(' const reader = {');
|
|
102
|
+
assert.ok(from > 0 && to > from, 'the reader helpers are still where the slice expects them');
|
|
103
|
+
const src = JS.slice(from, to);
|
|
104
|
+
const escapeHtml = (v) => String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
105
|
+
return new Function('escapeHtml', 'shortSid', 'fmtDate', 'V',
|
|
106
|
+
`let viewer = V;\n${src}\nreturn { canRead, readDoor, turnHtml };`)(escapeHtml, (s) => String(s).slice(0, 8), (d) => String(d), viewer);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test('a Read door appears only for a stored transcript the viewer may read', () => {
|
|
110
|
+
const owner = readerFns({ id: 7, rank: 'metic' });
|
|
111
|
+
assert.ok(owner.readDoor({ id: 1, session_id: 'abcdefgh1', builder_id: 7, has_transcript: true }).includes('data-read="1"'), 'own session');
|
|
112
|
+
assert.equal(owner.readDoor({ id: 2, session_id: 's', builder_id: 8, has_transcript: true }), '', "a Metic gets no door to someone else's transcript");
|
|
113
|
+
assert.equal(owner.readDoor({ id: 3, session_id: 's', builder_id: 7, has_transcript: false }), '', 'no stored transcript, no door');
|
|
114
|
+
const archon = readerFns({ id: 1, rank: 'archon' });
|
|
115
|
+
assert.ok(archon.readDoor({ id: 2, session_id: 's', builder_id: 8, has_transcript: true }).includes('data-read="2"'), 'an Archon may read any');
|
|
116
|
+
assert.equal(readerFns(null).readDoor({ id: 2, builder_id: 8, has_transcript: true }), '', 'no viewer, no door');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('transcript text is escaped — stored content is arbitrary builder + tool output', () => {
|
|
120
|
+
const { turnHtml } = readerFns({ id: 1, rank: 'archon' });
|
|
121
|
+
const html = turnHtml({
|
|
122
|
+
role: 'assistant', text: '<img src=x onerror=alert(1)>',
|
|
123
|
+
tool_calls: [{ name: '<b>Bash</b>', input: '<script>x</script>' }],
|
|
124
|
+
tool_results: [{ text: '"><svg onload=1>', is_error: true }],
|
|
125
|
+
});
|
|
126
|
+
assert.ok(!/<img|<script|<svg|<b>/i.test(html), 'no raw markup survives');
|
|
127
|
+
assert.match(html, /<img/);
|
|
128
|
+
assert.match(html, /turn__err/, 'an error result is marked');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the transcript panel lives on the SAME page, under the ledger (no second page)', () => {
|
|
132
|
+
assert.match(HTML, /id="transcript-scroll"/);
|
|
133
|
+
assert.match(JS, /\/sessions\/\$\{encodeURIComponent\(reader\.id\)\}\/transcript/);
|
|
134
|
+
});
|
|
@@ -635,12 +635,13 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-02-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-02-back/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
642
642
|
}
|
|
643
|
-
|
|
643
|
+
// task 1004477: the bar's exit is the shell's, which fills its words.
|
|
644
|
+
assert.match(HTML, /<a class="tw-back" data-shell-exit href="\/builders\/studio">Back to studio</);
|
|
644
645
|
assert.match(HTML, />submit page</);
|
|
645
646
|
assert.match(HTML, />back to the studio</);
|
|
646
647
|
});
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = 'e5094c169d5ee6ef';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
|
@@ -264,6 +264,11 @@ await t('read pages in SQL and clamps offset/limit', async () => {
|
|
|
264
264
|
await db.getSessionTranscript(5, {});
|
|
265
265
|
assert.deepEqual(captured[0].params, [5, 0, 500]);
|
|
266
266
|
});
|
|
267
|
+
await t('the ledger columns carry has_transcript from session_transcripts', async () => {
|
|
268
|
+
captured.length = 0;
|
|
269
|
+
await db.listSessionsByBuilder(7, 5);
|
|
270
|
+
assert.ok(captured[0].sql.replace(/\s+/g, ' ').includes('EXISTS (SELECT 1 FROM session_transcripts st WHERE st.session_id = sr.session_id) AS has_transcript'));
|
|
271
|
+
});
|
|
267
272
|
pool.query = origQuery;
|
|
268
273
|
|
|
269
274
|
Object.assign(auth, { requireBuilder: orig.requireBuilder, insertAuditLog: orig.insertAuditLog });
|