@bongos/core 1.19.326 → 1.19.328

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/src/module-api.js CHANGED
@@ -1079,8 +1079,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1079
1079
  // landed since 1.19.324 with no explicit bump. run 32738897943. (task 1002620)
1080
1080
  // 1.19.326 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1081
1081
  // landed since 1.19.325 with no explicit bump. run 32777788032. (task 1002620)
1082
+ // 1.19.327 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1083
+ // landed since 1.19.326 with no explicit bump. run 32782353048. (task 1002620)
1084
+ // 1.19.328 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1085
+ // landed since 1.19.327 with no explicit bump. run 32786284063. (task 1002620)
1082
1086
  // ---------------------------------------------------------------------------
1083
- const CORE_VERSION = '1.19.326'; // CI auto-patch carrier (ADR 0161); see changelog above
1087
+ const CORE_VERSION = '1.19.328'; // CI auto-patch carrier (ADR 0161); see changelog above
1084
1088
 
1085
1089
  // A namespaced logger so a module's log lines are attributable + consistent.
1086
1090
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,223 @@
1
+ // tests/projects_hub_pre_uat.mjs — the pre-UAT tweak pass over the project hub
2
+ // (task 1003232, goal 1000058). Each block pins one thing the 2026-08-24 review
3
+ // found a stranger would be misled by, so a later edit cannot quietly bring it
4
+ // back. Static analysis on purpose, like tests/projects_hub.mjs: the claims are
5
+ // about the FILE (copy, gates, order), not about a booted server.
6
+
7
+ import test from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ // LF-normalised: a Windows checkout (core.autocrlf) hands back CRLF, and the
15
+ // anchored regexes below reason about line ends
16
+ const HUB = fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
17
+ const COSMOS = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'cosmos.css'), 'utf8').replace(/\r\n/g, '\n');
18
+ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
19
+
20
+ // a function's text runs from its declaration to the next declaration in the
21
+ // file (none of the functions pinned here nest a named function of their own)
22
+ const fn = (name) => {
23
+ const start = SCRIPT.indexOf(`function ${name}(`);
24
+ assert.ok(start >= 0, `${name}() must exist`);
25
+ const rest = SCRIPT.slice(start + 1);
26
+ const next = rest.search(/\n\s*function \w+\(/);
27
+ return next < 0 ? rest : rest.slice(0, next);
28
+ };
29
+
30
+ /* ── the gate says why GitHub will ask for repository access ── */
31
+ test('the gate explains the repo scope BEFORE GitHub does — on the path every founder takes', () => {
32
+ const gate = (HUB.match(/<section class="panel" id="panel-gate"[\s\S]*?<\/section>/) || [''])[0];
33
+ assert.ok(gate, '#panel-gate exists');
34
+ const signin = gate.indexOf('id="wizSignin"');
35
+ const note = gate.indexOf('id="wizScopeNote"');
36
+ const agpl = gate.indexOf('agpl-3.0');
37
+ assert.ok(signin > 0 && note > signin && agpl > note, 'the scope note sits between the sign-in button and the AGPL line');
38
+ assert.match(gate, /all<\/em> your repositories/, 'it names GitHub’s own wording');
39
+ assert.match(gate, /Nothing else is touched/, 'and bounds what the access is used for');
40
+ assert.doesNotMatch(gate, /public_repo/, 'the stale scope comment is gone (ADR 0155 widened it to repo)');
41
+ });
42
+
43
+ /* ── the address row tells the truth in every state ── */
44
+ test('addressRow branches by state before it trusts the domain — torn down and snagged never "answer once live"', () => {
45
+ const body = fn('addressRow');
46
+ const stateFirst = body.indexOf("inst.status === 'torn_down' || inst.status === 'tearing_down'");
47
+ const domainTest = body.indexOf('if (inst.domain)');
48
+ assert.ok(stateFirst >= 0 && stateFirst < domainTest, 'the torn-down test comes before the domain test');
49
+ assert.match(body, /stays reserved for this project — it shows an error page until you bring it back/, 'torn down: the address is reserved, not answering');
50
+ assert.match(body, /if \(inst\.status === 'error'\) return 'Address <code>/, 'snagged has its own line');
51
+ assert.match(body, /nothing is answering there yet/);
52
+ assert.equal((body.match(/— it answers there once the instance is live\.'/g) || []).length, 1, 'the standup promise survives only for queued / standing-up');
53
+ assert.match(body, /We keep checking every few minutes/, 'an unreachable project says what the platform is doing');
54
+ });
55
+
56
+ test('"live" is ONE rule — chip, orb and address row all read freshUp()', () => {
57
+ assert.match(SCRIPT, /function freshUp\(inst\)/);
58
+ assert.match(fn('chipFor'), /freshUp\(inst\)/, 'the chip');
59
+ assert.match(fn('orbFor'), /!freshUp\(inst\)/, 'the orb');
60
+ const addr = fn('addressRow');
61
+ assert.match(addr, /if \(freshUp\(inst\)\) return 'Answering at '/, 'the address row claims "Answering" only when fresh');
62
+ assert.match(addr, /last answered/, 'an up-but-stale probe is dated, not asserted');
63
+ assert.doesNotMatch(addr, /if \(inst\.last_verified_up\) return 'Answering/, 'the bare truthy test is gone');
64
+ });
65
+
66
+ /* ── the queued strip is honest about which stage is current ── */
67
+ test('a queued standup lights "Request filed", not "Standing it up" — on the card and on the done panel', () => {
68
+ const strip = fn('stripFor');
69
+ assert.match(strip, /if \(s === 'requested'\) \{[\s\S]*?stripMarkup\(revive \? 'revive' : 'standup', 0, 0,/, 'card: requested → stage 0 current');
70
+ assert.match(strip, /if \(s === 'provisioning'\) \{[\s\S]*?stripMarkup\(revive \? 'revive' : 'standup', 1, 1,/, 'card: provisioning → stage 1 current');
71
+ assert.match(SCRIPT, /if \(s === 'requested'\) \{\n\s*el\.innerHTML = progressMarkup\(0, 0,/, 'done panel: requested → stage 0 current');
72
+ assert.doesNotMatch(SCRIPT, /Waiting for a runner/, '"runner" is operator vocabulary');
73
+ assert.doesNotMatch(SCRIPT, /a runner picks it up/, '"runner" is operator vocabulary (retry message)');
74
+ });
75
+
76
+ /* ── the done panel never lies at the end of its watch, and never opens a door early ── */
77
+ test('the 33-minute hand-off keeps a snag honest and keeps its flow', () => {
78
+ assert.match(SCRIPT, /if \(ticks > 400\) \{[\s\S]*?if \(live\.instStatus !== 'error'\) \{[\s\S]*?Still working[\s\S]*?watchFlow\);/, 'no "still building" over an error; the strip keeps its vocabulary');
79
+ assert.doesNotMatch(SCRIPT, /Still building — this tab will stop watching now/);
80
+ assert.match(SCRIPT, /progressMarkup\(1, 1, m\.slice\(5\), 'isErr', watchFlow\)/, 'the session-expiry message keeps its flow too');
81
+ });
82
+
83
+ test('First sign-in and Enter your builders hall unlock only when the project answers AND sign-in is wired', () => {
84
+ const first = fn('renderStepFirstSignIn');
85
+ const hall = fn('renderStepEnterHall');
86
+ for (const [name, body] of [['first-sign-in', first], ['enter-hall', hall]]) {
87
+ assert.match(body, /live\.instStatus !== 'active' \|\| live\.manifest !== 'done'/, `${name} gates on both`);
88
+ assert.match(body, /class="slocked"/, `${name} has a locked rendering`);
89
+ }
90
+ assert.match(first, /Unlocks once your project is answering and sign-in above is wired/);
91
+ });
92
+
93
+ test('the domainless build is standing, not "Live"', () => {
94
+ assert.match(SCRIPT, /progressMarkup\(-1, 2,\n\s*'The build finished — your project is standing, but it has no address yet/, 'two stages done, third unlit');
95
+ assert.doesNotMatch(SCRIPT, /has no address yet, so sign-in can’t be wired up[^']*', 'isLive'/, 'no green skin on a project that answers nowhere');
96
+ });
97
+
98
+ test('the fallback checklist carries every human step the canonical plan has', () => {
99
+ assert.match(SCRIPT, /FALLBACK_HUMAN_KEYS = \['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall'\]/);
100
+ });
101
+
102
+ /* ── the manifest leg: acknowledges GitHub, offers a way out, survives a reload ── */
103
+ test('the manifest step acknowledges code_received, offers a restart after a stall, and rides the CREATED record', () => {
104
+ assert.match(SCRIPT, /if \(live\.manifest === 'code_received'\)/, 'GitHub’s half is acknowledged');
105
+ assert.match(SCRIPT, /GitHub’s part is done/);
106
+ assert.match(SCRIPT, /manifestStale \? '<button type="button" class="linklike" data-action="manifest-start">/, 'a stalled wait gets a restart door');
107
+ assert.match(SCRIPT, /\(Date\.now\(\) - manifestStartedAt\) > 90000/, 'after ~90 seconds');
108
+ assert.match(SCRIPT, /function rememberManifest\(stateToken, status, appSlug\)/);
109
+ assert.match(SCRIPT, /rememberManifest\(d\.state, 'pending'\)/, 'the token is remembered when GitHub is opened');
110
+ assert.match(SCRIPT, /if \(created\.manifest\) \{ live\.manifest = created\.manifest; live\.appSlug = created\.app_slug \|\| null; \}/, 'enter() re-seeds the step');
111
+ assert.match(SCRIPT, /manifestState = created\.manifest_state; manifestPending = true;/, 'and resumes the poll');
112
+ });
113
+
114
+ test('"Start another project" asks before abandoning a standup still being watched', () => {
115
+ assert.match(SCRIPT, /if \(instanceId && pollTimer && !confirm\(/);
116
+ });
117
+
118
+ /* ── every recovery sign-in from the wizard keeps the repo scope ── */
119
+ test('the wizard’s 401 recoveries keep the repo scope they already granted', () => {
120
+ const plain = (SCRIPT.match(/\/auth\/web\/start\?return=\/projects%3Fview%3Dnew'\)/g) || []).length;
121
+ assert.equal(plain, 0, 'no wizard redirect drops the scope');
122
+ assert.ok((SCRIPT.match(/\/auth\/web\/start\?return=\/projects%3Fview%3Dnew&repos=1'\); throw new Error\('signing in'\)/g) || []).length >= 2, 'create + manifest');
123
+ assert.match(SCRIPT, /projects%3Fview%3Dnew&amp;repos=1">sign in again<\/a> to keep watching/, 'the progress watch');
124
+ assert.match(fn('signInAgain'), /currentView === 'new' \? '\/projects%3Fview%3Dnew&amp;repos=1'/, 'signInAgain from the wizard');
125
+ });
126
+
127
+ test('a resumed draft past Home still probes repo access before Create', () => {
128
+ assert.match(SCRIPT, /else if \(step > 3 && step < 6 && authKnown && repoAccess === null\) ensureRepoAccess\(\);/);
129
+ });
130
+
131
+ test('a half-typed custom domain says why Next is dead; a taken handle silences the format cheer', () => {
132
+ assert.match(SCRIPT, /That doesn’t look like a domain yet \(e\.g\. mercury\.example\.com\) — fix it, or clear the field to use the address above\./);
133
+ assert.match(SCRIPT, /if \(avail === 'taken' \|\| avail === 'yours' \|\| avail === 'reserved'\) \{ var hs = \$\('h-slug'\); hs\.innerHTML = '&nbsp;'; hs\.className = 'hint'; \}/);
134
+ });
135
+
136
+ /* ── owner voice ── */
137
+ test('no wall-clock promises, no operator vocabulary on the stranger’s path', () => {
138
+ assert.doesNotMatch(HUB, /about five minutes/, 'the empty state');
139
+ assert.doesNotMatch(HUB, /a runner does the real work/, 'the mine fine print');
140
+ assert.doesNotMatch(SCRIPT, /pinned platform core/, 'the review Shape row');
141
+ assert.match(SCRIPT, /\['Shape', 'Its own server, running the platform’s current release'\]/);
142
+ assert.doesNotMatch(HUB, /\/new-project<\/code> assistant/, 'the done panel’s closing line');
143
+ assert.match(HUB, /Sign-in switches on after the GitHub steps|switch sign-in on/, 'the address step does not promise sign-in "just works"');
144
+ assert.doesNotMatch(HUB, /Sign-in, sharing, everything just works/);
145
+ });
146
+
147
+ /* ── the manage view carries the card’s story, and its working sections come first ── */
148
+ test('the manage view renders the strip, the snag and the doors from the same helpers the card uses', () => {
149
+ for (const id of ['mStrip', 'mErr', 'mMsg', 'mActs', 'mTitle']) assert.match(HUB, new RegExp(`id="${id}"`), `#${id} is real markup`);
150
+ const rm = fn('renderManage');
151
+ assert.match(rm, /\$\('mStrip'\)\.innerHTML = stripFor\(inst\)/);
152
+ assert.match(rm, /errMarkup\(inst\)/);
153
+ assert.match(rm, /canRetry\(inst\)[\s\S]*?data-m-retry/);
154
+ assert.match(rm, /canRevive\(inst\)[\s\S]*?data-m-revive/);
155
+ assert.match(rm, /Open site →/);
156
+ assert.match(rm, /document\.title = \(inst\.slug \? inst\.slug \+ ' — ' : ''\) \+ BASE_TITLE/, 'the tab names the project');
157
+ assert.match(SCRIPT, /function mReProvision\(verb\)/);
158
+ });
159
+
160
+ test('an in-flight project is watched on its own page; a settled one keeps the C8 rule', () => {
161
+ const tick = fn('manageWatchTick');
162
+ assert.match(tick, /document\.hidden \|\| currentView !== 'manage' \|\| !manageInst \|\| !shouldWatch\(manageInst\)/, 'guarded on stage + in flight');
163
+ assert.match(tick, /loadManage\(true, true\)/, 'quiet, and without re-asking GitHub');
164
+ assert.match(SCRIPT, /if \(!keepRepo\) loadRepoVisibility\(inst\);/, 'the GitHub read stays on entry + tab return');
165
+ assert.match(SCRIPT, /setInterval\(manageWatchTick, 5000\)/);
166
+ });
167
+
168
+ test('working sections come before the not-wired ones on manage, and the placeholders state today’s behavior', () => {
169
+ const repo = HUB.indexOf('id="mRepoVisibility"');
170
+ const vis = HUB.indexOf('id="mVisibility"');
171
+ const join = HUB.indexOf('id="mJoinability"');
172
+ const del = HUB.indexOf('id="mDelete"');
173
+ assert.ok(repo > 0 && repo < vis && vis < join && join < del, 'repo visibility → who can see it → how people join → delete');
174
+ assert.match(HUB, /For now, every project’s pages are open to everyone\./);
175
+ assert.match(HUB, /For now, people apply to join and you invite them\./);
176
+ assert.doesNotMatch(HUB, /It lands in one of the next updates/);
177
+ });
178
+
179
+ test('a live private-repo project is not told it is broken', () => {
180
+ const rv = fn('renderRepoVis');
181
+ assert.match(rv, /var snagged = !!\(manageInst && \(manageInst\.status === 'error' \|\| manageInst\.error_note\)\)/);
182
+ assert.match(rv, /snagged\n\s*\? '<div class="projMsg err">/, 'red only on a snag');
183
+ assert.match(rv, /Nothing to do here\./, 'a healthy project reads calm');
184
+ assert.match(rv, /class="btn small ghost" data-repo-public/, 'Make it public is no longer the loudest control');
185
+ assert.doesNotMatch(rv, /retry the standup from your projects page/, 'no pointer at a button that does not exist');
186
+ });
187
+
188
+ /* ── the shell: Back works, the pill stays lit, phones and laptops keep the header on one row ── */
189
+ test('Back moves between views — a deliberate view change is a history entry', () => {
190
+ assert.match(SCRIPT, /history\.pushState\(null, '', '\?view=' \+ v \+ idPart\)/);
191
+ assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v \+ idPart\)/, 'boot + background settles still rewrite in place');
192
+ assert.match(SCRIPT, /window\.addEventListener\('popstate', function \(\) \{ setView\(viewFromUrl\(\)\); \}\)/);
193
+ assert.match(SCRIPT, /else if \(v === 'manage' && a\.dataset\.nav === 'mine'\) a\.setAttribute\('aria-current', 'page'\)/, 'manage keeps the Your-projects pill lit');
194
+ });
195
+
196
+ test('the signed-in header shrinks inside a phone and stays one row on a laptop', () => {
197
+ assert.match(COSMOS, /@media \(max-width:1079px\)\{[\s\S]*?\.shell-right\{flex:0 1 auto;min-width:0;max-width:100%;/, 'the cluster may shrink and wrap');
198
+ assert.match(COSMOS, /@media \(min-width:1080px\)\{[\s\S]*?\.shell \.navrow\{position:absolute;left:50%;top:50%;transform:translate\(-50%,-50%\);\}/, 'the nav is centred absolutely, flanks size to content');
199
+ assert.doesNotMatch(COSMOS, /@media \(max-width:820px\)\{\n\s*\.shell\{justify-content:center;\}/, 'no band is left between the two rules');
200
+ assert.match(COSMOS, /\.who>span\{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:14ch;\}/, 'a long login truncates');
201
+ assert.match(COSMOS, /\.search:focus-within\{box-shadow:/, 'keyboard focus on the search pills is visible');
202
+ assert.match(COSMOS, /\.mineBar \.search \.count\{display:none;\}/, 'the count leaves the placeholder alone at 390px');
203
+ assert.match(COSMOS, /\.projMeta>span\+span::before\{content:'·'/, 'the meta separator rides its item');
204
+ assert.match(HUB, /#view-mine \.hero,#view-manage \.hero\{max-width:none;\}/, 'titles align with their cards');
205
+ });
206
+
207
+ test('card metadata is span-wrapped so a separator can never strand', () => {
208
+ assert.match(fn('renderCard'), /meta\.map\(function \(m\) \{ return '<span>' \+ m \+ '<\/span>'; \}\)\.join\(''\)/);
209
+ assert.match(fn('renderManage'), /meta\.map\(function \(m\) \{ return '<span>' \+ m \+ '<\/span>'; \}\)\.join\(''\)/);
210
+ });
211
+
212
+ test('an open teardown confirm survives the minute re-render', () => {
213
+ assert.match(SCRIPT, /var cardAsking = null;/);
214
+ assert.match(fn('renderCard'), /if \(cardAsking !== null && cardAsking === String\(inst\.id\)\) return;/);
215
+ assert.match(fn('askTeardown'), /cardAsking = String\(id\);/);
216
+ assert.match(SCRIPT, /function unAskTeardown\(id\) \{ cardAsking = null;/);
217
+ });
218
+
219
+ test('the inputs a stranger types into are labelled', () => {
220
+ assert.match(HUB, /id="ghSearch"[^>]*aria-label="Search your repositories"/);
221
+ assert.match(HUB, /id="ghNewName"[^>]*aria-label="New repository name"/);
222
+ assert.match(SCRIPT, /aria-label="A domain you own, for ' \+ esc\(inst\.slug\) \+ '"/);
223
+ });