@bongos/core 1.21.28 → 1.21.30
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 +73 -63
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +386 -386
- package/docs/copy-registry.json +452 -452
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +462 -462
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +57 -3
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/shell.js +2 -0
- package/modules/hall-ui/public/street.html +3 -3
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +2 -0
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +7 -0
- package/modules/provisioning/routes/callback-page.js +28 -65
- package/modules/provisioning/routes/provisioning.js +6 -6
- package/modules/public-landing/public/projects.html +68 -3
- 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/craft_mode_nav.mjs +202 -0
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provisioning_callback_page.mjs +5 -5
- package/tests/provisioning_github_app_routes.mjs +24 -25
- package/tests/wizard_auto_open.mjs +94 -0
- package/tests/wizard_intent_resume.mjs +17 -2
|
@@ -38,7 +38,7 @@ test('esc renders an absent value as the empty string, never "null"/"undefined"'
|
|
|
38
38
|
assert.equal(esc(false), 'false');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('the
|
|
41
|
+
test('the hand-back variant escapes the title + domain it interpolates', () => {
|
|
42
42
|
const html = callbackPage({
|
|
43
43
|
ok: true,
|
|
44
44
|
title: 'Setting up <your> instance',
|
|
@@ -49,9 +49,9 @@ test('the progress variant escapes the title + domain it interpolates', () => {
|
|
|
49
49
|
assert.match(html, /evil"\.example\.com/, 'the domain rides through esc');
|
|
50
50
|
assert.doesNotMatch(html, /Setting up <your> instance/, 'no raw title anywhere on the page');
|
|
51
51
|
assert.doesNotMatch(html, /https:\/\/evil"\.example\.com/, 'no unescaped domain in an href');
|
|
52
|
-
// it IS the
|
|
53
|
-
assert.match(html, /id="
|
|
54
|
-
assert.match(html, /__CB_SETUP__=\{"state":"GOODSTATE","domain":"evil\\"\.example\.com"\}/);
|
|
52
|
+
// it IS the hand-back variant (task 1004548): the way back + the config block the client script reads
|
|
53
|
+
assert.match(html, /id="back" href="\/projects\?view=new"/);
|
|
54
|
+
assert.match(html, /__CB_SETUP__=\{"state":"GOODSTATE","domain":"evil\\"\.example\.com","back":"\/projects\?view=new"\}/);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test('the message variant escapes the title but passes server-authored body HTML through', () => {
|
|
@@ -63,7 +63,7 @@ test('the message variant escapes the title but passes server-authored body HTML
|
|
|
63
63
|
assert.match(html, /<h1>This setup link has <expired><\/h1>/);
|
|
64
64
|
assert.match(html, /<div class="sub"><p>Ask the owner for a fresh link\.<\/p><\/div>/,
|
|
65
65
|
'body is trusted server-authored HTML — the route composes it, a request never does');
|
|
66
|
-
assert.doesNotMatch(html, /id="
|
|
66
|
+
assert.doesNotMatch(html, /id="back"/, 'no hand-back without ok + state + domain');
|
|
67
67
|
assert.match(html, /badge-warn">⚠️</, 'a failure wears the warning badge');
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -93,44 +93,43 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
93
93
|
{
|
|
94
94
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
95
95
|
const html = await res.text();
|
|
96
|
-
t('good state → 200 text/html
|
|
96
|
+
t('good state → 200 text/html hand-back page', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /GitHub app created/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
97
97
|
t('the page names the instance host', /mercury\.example\.com/.test(html));
|
|
98
|
-
// task
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
t('
|
|
102
|
-
t('the auto-open countdown is reachable only past the pop-up guard', /var popup=!!window\.opener\|\|window\.name==='ghAppSetup';/.test(html) && /if\(!popup\)\{[^}]*return;\}\s*var n=4;\(function tick\(\)\{if\(n<=0\)\{location\.href=target;/.test(html) && (html.match(/location\.href=target/g) || []).length === 1);
|
|
98
|
+
// task 1004548: the founder no longer waits here — the page hands them back to the
|
|
99
|
+
// checklist, which shows the wait. A manual way back is always on the page.
|
|
100
|
+
t('carries a plain "back to my checklist" link (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my checklist/.test(html));
|
|
101
|
+
t('no progress stepper or countdown left on the page', !/id="steps"/.test(html) && !/Placing credentials/.test(html) && !/location\.href=target/.test(html));
|
|
103
102
|
|
|
104
103
|
// Run the served script itself (not a copy) against a minimal DOM, once as the
|
|
105
|
-
// wizard's pop-up and once as a same-tab return, and watch what it does
|
|
104
|
+
// wizard's pop-up and once as a same-tab return, and watch what it does.
|
|
106
105
|
const script = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
107
|
-
t('the
|
|
108
|
-
async function runCallback({ opener
|
|
106
|
+
t('the hand-back page ships the client script', typeof script === 'string' && script.length > 100);
|
|
107
|
+
async function runCallback({ opener }) {
|
|
109
108
|
const vm = await import('node:vm');
|
|
110
|
-
const
|
|
111
|
-
const els = { steps: el(), status: el(), cta: el(), go: el(), foot: el(), back: el() };
|
|
112
|
-
els.steps.querySelector = () => el();
|
|
113
|
-
els.back.classList.add('hidden'); els.cta.classList.add('hidden');
|
|
109
|
+
const back = { href: '/projects?view=new' };
|
|
114
110
|
const timers = [];
|
|
111
|
+
const calls = { close: 0, focusOpener: 0, replaced: null };
|
|
115
112
|
const sandbox = {
|
|
116
|
-
__CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com' },
|
|
117
|
-
opener,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
__CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com', back: '/projects?view=new' },
|
|
114
|
+
opener: opener ? { closed: false, focus: () => { calls.focusOpener++; } } : null,
|
|
115
|
+
close: () => { calls.close++; },
|
|
116
|
+
document: { getElementById: (id) => (id === 'back' ? back : null) },
|
|
117
|
+
location: { replace: (u) => { calls.replaced = u; } },
|
|
121
118
|
setTimeout: (fn) => { timers.push(fn); return timers.length; },
|
|
122
119
|
encodeURIComponent,
|
|
123
120
|
};
|
|
124
121
|
sandbox.window = sandbox;
|
|
125
122
|
vm.runInNewContext(script, sandbox);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return {
|
|
123
|
+
const replacedBeforeTimer = calls.replaced;
|
|
124
|
+
while (timers.length) timers.shift()();
|
|
125
|
+
return { ...calls, replacedBeforeTimer, backHref: back.href };
|
|
129
126
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
t('
|
|
127
|
+
const want = '/projects?view=new#ghstate=GOODSTATE';
|
|
128
|
+
const popup = await runCallback({ opener: true });
|
|
129
|
+
t('as the wizard’s pop-up it focuses the checklist tab and closes itself', popup.focusOpener === 1 && popup.close === 1 && popup.replacedBeforeTimer === null, JSON.stringify(popup));
|
|
130
|
+
t('…and if the browser refuses the close, the pop-up becomes the checklist', popup.replaced === want, JSON.stringify(popup));
|
|
131
|
+
const sameTab = await runCallback({ opener: false });
|
|
132
|
+
t('as a same-tab return it goes straight back to the checklist, carrying the attempt in the fragment', sameTab.close === 0 && sameTab.replaced === want && sameTab.backHref === want, JSON.stringify(sameTab));
|
|
134
133
|
}
|
|
135
134
|
{
|
|
136
135
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=X&state=NOPE`);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// tests/wizard_auto_open.mjs — the checklist's "Open my project automatically when it's
|
|
2
|
+
// ready" tick (task 1004548).
|
|
3
|
+
//
|
|
4
|
+
// WHY. The GitHub-App callback page used to hold the founder on its own stepper and open
|
|
5
|
+
// the instance itself. Since task 1004548 it hands them straight back to the checklist,
|
|
6
|
+
// so the auto-open moved there. The rule it must keep: open ONCE, only when sign-in is
|
|
7
|
+
// wired AND the project answers, only after a wait this page actually watched (so a
|
|
8
|
+
// revisit to a long-finished checklist never jumps away), never when the founder
|
|
9
|
+
// unticked it — and prefer a new tab, falling back to this one when the browser blocks
|
|
10
|
+
// an unprompted tab.
|
|
11
|
+
//
|
|
12
|
+
// Executed, not read: the real AUTO_OPEN_KEY…maybeAutoOpen region is sliced out of the
|
|
13
|
+
// page's inline script and run in a node:vm under stubs (the vm-slice idiom of
|
|
14
|
+
// tests/wizard_intent_resume.mjs). The slice boundaries are asserted, so a rename fails
|
|
15
|
+
// loudly instead of testing nothing.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/wizard_auto_open.mjs
|
|
18
|
+
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import vm from 'node:vm';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
28
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
29
|
+
const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
|
|
30
|
+
|
|
31
|
+
const START = SCRIPT.indexOf(" var AUTO_OPEN_KEY = 'cb-open-when-ready';");
|
|
32
|
+
const END = SCRIPT.indexOf(' /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow', START);
|
|
33
|
+
const SLICE = SCRIPT.slice(START, END);
|
|
34
|
+
|
|
35
|
+
test('the auto-open region is still findable (a rename must fail loudly)', () => {
|
|
36
|
+
assert.ok(START > 0 && END > START, 'both slice boundaries found');
|
|
37
|
+
assert.match(SLICE, /function maybeAutoOpen\(\)/);
|
|
38
|
+
assert.match(SLICE, /function autoOpenTick\(\)/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const READY = { manifest: 'done', instStatus: 'active', addrReady: true };
|
|
42
|
+
|
|
43
|
+
function run({ live = READY, armed = true, pref = null, popupBlocked = false, ticks = 1 } = {}) {
|
|
44
|
+
const store = new Map();
|
|
45
|
+
if (pref !== null) store.set('cb-open-when-ready', pref);
|
|
46
|
+
const calls = { opened: [], assigned: [] };
|
|
47
|
+
const sandbox = {
|
|
48
|
+
live: { ...live },
|
|
49
|
+
resolvedDomain: () => 'mercury.example.com',
|
|
50
|
+
localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, v) },
|
|
51
|
+
window: {
|
|
52
|
+
open: (u, t) => { calls.opened.push([u, t]); return popupBlocked ? null : { opener: 'checklist' }; },
|
|
53
|
+
location: { assign: (u) => { calls.assigned.push(u); } },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
sandbox.globalThis = sandbox;
|
|
57
|
+
vm.createContext(sandbox);
|
|
58
|
+
vm.runInContext(`${SLICE}\nautoOpenArmed = ${armed ? 'true' : 'false'};\nfor (var i = 0; i < ${ticks}; i++) maybeAutoOpen();\nvar tickHtml = autoOpenTick();`, sandbox);
|
|
59
|
+
return { calls, sandbox };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('ready + armed + ticked (the default) → opens the project ONCE, in a new tab', () => {
|
|
63
|
+
const { calls } = run({ ticks: 3 });
|
|
64
|
+
assert.deepEqual(calls.opened, [['https://mercury.example.com', '_blank']], 'one new tab, however many renders follow');
|
|
65
|
+
assert.deepEqual(calls.assigned, [], 'this checklist stays put');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a browser that blocks the unprompted tab sends THIS tab instead — the founder asked', () => {
|
|
69
|
+
const { calls } = run({ popupBlocked: true });
|
|
70
|
+
assert.deepEqual(calls.assigned, ['https://mercury.example.com']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('nothing opens until sign-in is wired AND the project answers', () => {
|
|
74
|
+
for (const live of [
|
|
75
|
+
{ ...READY, manifest: 'code_received' },
|
|
76
|
+
{ ...READY, instStatus: 'provisioning' },
|
|
77
|
+
{ ...READY, addrReady: false },
|
|
78
|
+
]) {
|
|
79
|
+
const { calls } = run({ live });
|
|
80
|
+
assert.deepEqual([calls.opened, calls.assigned], [[], []], JSON.stringify(live));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('unticked → never opens, and the tick renders unticked', () => {
|
|
85
|
+
const { calls, sandbox } = run({ pref: '0' });
|
|
86
|
+
assert.deepEqual([calls.opened, calls.assigned], [[], []]);
|
|
87
|
+
assert.doesNotMatch(sandbox.tickHtml, / checked/);
|
|
88
|
+
assert.match(run().sandbox.tickHtml, / checked>/, 'the default is ticked');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a checklist that never watched the wait (a revisit) does not jump away', () => {
|
|
92
|
+
const { calls } = run({ armed: false });
|
|
93
|
+
assert.deepEqual([calls.opened, calls.assigned], [[], []]);
|
|
94
|
+
});
|
|
@@ -70,10 +70,10 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
|
|
|
70
70
|
// A faithful miniature: the stubs record what enterCreated ASKS FOR, which is the whole
|
|
71
71
|
// contract — which panel is shown, whether the answers were reset, and what the offer
|
|
72
72
|
// above step 1 was told to name.
|
|
73
|
-
function runEnter({ focus, resumeIntent = false, draft = null, record }) {
|
|
73
|
+
function runEnter({ focus, resumeIntent = false, draft = null, record, handbackState = null }) {
|
|
74
74
|
const store = new Map();
|
|
75
75
|
if (draft) store.set('draft', JSON.stringify(draft));
|
|
76
|
-
const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
|
|
76
|
+
const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0, remembered: [] };
|
|
77
77
|
// state.step is the DONE step for the whole life of a finished standup — the value
|
|
78
78
|
// a draft-blind reading would have trusted
|
|
79
79
|
const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
|
|
@@ -83,6 +83,8 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
|
|
|
83
83
|
live: { manifest: null, appSlug: null },
|
|
84
84
|
manifestState: null,
|
|
85
85
|
manifestPending: false,
|
|
86
|
+
handbackState, // the #ghstate=… a GitHub hand-back carries (task 1004548); null on a plain arrival
|
|
87
|
+
rememberManifest(st, status) { calls.remembered.push([st, status]); },
|
|
86
88
|
sessionStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null) },
|
|
87
89
|
resetForNewProject() { calls.resets += 1; state.step = 1; state.name = ''; state.slug = ''; },
|
|
88
90
|
paintResumeOffer(c) { calls.offer.push(c ? (c.name || c.slug) : null); },
|
|
@@ -111,6 +113,19 @@ test('a reload / Back / identity settle (focus false) still resumes the done pan
|
|
|
111
113
|
assert.deepEqual(calls.offer, [null], 'the offer is down — the done panel is on stage, not step 1');
|
|
112
114
|
});
|
|
113
115
|
|
|
116
|
+
test('a hand-back from the GitHub callback page resumes watching THAT attempt (task 1004548)', () => {
|
|
117
|
+
const { calls, sandbox } = runEnter({ focus: false, record: { ...REC }, handbackState: 'ab'.repeat(32) });
|
|
118
|
+
assert.deepEqual(calls.show, [[6, false]], 'the founder lands on their checklist');
|
|
119
|
+
assert.equal(sandbox.manifestState, 'ab'.repeat(32), 'the sign-in step polls the attempt GitHub just approved');
|
|
120
|
+
assert.equal(sandbox.manifestPending, true, 'and the poll is queued for resumePolls()');
|
|
121
|
+
assert.equal(sandbox.live.manifest, 'code_received', 'it shows the wait, not "Set up GitHub sign-in" again');
|
|
122
|
+
assert.deepEqual(calls.remembered, [['ab'.repeat(32), 'code_received']], 'the attempt is kept across a reload');
|
|
123
|
+
assert.equal(sandbox.handbackState, null, 'read once — a later arrival is not a hand-back');
|
|
124
|
+
const done = runEnter({ focus: false, record: { ...REC, manifest: 'done' }, handbackState: 'cd'.repeat(32) });
|
|
125
|
+
assert.equal(done.sandbox.live.manifest, 'done', 'a finished setup is never walked back to waiting');
|
|
126
|
+
assert.equal(done.sandbox.manifestPending, false, 'and nothing is polled for it');
|
|
127
|
+
});
|
|
128
|
+
|
|
114
129
|
test('a deliberate "New project" click after a finished standup opens a FRESH step 1', () => {
|
|
115
130
|
const { calls, state } = runEnter({ focus: true, record: REC });
|
|
116
131
|
assert.equal(calls.done, 0, 'the previous project\'s done panel is not painted');
|