@bongos/core 1.19.583 → 1.19.585
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 +25 -15
- package/docs/copy-inventory.md +34 -33
- package/docs/copy-registry.json +52 -43
- package/docs/module-api-changelog.md +4 -0
- package/modules/public-landing/public/assets/cosmos.css +64 -2
- package/modules/public-landing/public/projects.html +105 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hub_map_orb_states.mjs +255 -0
- package/tests/wizard_draft_resume.mjs +194 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.585",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.585'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// tests/hub_map_orb_states.mjs — the hub map draws the VISIBILITY render-type,
|
|
2
|
+
// EXECUTED (BV1.R09, task 1002328; ADR 0182 D1/D2a, criterion proj-visibility-states).
|
|
3
|
+
//
|
|
4
|
+
// R06 (task 1002325) made the feed answer what each project draws as — `orb` is
|
|
5
|
+
// planet / star / black-hole, derived server-side from the join door, with stealth
|
|
6
|
+
// rows redacted before they leave the server and dark matter excluded in SQL. R07
|
|
7
|
+
// (task 1002326) dogfooded it on the Explore page. This file pins the surface a
|
|
8
|
+
// visitor actually lands on: the hub map at modules/public-landing/public/projects.html,
|
|
9
|
+
// in both its views — the sky and the table.
|
|
10
|
+
//
|
|
11
|
+
// The original R09 scoped this to a separate-repo static gate site; that gate was
|
|
12
|
+
// retired (src/platform-server.js: the apex 302 into /gate/ "is gone with the gate
|
|
13
|
+
// itself"), and the orb sky became a view inside the hub. So the criterion's last
|
|
14
|
+
// clause lands here.
|
|
15
|
+
//
|
|
16
|
+
// Harness: the tests/project_door_ui.mjs pattern — there is no jsdom in this repo,
|
|
17
|
+
// so the real region is sliced out of the page verbatim, run in a vm under a narrow
|
|
18
|
+
// DOM stub, and the markup it writes is parsed back. Both slice boundaries are
|
|
19
|
+
// asserted so a rename can never leave this file executing an empty string.
|
|
20
|
+
//
|
|
21
|
+
// What it proves, and why each matters:
|
|
22
|
+
// • the three states draw as three different things — a private project stopped
|
|
23
|
+
// being indistinguishable from a public one
|
|
24
|
+
// • a black hole is drawn from NOTHING but its existence: no name, no address,
|
|
25
|
+
// no per-project artwork, and nothing to click. The server already redacted the
|
|
26
|
+
// row; this asserts the page cannot re-identify it from what it kept
|
|
27
|
+
// • an unrecognised render-type is DROPPED, not drawn — the fail-closed choice
|
|
28
|
+
// projects-feed.js makes for the same question, so a fourth state added later
|
|
29
|
+
// can never inherit the public planet
|
|
30
|
+
// • the mapping is never re-derived client-side from `state` — one copy, server-side
|
|
31
|
+
// • the search box cannot surface a black hole
|
|
32
|
+
// • the viewer's OWN provisioning rows, which carry no feed `orb`, still draw —
|
|
33
|
+
// fail-closed must not delete the owner's own sky
|
|
34
|
+
//
|
|
35
|
+
// Run: node --test --test-reporter=tap tests/hub_map_orb_states.mjs
|
|
36
|
+
|
|
37
|
+
import assert from 'node:assert/strict';
|
|
38
|
+
import { test } from 'node:test';
|
|
39
|
+
import fs from 'node:fs';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import vm from 'node:vm';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
|
|
44
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
45
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
46
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8');
|
|
47
|
+
const SCRIPT = (HUB.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/) || [, ''])[1];
|
|
48
|
+
|
|
49
|
+
// ── the region under test, sliced verbatim ───────────────────────────────────
|
|
50
|
+
const START = SCRIPT.indexOf('var ORB_SLOTS = [');
|
|
51
|
+
const END = SCRIPT.indexOf('function applyMapFilter(');
|
|
52
|
+
assert.ok(START > 0, 'the map-render region must start at ORB_SLOTS');
|
|
53
|
+
assert.ok(END > START, 'the map-render region must end before applyMapFilter');
|
|
54
|
+
const REGION = SCRIPT.slice(START, END);
|
|
55
|
+
for (const fn of ['var ORB_STATE = {', 'function drawableCatalogRow(', 'function renderOrbs(', 'function renderRows(']) {
|
|
56
|
+
assert.ok(REGION.includes(fn), `the region must carry ${fn}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// planetAttrs() lives further down the page (it is shared with the manage cards),
|
|
60
|
+
// so it is sliced in rather than re-typed — a hand-copied stub would drift from the
|
|
61
|
+
// thing whose output this file asserts about.
|
|
62
|
+
const PA_START = SCRIPT.indexOf('function planetAttrs(');
|
|
63
|
+
const PA_END = SCRIPT.indexOf('var PLANET_SHAPE_WORDS');
|
|
64
|
+
assert.ok(PA_START > 0 && PA_END > PA_START, 'planetAttrs must still be sliceable');
|
|
65
|
+
const PLANET_ATTRS = SCRIPT.slice(PA_START, PA_END);
|
|
66
|
+
|
|
67
|
+
// ── the DOM stub ─────────────────────────────────────────────────────────────
|
|
68
|
+
// Narrow on purpose: it supports exactly what the region touches, so it stays
|
|
69
|
+
// honest about what it can prove.
|
|
70
|
+
|
|
71
|
+
function makeNode(tag) {
|
|
72
|
+
return {
|
|
73
|
+
tagName: String(tag).toUpperCase(),
|
|
74
|
+
_attrs: {},
|
|
75
|
+
_children: [],
|
|
76
|
+
className: '',
|
|
77
|
+
tabIndex: 0,
|
|
78
|
+
style: { cssText: '' },
|
|
79
|
+
innerHTML: '',
|
|
80
|
+
setAttribute(k, v) { this._attrs[k] = String(v); },
|
|
81
|
+
getAttribute(k) { return Object.prototype.hasOwnProperty.call(this._attrs, k) ? this._attrs[k] : null; },
|
|
82
|
+
hasAttribute(k) { return Object.prototype.hasOwnProperty.call(this._attrs, k); },
|
|
83
|
+
appendChild(n) { this._children.push(n); return n; },
|
|
84
|
+
querySelectorAll() { return []; },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function boot() {
|
|
89
|
+
const sky = makeNode('div');
|
|
90
|
+
let rowsHtml = '';
|
|
91
|
+
const rows = makeNode('ul');
|
|
92
|
+
rows.insertAdjacentHTML = (_where, html) => { rowsHtml += html; };
|
|
93
|
+
|
|
94
|
+
const els = { skyCam: sky, mapRows: rows };
|
|
95
|
+
const sandbox = {
|
|
96
|
+
// safeHttpUrl() (inside the region) scheme-checks through the URL parser — a vm
|
|
97
|
+
// context has no globals of its own, so it is handed the real one
|
|
98
|
+
URL,
|
|
99
|
+
document: { createElement: makeNode, getElementById: (id) => els[id] || null },
|
|
100
|
+
$: (id) => els[id] || null,
|
|
101
|
+
esc: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
102
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
103
|
+
productName: 'Cloud Bongos',
|
|
104
|
+
// the surrounding page's own helpers, narrowed to what the region reads back
|
|
105
|
+
chipFor: () => '<span class="chip"></span>',
|
|
106
|
+
hallGlyphHtml: () => '',
|
|
107
|
+
shippedFigure: () => '',
|
|
108
|
+
listedDate: (iso) => String(iso),
|
|
109
|
+
};
|
|
110
|
+
vm.createContext(sandbox);
|
|
111
|
+
vm.runInContext(PLANET_ATTRS + '\n' + REGION, sandbox);
|
|
112
|
+
return {
|
|
113
|
+
sandbox,
|
|
114
|
+
sky,
|
|
115
|
+
rowsHtml: () => rowsHtml,
|
|
116
|
+
orbs: () => sky._children,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A feed row as the server actually shapes it (projects-feed.js): `state` and `orb`
|
|
121
|
+
// on every row, and a stealth row nulled down to those two keys.
|
|
122
|
+
const PUBLIC_ROW = {
|
|
123
|
+
origin: 'https://demo.example.com', name: 'Demo', tagline: 'a demo project',
|
|
124
|
+
art_url: null, created_at: '2026-01-01T00:00:00.000Z', builders: 3, tags: [],
|
|
125
|
+
type: null, state: 'public', orb: 'planet',
|
|
126
|
+
};
|
|
127
|
+
const PRIVATE_ROW = {
|
|
128
|
+
origin: 'https://circles.example.com', name: 'Circles', tagline: 'invite only',
|
|
129
|
+
art_url: null, created_at: '2026-01-02T00:00:00.000Z', builders: 2, tags: [],
|
|
130
|
+
type: null, state: 'private', orb: 'star',
|
|
131
|
+
};
|
|
132
|
+
const STEALTH_ROW = {
|
|
133
|
+
origin: null, name: null, tagline: null, art_url: null, created_at: null,
|
|
134
|
+
builders: null, tags: null, type: null, state: 'stealth', orb: 'black-hole',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
test('the three states draw as three different orbs', () => {
|
|
138
|
+
const h = boot();
|
|
139
|
+
h.sandbox.renderOrbs([PUBLIC_ROW, PRIVATE_ROW, STEALTH_ROW], []);
|
|
140
|
+
const drawn = h.orbs();
|
|
141
|
+
assert.equal(drawn.length, 3, 'every drawable row reaches the sky');
|
|
142
|
+
assert.deepEqual(drawn.map((n) => n.getAttribute('data-orb')),
|
|
143
|
+
['planet', 'star', 'black-hole'],
|
|
144
|
+
'each orb carries the render-type the feed derived');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('a public project keeps its planet: name, address and artwork', () => {
|
|
148
|
+
const h = boot();
|
|
149
|
+
h.sandbox.renderOrbs([PUBLIC_ROW], []);
|
|
150
|
+
const [orb] = h.orbs();
|
|
151
|
+
assert.equal(orb.tagName, 'A', 'a public project is a real link');
|
|
152
|
+
assert.equal(orb.href, 'https://demo.example.com/');
|
|
153
|
+
assert.match(orb.innerHTML, /Demo/, 'it is named');
|
|
154
|
+
assert.ok(orb.getAttribute('data-template'), 'it carries its planet artwork');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a private project is a star, and is still itself', () => {
|
|
158
|
+
const h = boot();
|
|
159
|
+
h.sandbox.renderOrbs([PRIVATE_ROW], []);
|
|
160
|
+
const [orb] = h.orbs();
|
|
161
|
+
assert.equal(orb.getAttribute('data-orb'), 'star');
|
|
162
|
+
// identified, per ADR 0182 D1: private is on the map UNDER ITS NAME — what
|
|
163
|
+
// changes is the door, not the anonymity
|
|
164
|
+
assert.match(orb.innerHTML, /Circles/);
|
|
165
|
+
assert.equal(orb.href, 'https://circles.example.com/');
|
|
166
|
+
assert.ok(orb.getAttribute('data-template'), 'it keeps its planet artwork');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('a black hole is existence without identity — nothing to read, nothing to click', () => {
|
|
170
|
+
const h = boot();
|
|
171
|
+
h.sandbox.renderOrbs([STEALTH_ROW], []);
|
|
172
|
+
const [orb] = h.orbs();
|
|
173
|
+
assert.equal(orb.getAttribute('data-orb'), 'black-hole');
|
|
174
|
+
assert.notEqual(orb.tagName, 'A', 'a black hole is not a link — there is nowhere to go');
|
|
175
|
+
assert.ok(!orb.href, 'and it carries no address');
|
|
176
|
+
assert.equal(orb.getAttribute('data-origin'), null, 'not even as a data attribute');
|
|
177
|
+
// no per-project artwork: the shape/material walk is an IDENTITY the page
|
|
178
|
+
// assigns, and assigning one to a black hole would make interchangeable rows
|
|
179
|
+
// individually recognisable across snapshots
|
|
180
|
+
for (const a of ['data-template', 'data-shape', 'data-material', 'data-style']) {
|
|
181
|
+
assert.equal(orb.getAttribute(a), null, `a black hole must not carry ${a}`);
|
|
182
|
+
}
|
|
183
|
+
assert.doesNotMatch(orb.innerHTML, /class="badge"|class="chip"/, 'no badge, no liveness chip');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('an unrecognised render-type is dropped, not drawn (fail-closed)', () => {
|
|
187
|
+
const { sandbox } = boot();
|
|
188
|
+
for (const orb of ['planet', 'star', 'black-hole']) {
|
|
189
|
+
assert.equal(sandbox.drawableCatalogRow({ orb }), true, `${orb} is drawable`);
|
|
190
|
+
}
|
|
191
|
+
// the shape of the risk: a fourth state shipped by a newer server, read by an
|
|
192
|
+
// older page. It must not inherit the public planet.
|
|
193
|
+
for (const orb of ['wormhole', 'nebula', '', null, undefined]) {
|
|
194
|
+
assert.equal(sandbox.drawableCatalogRow({ orb }), false,
|
|
195
|
+
`an unknown render-type (${String(orb)}) is not drawn`);
|
|
196
|
+
}
|
|
197
|
+
assert.equal(sandbox.drawableCatalogRow({}), false, 'a row with no render-type at all is not drawn');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('the drop is wired into the browse list, not left as an orphan predicate', () => {
|
|
201
|
+
// behaviour above proves the predicate; this proves the page actually asks it,
|
|
202
|
+
// and asks it of the CATALOG (the viewer's own provisioning rows carry no feed
|
|
203
|
+
// `orb` and must survive)
|
|
204
|
+
assert.match(SCRIPT, /catalog\.filter\(drawableCatalogRow\)/,
|
|
205
|
+
'applyMapFilter must filter the catalog through drawableCatalogRow');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("the viewer's own provisioning rows still draw — fail-closed must not empty the owner's sky", () => {
|
|
209
|
+
const h = boot();
|
|
210
|
+
// the synthetic row applyMapFilter pushes for an owned instance the catalog
|
|
211
|
+
// does not carry: no feed `orb`, because it never came from the feed
|
|
212
|
+
const owned = {
|
|
213
|
+
name: 'my-project', tagline: 'my-project.example.com',
|
|
214
|
+
origin: 'https://my-project.example.com', builders: null, tags: null, type: null,
|
|
215
|
+
_inst: { id: '42', slug: 'my-project', domain: 'my-project.example.com' },
|
|
216
|
+
};
|
|
217
|
+
h.sandbox.renderOrbs([owned], []);
|
|
218
|
+
const [orb] = h.orbs();
|
|
219
|
+
assert.equal(h.orbs().length, 1, 'an owned-only entry is drawn');
|
|
220
|
+
assert.equal(orb.getAttribute('data-orb'), null, 'and is not labelled with a state it never claimed');
|
|
221
|
+
assert.ok(orb.getAttribute('data-template'), 'it keeps its slug-walked planet');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('the state→orb mapping is read from the feed, never re-derived here', () => {
|
|
225
|
+
// projects-feed.js owns ORB_BY_STATE. A second copy on the client is a copy
|
|
226
|
+
// that drifts — the explicit rule R07 already follows in explore.js.
|
|
227
|
+
assert.doesNotMatch(REGION, /['"]public['"]\s*:\s*['"]planet['"]/,
|
|
228
|
+
'the page must not carry its own state→orb table');
|
|
229
|
+
assert.match(REGION, /p\.orb|\bORB_STATE\b/, 'it reads the feed’s own orb field');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a black hole cannot be surfaced by the search box', () => {
|
|
233
|
+
const { sandbox } = boot();
|
|
234
|
+
// it publishes no text, so there is nothing for a query to match — asserted
|
|
235
|
+
// rather than assumed, because a future row that gained a searchable field
|
|
236
|
+
// would silently make stealth projects probeable by guessing names
|
|
237
|
+
for (const q of ['a', 'project', 'demo', 'circles']) {
|
|
238
|
+
assert.equal(sandbox.matchesMapQuery(STEALTH_ROW, q), false,
|
|
239
|
+
`a query (${q}) must never match a black hole`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('the table view says the same three things as the sky', () => {
|
|
244
|
+
const h = boot();
|
|
245
|
+
h.sandbox.renderRows([PUBLIC_ROW, PRIVATE_ROW, STEALTH_ROW]);
|
|
246
|
+
const html = h.rowsHtml();
|
|
247
|
+
for (const orb of ['planet', 'star', 'black-hole']) {
|
|
248
|
+
assert.ok(html.includes('data-orb="' + orb + '"'), `the table marks its ${orb} row`);
|
|
249
|
+
}
|
|
250
|
+
// the anonymous row is anonymous HERE too: the table is a peer view of the map,
|
|
251
|
+
// not a back door around D2a
|
|
252
|
+
const anon = html.slice(html.indexOf('data-orb="black-hole"'));
|
|
253
|
+
assert.doesNotMatch(anon, /class="rowGo"/, 'a black hole has no "view" door');
|
|
254
|
+
assert.doesNotMatch(anon, /data-template=/, 'and no per-project artwork');
|
|
255
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// tests/wizard_draft_resume.mjs — the create wizard survives a round-trip through GitHub
|
|
2
|
+
// (task 1003683).
|
|
3
|
+
//
|
|
4
|
+
// Two defects on the Home step, both found while walking the R24 proof on live:
|
|
5
|
+
//
|
|
6
|
+
// 1. EVERY boot reset the draft to step 1. enter() deliberately renders the name step
|
|
7
|
+
// before /me settles ("deeper steps carry authenticated side effects"), and its own
|
|
8
|
+
// comment promises "onAuth() re-enters to the real draft step the moment /me answers".
|
|
9
|
+
// But show() PERSISTS whatever step it renders, so the placeholder overwrote the
|
|
10
|
+
// draft's real position with 1 — leaving onAuth() nothing to return to. Granting repo
|
|
11
|
+
// access from step 5 therefore dumped the visitor back at the name step, every time.
|
|
12
|
+
// Reproduced live on core 1.19.580: planting {step:5} and reloading read back step 1.
|
|
13
|
+
//
|
|
14
|
+
// 2. "or enter it manually" was a ONE-WAY door. #ghConnect renders only while repoAccess
|
|
15
|
+
// is 'need-connect', the toggle set repoAccess='manual', and nothing ever set it back
|
|
16
|
+
// — so the "Allow repo access" button vanished until a full page reload. Navigating
|
|
17
|
+
// back to step 4 and forward again did not restore it.
|
|
18
|
+
//
|
|
19
|
+
// Harness: there is no jsdom in this repo, so this follows tests/project_door_ui.mjs —
|
|
20
|
+
// slice the real region out of the page's INLINE script and run it in a vm under a minimal
|
|
21
|
+
// DOM stub. The slice boundaries are asserted, so a rename can never leave this file
|
|
22
|
+
// quietly testing nothing.
|
|
23
|
+
//
|
|
24
|
+
// NOT covered here, deliberately: the team shape. The first draft of this task claimed the
|
|
25
|
+
// OAuth hop dropped it; that was wrong — teamShape, projType and modules all survive a
|
|
26
|
+
// reload (verified live). What looked like a drop was the documented "clicking the chosen
|
|
27
|
+
// pill again UNSAYS it" toggle being re-clicked by hand.
|
|
28
|
+
//
|
|
29
|
+
// Run: node --test --test-reporter=tap tests/wizard_draft_resume.mjs
|
|
30
|
+
|
|
31
|
+
import assert from 'node:assert/strict';
|
|
32
|
+
import { test } from 'node:test';
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import vm from 'node:vm';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
|
|
40
|
+
|
|
41
|
+
// ── slice 1 · the step-resolution block inside enter() ────────────────────────────
|
|
42
|
+
const STEP_RE = /var step = authKnown &&[\s\S]*?show\(step, !!focus\);\r?\n\s*\}/;
|
|
43
|
+
const stepSlice = (HUB.match(STEP_RE) || [null])[0];
|
|
44
|
+
|
|
45
|
+
test('the step-resolution slice is still findable (a rename must fail loudly, not silently pass)', () => {
|
|
46
|
+
assert.ok(stepSlice, 'enter()\'s step-resolution block was not found in projects.html');
|
|
47
|
+
assert.match(stepSlice, /show\(1, false\)/, 'the pre-auth placeholder render is part of the slice');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// A faithful miniature of the real show(): the ONE behaviour that matters here is that it
|
|
51
|
+
// writes state.step and then persists the whole state. If show() ever stops persisting,
|
|
52
|
+
// this test still passes for the right reason — the draft keeps the real step either way.
|
|
53
|
+
function runStep({ authKnown, draftStep }) {
|
|
54
|
+
const store = new Map();
|
|
55
|
+
const state = { step: draftStep, name: 'probe', teamShape: 'small-team' };
|
|
56
|
+
store.set('draft', JSON.stringify(state));
|
|
57
|
+
const rendered = [];
|
|
58
|
+
const sandbox = {
|
|
59
|
+
authKnown,
|
|
60
|
+
state,
|
|
61
|
+
focus: false,
|
|
62
|
+
saveDraft() { store.set('draft', JSON.stringify(state)); },
|
|
63
|
+
show(step) { state.step = step; rendered.push(step); sandbox.saveDraft(); },
|
|
64
|
+
$: () => ({ focus() {} }),
|
|
65
|
+
};
|
|
66
|
+
sandbox.globalThis = sandbox;
|
|
67
|
+
vm.createContext(sandbox);
|
|
68
|
+
vm.runInContext(stepSlice, sandbox);
|
|
69
|
+
return { rendered, persisted: JSON.parse(store.get('draft')), state };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test('pre-auth: the placeholder renders step 1 but the draft KEEPS the real step', () => {
|
|
73
|
+
const { rendered, persisted } = runStep({ authKnown: false, draftStep: 5 });
|
|
74
|
+
assert.deepEqual(rendered, [1], 'still renders the name step before identity settles');
|
|
75
|
+
assert.equal(persisted.step, 5, 'the draft must not be overwritten with the placeholder');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('pre-auth: the in-memory step is restored too, so onAuth() can re-enter to it', () => {
|
|
79
|
+
// onAuth() calls enter() again and reads state.step from MEMORY, not from storage — so
|
|
80
|
+
// restoring only the stored copy would fix nothing.
|
|
81
|
+
const { state } = runStep({ authKnown: false, draftStep: 6 });
|
|
82
|
+
assert.equal(state.step, 6);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('post-auth: the real step is rendered', () => {
|
|
86
|
+
const { rendered, persisted } = runStep({ authKnown: true, draftStep: 5 });
|
|
87
|
+
assert.deepEqual(rendered, [5]);
|
|
88
|
+
assert.equal(persisted.step, 5);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a draft already at step 1 is unchanged, and an out-of-range step is not resurrected', () => {
|
|
92
|
+
assert.equal(runStep({ authKnown: false, draftStep: 1 }).persisted.step, 1);
|
|
93
|
+
// 8 is the done panel, reached by its own branch above this slice; 0 is the gate. Neither
|
|
94
|
+
// is a resumable wizard position, so the placeholder is allowed to stand.
|
|
95
|
+
for (const bad of [0, 8, 99]) {
|
|
96
|
+
assert.equal(runStep({ authKnown: false, draftStep: bad }).persisted.step, 1, `step ${bad} is not resumed`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── slice 2 · syncHome's control over the way back ────────────────────────────────
|
|
101
|
+
const SYNC_RE = /function syncHome\(\) \{[\s\S]*?\n {4}\}/;
|
|
102
|
+
const syncSlice = (HUB.match(SYNC_RE) || [null])[0];
|
|
103
|
+
|
|
104
|
+
test('the syncHome slice is still findable', () => {
|
|
105
|
+
assert.ok(syncSlice, 'syncHome() was not found in projects.html');
|
|
106
|
+
assert.match(syncSlice, /ghManualBack/, 'syncHome governs the way-back control');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function runSync(repoAccess) {
|
|
110
|
+
const els = {};
|
|
111
|
+
// Elements start HIDDEN so that "shown" is only ever the result of syncHome setting it.
|
|
112
|
+
// With a `hidden: false` default, a control syncHome never mentions reads as visible and
|
|
113
|
+
// the assertion below would pass against the unfixed page.
|
|
114
|
+
const el = (id) => (els[id] ||= {
|
|
115
|
+
id, hidden: true, textContent: '', _on: false,
|
|
116
|
+
classList: { toggle(_c, v) { els[id]._on = !!v; } },
|
|
117
|
+
});
|
|
118
|
+
const sandbox = {
|
|
119
|
+
repoAccess,
|
|
120
|
+
state: { mode: 'greenfield' },
|
|
121
|
+
$: el,
|
|
122
|
+
isManualHome: () => repoAccess === 'manual' || repoAccess === 'unavailable',
|
|
123
|
+
initAdoptPicker() {}, syncFresh() {},
|
|
124
|
+
};
|
|
125
|
+
sandbox.globalThis = sandbox;
|
|
126
|
+
vm.createContext(sandbox);
|
|
127
|
+
vm.runInContext(syncSlice + '\nsyncHome();', sandbox);
|
|
128
|
+
return { connectShown: el('ghConnect')._on, manualShown: !el('manualHome').hidden, backShown: !el('ghManualBack').hidden };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test('choosing manual leaves a way back to the connect offer', () => {
|
|
132
|
+
const manual = runSync('manual');
|
|
133
|
+
assert.equal(manual.manualShown, true, 'the manual fields are up');
|
|
134
|
+
assert.equal(manual.connectShown, false, 'the connect box is down — that is what made it a trap');
|
|
135
|
+
assert.equal(manual.backShown, true, 'and the way back is offered');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('a FORCED fallback offers no way back, because connecting cannot help', () => {
|
|
139
|
+
// 'unavailable' means the server could not store a token. Offering "use my GitHub
|
|
140
|
+
// repositories instead" there would send the visitor through a round-trip that lands
|
|
141
|
+
// them right back here — an offer that cannot be honoured is worse than none.
|
|
142
|
+
const forced = runSync('unavailable');
|
|
143
|
+
assert.equal(forced.manualShown, true);
|
|
144
|
+
assert.equal(forced.backShown, false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('while the connect offer is up, the way back is not', () => {
|
|
148
|
+
const need = runSync('need-connect');
|
|
149
|
+
assert.equal(need.connectShown, true);
|
|
150
|
+
assert.equal(need.backShown, false, 'no second, redundant control beside the button itself');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── slice 3 · why this task's original team-shape claim was wrong ─────────────────
|
|
154
|
+
// The bug was filed claiming a GitHub round-trip drops the team shape. It does not: a
|
|
155
|
+
// planted draft reads teamShape back intact after a reload (verified live on 1.19.580).
|
|
156
|
+
// The real explanation is this handler — the pill is an UNSAY toggle, so re-clicking the
|
|
157
|
+
// already-chosen shape clears it. During the R24 walk that second click was mine. Pinning
|
|
158
|
+
// it here so the claim rests on the handler's actual behaviour rather than on prose.
|
|
159
|
+
// Anchored on addEventListener: the same selector also appears inside paintTeam(), which
|
|
160
|
+
// only writes aria-pressed. Matching that one instead yields a slice that will not parse.
|
|
161
|
+
const TEAM_RE = /root\.querySelectorAll\('#teamCards \.tpill'\)\.forEach\(function \(b\) \{\s*b\.addEventListener[\s\S]*?\n {4}\}\);/;
|
|
162
|
+
const teamSlice = (HUB.match(TEAM_RE) || [null])[0];
|
|
163
|
+
|
|
164
|
+
test('the team-pill slice is still findable', () => {
|
|
165
|
+
assert.ok(teamSlice, 'the #teamCards pill handler was not found in projects.html');
|
|
166
|
+
assert.match(teamSlice, /state\.teamShape/, 'the handler owns the team-shape answer');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the team pill UNSAYS on a second click — the answer is not dropped, it is toggled off', () => {
|
|
170
|
+
const state = { teamShape: null };
|
|
171
|
+
const handlers = [];
|
|
172
|
+
const pill = (team) => ({
|
|
173
|
+
getAttribute: (a) => (a === 'data-team' ? team : null),
|
|
174
|
+
addEventListener: (_e, fn) => handlers.push({ team, fn }),
|
|
175
|
+
});
|
|
176
|
+
const sandbox = {
|
|
177
|
+
state,
|
|
178
|
+
root: { querySelectorAll: () => [pill('solo'), pill('small-team'), pill('community')] },
|
|
179
|
+
paintTeam() {}, paintDetail() {}, saveDraft() {},
|
|
180
|
+
};
|
|
181
|
+
sandbox.globalThis = sandbox;
|
|
182
|
+
vm.createContext(sandbox);
|
|
183
|
+
vm.runInContext(teamSlice, sandbox);
|
|
184
|
+
|
|
185
|
+
const click = (team) => handlers.find((h) => h.team === team).fn();
|
|
186
|
+
click('small-team');
|
|
187
|
+
assert.equal(state.teamShape, 'small-team', 'first click answers');
|
|
188
|
+
click('small-team');
|
|
189
|
+
assert.equal(state.teamShape, null, 'the SAME pill again unsays it — this is the mis-read bug');
|
|
190
|
+
click('small-team');
|
|
191
|
+
assert.equal(state.teamShape, 'small-team', 'and a third click answers again');
|
|
192
|
+
click('community');
|
|
193
|
+
assert.equal(state.teamShape, 'community', 'a DIFFERENT pill replaces rather than clears');
|
|
194
|
+
});
|