@bongos/core 1.19.419 → 1.19.421
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 +108 -58
- package/.claude/skills/new-project/SKILL.md +22 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +79 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +103 -5
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +282 -269
- package/docs/copy-registry.json +487 -368
- package/docs/design/mocks/hub/mine.html +377 -0
- package/docs/design/projects-hub-direction-v2.md +25 -1
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/settings.html +3 -2
- package/modules/hall-ui/public/settings.js +10 -6
- package/modules/onboarding/routes/access-requests.js +16 -3
- package/modules/platform-identity/account-visibility.js +94 -3
- package/modules/platform-identity/platform-identity.js +50 -55
- package/modules/platform-identity/routes/connections.js +3 -3
- package/modules/platform-identity/tests/platform-identity.mjs +20 -0
- package/modules/provisioning/migrations/provisioning_013_project_detail.sql +54 -0
- package/modules/provisioning/onboard-plan.js +13 -0
- package/modules/provisioning/provisioning.js +113 -5
- package/modules/provisioning/routes/provisioning.js +105 -0
- package/modules/provisioning/tests/provisioning.mjs +84 -0
- package/modules/ui-design/kit/fixtures/featured-hub.json +1 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-empty.json +4 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +99 -0
- package/modules/ui-design/kit/lib.js +6 -2
- package/modules/ui-design/kit/probe.js +1 -1
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/kit/serve.js +63 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +224 -18
- package/public-landing/projects-empty.states.json +49 -0
- package/public-landing/projects-floor.states.json +63 -0
- package/public-landing/projects.html +273 -93
- package/public-landing/projects.probes.json +32 -7
- package/public-landing/projects.states.json +10 -6
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/onboard.js +41 -1
- package/scripts/gds/provision-net.js +7 -3
- package/src/bongos/middleware/rate-limit.js +68 -2
- package/src/bongos/routes/auth.js +19 -12
- package/src/module-api.js +13 -1
- package/tests/account_existence_rate_limit.mjs +375 -0
- package/tests/account_privacy_flags.mjs +17 -10
- package/tests/effective_visibility_predicate.mjs +891 -0
- package/tests/module_api.mjs +1 -0
- package/tests/onboard.mjs +56 -0
- package/tests/profile_route.mjs +22 -11
- package/tests/projects_hub.mjs +102 -5
- package/tests/projects_hub_pre_uat.mjs +45 -11
- package/tests/provision.mjs +20 -0
- package/tests/ui_design_kit.mjs +32 -2
package/tests/module_api.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const PUBLISHED_SURFACE = [
|
|
|
24
24
|
'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
|
|
25
25
|
'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
|
|
26
26
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
27
|
+
'accountExistenceReadRateLimit', // task 1003339 (ADR 0209): the shared per-IP budget both account-existence probes spend from — one instance, not a factory, so the budget is genuinely shared
|
|
27
28
|
'routeRankCheck', 'permissionPathCheck', // BV1.R73: kernel trust classifiers (1.6.0)
|
|
28
29
|
'repoInfo', 'moduleScopeMap', 'pathMatch', // BV1.R86: kernel repo-identity + scope-wall + touches-overlap for the carved lifecycle (1.12.0)
|
|
29
30
|
'goalScopeCheck', // BV1.R60: kernel scope wall for the goal-authored-task create gate (1.13.0; ADR 0086 §3)
|
package/tests/onboard.mjs
CHANGED
|
@@ -31,6 +31,62 @@ test('onboardPlan is the canonical sequence: name+address first, the builders ha
|
|
|
31
31
|
assert.ok(keys.indexOf('device-flow') < keys.indexOf('first-sign-in'), 'checkbox prompted before sign-in');
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
test('the OPTIONAL project-detail step sits in the canonical sequence, and optional means skippable (task 1002334)', () => {
|
|
35
|
+
const plan = ob.onboardPlan();
|
|
36
|
+
const keys = plan.map((s) => s.key);
|
|
37
|
+
const step = plan.find((s) => s.key === 'project-detail');
|
|
38
|
+
assert.ok(step, 'the interview is a real step in the sequence, not a wizard-only flourish');
|
|
39
|
+
assert.equal(keys.indexOf('project-detail'), keys.indexOf('name-address') + 1,
|
|
40
|
+
'it follows the name interview — the project is described before its repo and address are decided');
|
|
41
|
+
assert.equal(step.optional, true, 'the flag is what tells a form it may be walked past');
|
|
42
|
+
assert.equal(step.kind, 'auto',
|
|
43
|
+
"'auto' like name-address: the FORM collects it, so a done panel of outstanding human steps must not list it");
|
|
44
|
+
// one optional step keeps the flag meaningful; a second would need its own rule
|
|
45
|
+
assert.deepEqual(plan.filter((s) => s.optional).map((s) => s.key), ['project-detail']);
|
|
46
|
+
// and the adopt branch carries it identically — the sequence branches only at scaffold
|
|
47
|
+
assert.ok(ob.onboardPlan('adopt').some((s) => s.key === 'project-detail' && s.optional === true));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('every form OFFERS the optional step and names what skipping costs — skill / CLI / wizard / route (task 1002334)', () => {
|
|
51
|
+
const { readFileSync } = require('node:fs');
|
|
52
|
+
const path = require('node:path');
|
|
53
|
+
const read = (rel) => readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
|
54
|
+
|
|
55
|
+
// the skill — the source of truth for the sequence
|
|
56
|
+
const skill = read('.claude/skills/new-project/SKILL.md');
|
|
57
|
+
assert.match(skill, /OPTIONAL/, 'the canonical runbook marks the step optional');
|
|
58
|
+
assert.match(skill, /team_shape/, 'and names the answers that ride the request');
|
|
59
|
+
assert.match(skill, /dark matter/i, 'and states what skipping costs');
|
|
60
|
+
|
|
61
|
+
// the CLI form
|
|
62
|
+
const cli = read('scripts/gds/onboard.js');
|
|
63
|
+
assert.match(cli, /Press Enter to skip/, 'the terminal form offers the skip in the prompt itself');
|
|
64
|
+
assert.match(cli, /off the public map/, 'and says what a missing description means');
|
|
65
|
+
|
|
66
|
+
// the web form
|
|
67
|
+
const hub = read('public-landing/projects.html');
|
|
68
|
+
assert.match(hub, /id="panel-detail"/, 'the wizard ships the panel');
|
|
69
|
+
assert.match(hub, /id="skipDetail"/, 'with a skip control of its own');
|
|
70
|
+
|
|
71
|
+
// the route that receives it
|
|
72
|
+
const route = read('modules/provisioning/routes/provisioning.js');
|
|
73
|
+
assert.match(route, /description: \{ type: 'string', maxLength: provisioning\.DESCRIPTION_MAX \}/,
|
|
74
|
+
'the create route accepts the description');
|
|
75
|
+
assert.match(route, /\/provisioning\/instances\/:id\/detail/,
|
|
76
|
+
'and there is a way back, or "optional" would mean permanently unpublishable');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the answers the CLI collects are the same body the web form files (buildProjectDetail)', () => {
|
|
80
|
+
assert.deepEqual(ob.buildProjectDetail({ description: ' a b ', teamShape: 'Community' }),
|
|
81
|
+
{ description: 'a b', detail: { team_shape: 'community' } });
|
|
82
|
+
// a skip files nothing — absent means skipped, and an empty string would read
|
|
83
|
+
// to the publish gate as an answered-but-blank description
|
|
84
|
+
assert.deepEqual(ob.buildProjectDetail({}), {});
|
|
85
|
+
assert.deepEqual(ob.buildProjectDetail({ description: ' ', teamShape: '' }), {});
|
|
86
|
+
// an off-vocabulary answer is dropped, never guessed into a recommendation
|
|
87
|
+
assert.deepEqual(ob.buildProjectDetail({ teamShape: 'a big guild' }), {});
|
|
88
|
+
});
|
|
89
|
+
|
|
34
90
|
test('onboardPlan(adopt): same sequence + human steps, but the scaffold-leg labels branch (task 2046)', () => {
|
|
35
91
|
const green = ob.onboardPlan();
|
|
36
92
|
const adopt = ob.onboardPlan('adopt');
|
package/tests/profile_route.mjs
CHANGED
|
@@ -341,6 +341,7 @@ await t('hub port returns null (hidden/provisional/none) → cross_project: null
|
|
|
341
341
|
const HUB_GITHUB_ID = FULL_ROW.github_id;
|
|
342
342
|
const HIDDEN_LOGIN = 'hidden-DO-NOT-LEAK-r06';
|
|
343
343
|
const PROV_LOGIN = 'provisional-DO-NOT-LEAK-r06';
|
|
344
|
+
const PRIVATE_LOGIN = 'private-DO-NOT-LEAK-r06';
|
|
344
345
|
|
|
345
346
|
// TWO projects — one publicly catalogued, one not. Distinctive digits so the
|
|
346
347
|
// "no partial numbers" scan below cannot pass by coincidence.
|
|
@@ -353,22 +354,28 @@ const PROOF_PROJECTS = [
|
|
|
353
354
|
const sumOf = (k) => PROOF_PROJECTS.reduce((n, p) => n + p[k], 0);
|
|
354
355
|
|
|
355
356
|
// The one account row the reads consult. setAccountHideStats mutates it.
|
|
356
|
-
const hubAccount = { profile_state: 'active', hide_stats: false, handle: 'r06-handle' };
|
|
357
|
+
const hubAccount = { profile_state: 'active', account_visibility: 'public', hide_stats: false, handle: 'r06-handle' };
|
|
357
358
|
|
|
358
|
-
// The scouting "table": one visible builder plus the
|
|
359
|
+
// The scouting "table": one visible builder plus the THREE that consent and
|
|
360
|
+
// visibility exclude (the private row joined at task 1002967 — the same axis,
|
|
361
|
+
// enforced by the same shared predicate).
|
|
359
362
|
const SCOUTING_TABLE = [
|
|
360
|
-
{ profile_state: 'active', hide_stats: false,
|
|
363
|
+
{ profile_state: 'active', account_visibility: 'public', hide_stats: false,
|
|
361
364
|
row: { github_id: '11', github_login: 'r06-visible', display_name: 'Visible', avatar_url: null,
|
|
362
365
|
project_count: 2, total_credits: 5, total_tasks_shipped: 1, total_karma: 0,
|
|
363
366
|
last_active: '2026-08-01T00:00:00.000Z', disciplines: ['engineer'] } },
|
|
364
|
-
{ profile_state: 'active', hide_stats: true,
|
|
367
|
+
{ profile_state: 'active', account_visibility: 'public', hide_stats: true,
|
|
365
368
|
row: { github_id: '12', github_login: HIDDEN_LOGIN, display_name: 'Hidden', avatar_url: null,
|
|
366
369
|
project_count: 7, total_credits: 88817, total_tasks_shipped: 6613, total_karma: 5519,
|
|
367
370
|
last_active: '2026-08-02T00:00:00.000Z', disciplines: ['ui'] } },
|
|
368
|
-
{ profile_state: 'provisional', hide_stats: false,
|
|
371
|
+
{ profile_state: 'provisional', account_visibility: 'public', hide_stats: false,
|
|
369
372
|
row: { github_id: '13', github_login: PROV_LOGIN, display_name: 'Provisional', avatar_url: null,
|
|
370
373
|
project_count: 1, total_credits: 71713, total_tasks_shipped: 1, total_karma: 0,
|
|
371
374
|
last_active: '2026-08-03T00:00:00.000Z', disciplines: [] } },
|
|
375
|
+
{ profile_state: 'active', account_visibility: 'private', hide_stats: false,
|
|
376
|
+
row: { github_id: '14', github_login: PRIVATE_LOGIN, display_name: 'Private', avatar_url: null,
|
|
377
|
+
project_count: 3, total_credits: 63127, total_tasks_shipped: 4409, total_karma: 2287,
|
|
378
|
+
last_active: '2026-08-04T00:00:00.000Z', disciplines: ['engineer'] } },
|
|
372
379
|
];
|
|
373
380
|
|
|
374
381
|
const engine = {
|
|
@@ -382,6 +389,7 @@ const engine = {
|
|
|
382
389
|
if (/AS project_count/.test(sql)) {
|
|
383
390
|
let rows = SCOUTING_TABLE;
|
|
384
391
|
if (/pa\.profile_state = 'active'/.test(sql)) rows = rows.filter((r) => r.profile_state === 'active');
|
|
392
|
+
if (/pa\.account_visibility = 'public'/.test(sql)) rows = rows.filter((r) => r.account_visibility === 'public');
|
|
385
393
|
if (/NOT pa\.hide_stats/.test(sql)) rows = rows.filter((r) => !r.hide_stats);
|
|
386
394
|
return { rows: rows.map((r) => ({ ...r.row })) };
|
|
387
395
|
}
|
|
@@ -390,9 +398,11 @@ const engine = {
|
|
|
390
398
|
hubAccount.hide_stats = !!params[4];
|
|
391
399
|
return { rows: [{ github_id: String(HUB_GITHUB_ID), github_login: 'jaxri', ...hubAccount }] };
|
|
392
400
|
}
|
|
393
|
-
// getPublicProfileExtras' consent read
|
|
394
|
-
|
|
395
|
-
|
|
401
|
+
// getPublicProfileExtras' consent read — three columns since task 1002967
|
|
402
|
+
// wired the existence axis in (the shared rollupIsVisible port reads them).
|
|
403
|
+
if (/SELECT profile_state, account_visibility, hide_stats/.test(sql)) {
|
|
404
|
+
return { rows: [{ profile_state: hubAccount.profile_state,
|
|
405
|
+
account_visibility: hubAccount.account_visibility, hide_stats: hubAccount.hide_stats }] };
|
|
396
406
|
}
|
|
397
407
|
// getRollupForBuilder's per-project rows (the client_id → origin two-hop).
|
|
398
408
|
if (/platform_identity_sso_clients/.test(sql)) {
|
|
@@ -537,14 +547,15 @@ scoutApp.use(attachFail);
|
|
|
537
547
|
scoutApp.use('/api/gds', buildScoutingRoutes());
|
|
538
548
|
const scout = await listen(scoutApp);
|
|
539
549
|
|
|
540
|
-
await t('R06.3 GET /scouting drops the hidden AND the
|
|
550
|
+
await t('R06.3 GET /scouting drops the hidden, the provisional AND the private builder', async () => {
|
|
541
551
|
engine.calls.length = 0;
|
|
542
552
|
const r = await get(scout.port, '/api/gds/scouting');
|
|
543
553
|
assert.equal(r.status, 200);
|
|
544
554
|
const logins = r.json.builders.map((b) => b.github_login);
|
|
545
|
-
assert.deepEqual(logins, ['r06-visible'],
|
|
555
|
+
assert.deepEqual(logins, ['r06-visible'],
|
|
556
|
+
'only the consented, publicly-visible, non-hidden builder is listed (task 1002967 added the visibility axis)');
|
|
546
557
|
assert.equal(engine.calls.length, 1, 'exactly ONE read — nothing can re-enter via a second, unfiltered query');
|
|
547
|
-
for (const s of [HIDDEN_LOGIN, PROV_LOGIN, '88817', '6613', '5519', '71713']) {
|
|
558
|
+
for (const s of [HIDDEN_LOGIN, PROV_LOGIN, PRIVATE_LOGIN, '88817', '6613', '5519', '71713', '63127', '4409', '2287']) {
|
|
548
559
|
assert.ok(!r.text.includes(s), `the excluded builder leaks nothing — found "${s}"`);
|
|
549
560
|
}
|
|
550
561
|
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -172,12 +172,40 @@ test('orb accents are tokens, never literal hexes (§6)', () => {
|
|
|
172
172
|
for (const t of ['--sea', '--terracotta', '--accent', '--sun-lit', '--gold-soft']) {
|
|
173
173
|
assert.match(accents, new RegExp(`var\\(${t}\\)`), `${t} must be one of the orb accents`);
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
assert.doesNotMatch(
|
|
175
|
+
// the sphere's glow left with the sphere (task 1003345): a planet carries its accent only
|
|
176
|
+
assert.doesNotMatch(SCRIPT, /ORB_GLOWS|--glow:/, 'no glow — the row draws the sky\'s planet');
|
|
177
177
|
assert.equal((SCRIPT.match(/var ORB_ACCENTS/g) || []).length, 1,
|
|
178
178
|
'one palette, not two — the map and the manage cards must not drift apart');
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
/* ── the row's planet is the sky's object (task 1003345, direction record §14: v1 the ledger row) ── */
|
|
182
|
+
test('the mine row draws the project\'s planet — the catalog\'s template when matched, the slug walk otherwise', () => {
|
|
183
|
+
const orb = (SCRIPT.match(/function orbFor\(inst\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
184
|
+
assert.match(orb, /var pl = planetOf\(inst\);/, 'orbFor asks planetOf');
|
|
185
|
+
assert.match(orb, /class="' \+ cls \+ '"' \+ attrs/, 'the planet is a .worldOrb with the template attributes');
|
|
186
|
+
assert.match(orb, /<span class="disc"><\/span>/, 'the disc cosmos.css draws');
|
|
187
|
+
assert.doesNotMatch(orb, /class="orb/, 'the sphere is gone');
|
|
188
|
+
const planet = (SCRIPT.match(/function planetOf\(inst\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
189
|
+
assert.match(planet, /hostOf\(catalog\[i\]\.origin\) === h/, 'matched by host, the same join the map makes');
|
|
190
|
+
assert.match(planet, /tpl: templateOf\(catalog\[i\]\), acc: accentOf\(catalog\[i\]\)/, 'a catalog match takes the catalog\'s template + accent');
|
|
191
|
+
assert.match(planet, /tpl: slugTemplate\(inst\.slug\), acc: ORB_ACCENTS\[slugAccent\(inst\.slug\)\]/, 'owned-only walks by slug');
|
|
192
|
+
assert.match(SCRIPT, /assignAccents\(\); applyMapFilter\(\); repaintPlanets\(\);/, 'a catalog that lands late repaints the rows');
|
|
193
|
+
assert.doesNotMatch(COSMOS, /\n\.orb\{/, 'cosmos no longer carries the sphere');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('the mine list lays every row on one set of FIXED tracks (task 1003348) and the manage h1 stands outside #mMain', () => {
|
|
197
|
+
assert.match(COSMOS, /#view-mine \.projBody\{display:grid;grid-template-columns:minmax\(0,1\.1fr\) minmax\(0,1\.3fr\) minmax\(0,1fr\) 232px;/, 'no content minimum, so the tracks are the same on every row');
|
|
198
|
+
assert.match(COSMOS, /#view-mine \.proj\{display:grid;grid-template-columns:72px minmax\(0,1fr\);/, 'the planet column is a width');
|
|
199
|
+
assert.match(COSMOS, /#view-mine \.card,#view-manage \.card\{background:none;/, 'no card ground on either view');
|
|
200
|
+
assert.match(COSMOS, /#view-mine \.strip,#view-manage \.strip\{border:0;border-radius:0;padding:0;background:none;/, 'the strip is drawn as the rail');
|
|
201
|
+
assert.doesNotMatch(COSMOS.slice(COSMOS.indexOf('YOUR PROJECTS + THE MANAGE PAGE')), /box-shadow:(?!none)/, 'halos are outlines in the world, never box-shadow');
|
|
202
|
+
const title = HUB.indexOf('<h1 id="mTitle">');
|
|
203
|
+
const main = HUB.indexOf('<section id="mMain"');
|
|
204
|
+
assert.ok(title > -1 && main > -1 && title < main, 'the h1 renders in every manage state — signed out, not found, loading');
|
|
205
|
+
assert.match(HUB, /<h1 id="mTitle"><span id="mName">Manage<\/span><\/h1>/, '#mName is the heading\'s text, so renderManage names the page by filling it');
|
|
206
|
+
for (const id of ['mOrb', 'mFacts', 'mPlanet']) assert.match(HUB, new RegExp(`id="${id}"`), `#${id} is real markup`);
|
|
207
|
+
});
|
|
208
|
+
|
|
181
209
|
test('accents are assigned by canonical position, not by hashing the name', () => {
|
|
182
210
|
// a name hash collided: with five tokens and four projects it put three of
|
|
183
211
|
// them on the same ember and the sky stopped being readable
|
|
@@ -613,6 +641,75 @@ test('every table column has a source — Type is declared, never guessed', () =
|
|
|
613
641
|
assert.match(SCRIPT, /type: state\.projType/, 'the create request carries the declared type');
|
|
614
642
|
});
|
|
615
643
|
|
|
644
|
+
test('every per-step hook in show() still points at the panel it belongs to (renumbering guard)', () => {
|
|
645
|
+
// The renumbering risk is NOT the data-next attributes (guarded above) — it is
|
|
646
|
+
// show()'s side-effect hooks, which key off bare integers. Point one at the
|
|
647
|
+
// wrong panel and the wizard still navigates perfectly while silently never
|
|
648
|
+
// probing repo access, never re-checking the handle, or rendering a stale
|
|
649
|
+
// review. Derived from the panels map so this survives the next renumbering.
|
|
650
|
+
const map = SCRIPT.match(/var panels = \{([^}]*)\}/)[1];
|
|
651
|
+
const STEP = {};
|
|
652
|
+
for (const m of map.matchAll(/(\d+):\s*'([a-z-]+)'/g)) STEP[m[2]] = Number(m[1]);
|
|
653
|
+
const show = SCRIPT.slice(SCRIPT.indexOf('function show(step, moveFocus)'));
|
|
654
|
+
const body = show.slice(0, show.search(/\n\s*\/\* One delegated pair on the rail/));
|
|
655
|
+
|
|
656
|
+
assert.ok(body.includes(`if (step === ${STEP['panel-home']}) { syncHome();`),
|
|
657
|
+
'the repo probe fires on the Home panel');
|
|
658
|
+
assert.ok(body.includes(`if (step === ${STEP['panel-address']}) { checkSlugAvailable(); renderAddrPanel(); }`),
|
|
659
|
+
'the handle re-check fires on the Address panel');
|
|
660
|
+
assert.ok(body.includes(`if (step === ${STEP['panel-review']}) renderReview();`),
|
|
661
|
+
'the review is rendered on the Review panel');
|
|
662
|
+
assert.ok(body.includes(`'wizDone', step === ${STEP['panel-done']}`),
|
|
663
|
+
'the done panel is the one that takes the whole card');
|
|
664
|
+
// and the About panel claims no hook of its own — it has nothing to fetch
|
|
665
|
+
assert.ok(!body.includes(`if (step === ${STEP['panel-detail']})`),
|
|
666
|
+
'the optional step needs no side effect; adding one would give it a way to fail');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test('the About step is OFFERED by every form and can be walked past — the skip is a real control (task 1002334)', () => {
|
|
670
|
+
const about = (HUB.match(/<section class="panel" id="panel-detail"[\s\S]*?<\/section>/) || [''])[0];
|
|
671
|
+
assert.ok(about, '#panel-detail ships as real markup');
|
|
672
|
+
assert.match(about, /id="f-desc"/, 'it asks what the owner is building — the publish gate half');
|
|
673
|
+
assert.match(about, /id="teamCards"/, 'and who is building it — what the recommendations read');
|
|
674
|
+
// the skip must be its OWN button: "leave it blank and press Next" is an
|
|
675
|
+
// escape hatch the visitor has to infer, which is not an optional step
|
|
676
|
+
assert.match(about, /id="skipDetail"/, 'the skip is a control, not an inference');
|
|
677
|
+
assert.match(about, /Optional/, 'and the panel says so before it asks');
|
|
678
|
+
// the consequence of skipping is stated where the choice is made
|
|
679
|
+
assert.match(SCRIPT, /stays off the public map until it has one/,
|
|
680
|
+
'skipping names its cost (ADR 0182 D4), never leaves it to be discovered later');
|
|
681
|
+
// nothing in validateStep may gate the About step
|
|
682
|
+
const validate = SCRIPT.slice(SCRIPT.indexOf('function validateStep('));
|
|
683
|
+
const map = SCRIPT.match(/var panels = \{([^}]*)\}/)[1];
|
|
684
|
+
const aboutStep = Number((map.match(/(\d+):\s*'panel-detail'/) || [, 0])[1]);
|
|
685
|
+
assert.ok(aboutStep > 0, 'the About panel is in the map');
|
|
686
|
+
assert.ok(!validate.slice(0, validate.indexOf('function ', 1)).includes(`if (step === ${aboutStep}) {`),
|
|
687
|
+
'the optional step has no validation clause — Next past an empty answer is the point');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('the About answers travel on the create request, and an empty one is OMITTED not blanked', () => {
|
|
691
|
+
assert.match(SCRIPT, /description: String\(state\.description \|\| ''\)\.trim\(\) \|\| undefined/,
|
|
692
|
+
'a skipped description is absent from the body — an empty string would read as answered');
|
|
693
|
+
assert.match(SCRIPT, /detail: state\.teamShape \? \{ team_shape: state\.teamShape \} : undefined/,
|
|
694
|
+
'and an unanswered team shape is never guessed into the request');
|
|
695
|
+
// the review is the last screen before Create, so the skip's cost is restated there
|
|
696
|
+
const review = (SCRIPT.match(/function renderReview\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
697
|
+
assert.match(review, /off the public map until you add it/,
|
|
698
|
+
'the review row states the consequence rather than a bare dash');
|
|
699
|
+
assert.match(review, /\['About',/, 'and the About answer has a row of its own');
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('a renumbering of the wizard invalidates old drafts — the key carries the version', () => {
|
|
703
|
+
// task 1002772 renumbered once and bumped to -v2; task 1002334 renumbered again.
|
|
704
|
+
// A draft replayed under a different numbering lands on the wrong panel with the
|
|
705
|
+
// rail already marking the skipped steps done.
|
|
706
|
+
const key = (SCRIPT.match(/var DRAFT_KEY = '([^']+)'/) || [, ''])[1];
|
|
707
|
+
assert.match(key, /-v\d+$/, 'the draft key is versioned');
|
|
708
|
+
const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
|
|
709
|
+
const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
|
|
710
|
+
assert.equal(count, 8, 'gate + six numbered steps + done — bump DRAFT_KEY when this changes');
|
|
711
|
+
});
|
|
712
|
+
|
|
616
713
|
test('header and body agree on the column count', () => {
|
|
617
714
|
// a stray <td> shifts every cell after it — silent, and only visible on screen
|
|
618
715
|
const n = ((HUB.match(/<thead>([\s\S]*?)<\/thead>/) || [, ''])[1].match(/<th/g) || []).length;
|
|
@@ -1173,7 +1270,7 @@ test('the delete section is live markup with its body slot (C7)', () => {
|
|
|
1173
1270
|
assert.match(HUB, /id="mDel"/, 'the volatile slot is real markup, not JS-built');
|
|
1174
1271
|
assert.match(HUB, /id="mDelWhat"/, 'the teardown-vs-delete fold ships with the page');
|
|
1175
1272
|
// the fold is a SIBLING of the volatile slot, so no re-render can drop it
|
|
1176
|
-
const card = (HUB.match(/<div class="card" id="mDelete">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1273
|
+
const card = (HUB.match(/<div class="card mSec" id="mDelete">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1177
1274
|
assert.ok(card, 'the delete card must be extractable');
|
|
1178
1275
|
const slotAt = card.indexOf('id="mDel"');
|
|
1179
1276
|
const foldAt = card.indexOf('id="mDelWhat"');
|
|
@@ -1253,7 +1350,7 @@ const fnHub = (name) => {
|
|
|
1253
1350
|
test('the visibility section is live markup with its body slot and a sibling fold (C7)', () => {
|
|
1254
1351
|
assert.match(HUB, /id="mVis"/, 'the volatile slot is real markup, not JS-built');
|
|
1255
1352
|
assert.match(HUB, /id="mVisWhat"/, 'the what-changes fold ships with the page');
|
|
1256
|
-
const card = (HUB.match(/<div class="card" id="mVisibility">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1353
|
+
const card = (HUB.match(/<div class="card mSec" id="mVisibility">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1257
1354
|
assert.ok(card, 'the visibility card must be extractable');
|
|
1258
1355
|
const slotAt = card.indexOf('id="mVis"');
|
|
1259
1356
|
const foldAt = card.indexOf('id="mVisWhat"');
|
|
@@ -1335,7 +1432,7 @@ test('the settings reads fire on entry + tab return only — never on the in-fli
|
|
|
1335
1432
|
test('the joinability section is live markup with its body slot and a sibling fold', () => {
|
|
1336
1433
|
assert.match(HUB, /id="mJoin"/, 'the volatile slot is real markup, not JS-built');
|
|
1337
1434
|
assert.match(HUB, /id="mJoinWhat"/, 'the what-changes fold ships with the page');
|
|
1338
|
-
const card = (HUB.match(/<div class="card" id="mJoinability">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1435
|
+
const card = (HUB.match(/<div class="card mSec" id="mJoinability">[\s\S]*?\n <\/div>/) || [''])[0];
|
|
1339
1436
|
assert.ok(card, 'the joinability card must be extractable');
|
|
1340
1437
|
const slotAt = card.indexOf('id="mJoin"');
|
|
1341
1438
|
const foldAt = card.indexOf('id="mJoinWhat"');
|
|
@@ -28,6 +28,20 @@ const fn = (name) => {
|
|
|
28
28
|
return next < 0 ? rest : rest.slice(0, next);
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// The step indices are DERIVED from the panels map, not typed (task 1002334
|
|
32
|
+
// renumbered them a second time and every literal below broke). What these
|
|
33
|
+
// blocks actually claim is "the DONE panel" and "the step before it" — say
|
|
34
|
+
// that, and the next renumbering costs nothing.
|
|
35
|
+
const PANELS = (() => {
|
|
36
|
+
const m = SCRIPT.match(/var panels = \{([^}]*)\}/);
|
|
37
|
+
assert.ok(m, 'the panels map must exist');
|
|
38
|
+
const map = {};
|
|
39
|
+
for (const p of m[1].matchAll(/(\d+):\s*'([a-z-]+)'/g)) map[Number(p[1])] = p[2];
|
|
40
|
+
return map;
|
|
41
|
+
})();
|
|
42
|
+
const STEP = Object.fromEntries(Object.entries(PANELS).map(([n, id]) => [id, Number(n)]));
|
|
43
|
+
const DONE = STEP['panel-done'];
|
|
44
|
+
|
|
31
45
|
/* ── the gate says why GitHub will ask for repository access ── */
|
|
32
46
|
test('the gate explains the repo scope BEFORE GitHub does — on the path every founder takes', () => {
|
|
33
47
|
const gate = (HUB.match(/<section class="panel" id="panel-gate"[\s\S]*?<\/section>/) || [''])[0];
|
|
@@ -203,7 +217,9 @@ test('the wizard’s 401 recoveries keep the repo scope they already granted', (
|
|
|
203
217
|
});
|
|
204
218
|
|
|
205
219
|
test('a resumed draft past Home still probes repo access before Create', () => {
|
|
206
|
-
assert.
|
|
220
|
+
assert.ok(SCRIPT.includes(
|
|
221
|
+
`else if (step > ${STEP['panel-home']} && step < ${DONE} && authKnown && repoAccess === null) ensureRepoAccess();`),
|
|
222
|
+
'every step between Home and the done panel re-probes');
|
|
207
223
|
});
|
|
208
224
|
|
|
209
225
|
test('a half-typed custom domain says why Next is dead; a taken handle silences the format cheer', () => {
|
|
@@ -288,9 +304,19 @@ test('the bar is the page ground closed by a hairline, holds one row on a phone,
|
|
|
288
304
|
assert.match(HUB, /#view-mine \.hero,#view-manage \.hero\{max-width:none;\}/, 'titles align with their cards');
|
|
289
305
|
});
|
|
290
306
|
|
|
291
|
-
test('card metadata is span-wrapped so a separator can never strand', () => {
|
|
307
|
+
test('card metadata is span-wrapped so a separator can never strand; the manage page lays its facts as a ledger', () => {
|
|
292
308
|
assert.match(fn('renderCard'), /meta\.map\(function \(m\) \{ return '<span>' \+ m \+ '<\/span>'; \}\)\.join\(''\)/);
|
|
293
|
-
|
|
309
|
+
/* task 1003345: the manage page's facts are label · value rows on hairlines — one
|
|
310
|
+
pair per fact, each fact keeping its id so a missing answer still hides */
|
|
311
|
+
assert.match(SCRIPT, /function ledgerRow\(r\) \{ return '<span class="k">' \+ r\[0\] \+ '<\/span><span class="v">' \+ r\[1\] \+ '<\/span>'; \}/);
|
|
312
|
+
const rm = fn('renderManage');
|
|
313
|
+
assert.match(rm, /\$\('mMeta'\)\.innerHTML = rows\.map\(ledgerRow\)\.join\(''\);/);
|
|
314
|
+
assert.match(rm, /\$\('mAddr'\)\.innerHTML = addr \? ledgerRow\(\['address', addr\]\) : '';/);
|
|
315
|
+
assert.match(rm, /paintManagePlanet\(inst\);/, 'the head and the planet row paint through one fn');
|
|
316
|
+
const pm = fn('paintManagePlanet');
|
|
317
|
+
assert.match(pm, /\$\('mPlanet'\)\.innerHTML = ledgerRow\(\['planet', planetWords\(pl\)/, 'the planet row is the slot the picker (task 1003347) fills');
|
|
318
|
+
assert.match(pm, /\$\('mOrb'\)\.innerHTML = orbFor\(inst\);/, 'the same planet the row shows, at the head');
|
|
319
|
+
assert.match(fn('repaintPlanets'), /if \(manageInst\) paintManagePlanet\(manageInst\);/, 'a catalog that lands after the manage page rendered repaints its head too');
|
|
294
320
|
});
|
|
295
321
|
|
|
296
322
|
test('an open teardown confirm survives the minute re-render', () => {
|
|
@@ -377,7 +403,7 @@ test('a heartbeat re-render keeps an open fold open and a half-typed domain type
|
|
|
377
403
|
|
|
378
404
|
/* ── the owner's five calls of 2026-08-24 (task 1003250) ── */
|
|
379
405
|
test('decision 6 — the manage section is "Take it offline", and the teardown truth ends by saying nothing erases', () => {
|
|
380
|
-
const del = (HUB.match(/<div class="card" id="mDelete">[\s\S]*?<\/details>\s*<\/div>/) || [''])[0];
|
|
406
|
+
const del = (HUB.match(/<div class="card mSec" id="mDelete">[\s\S]*?<\/details>\s*<\/div>/) || [''])[0];
|
|
381
407
|
assert.ok(del, '#mDelete exists');
|
|
382
408
|
assert.match(del, /<h2[^>]*>Take it offline<\/h2>/, 'the heading says what the control does');
|
|
383
409
|
assert.doesNotMatch(HUB, /Delete the project/, 'the word "Delete" no longer promises an erase anywhere on the page');
|
|
@@ -447,13 +473,16 @@ test('a manifest error says a plain sentence, folds the route text, and sends a
|
|
|
447
473
|
|
|
448
474
|
test('the rail at the done panel keeps its marker on Create until the project answers', () => {
|
|
449
475
|
const rail = fn('paintRail');
|
|
450
|
-
assert.
|
|
451
|
-
|
|
476
|
+
assert.ok(rail.includes(`var landed = step === ${DONE} && !!(live && live.instStatus === 'active');`),
|
|
477
|
+
'the rail only finishes on the done panel');
|
|
478
|
+
assert.ok(rail.includes(`var cur = step === ${DONE} ? ${DONE - 1} : step;`),
|
|
479
|
+
'the done panel keeps the marker on the last numbered beat');
|
|
452
480
|
assert.match(rail, /li\.classList\.toggle\('done', landed \|\| n < cur\);/);
|
|
453
481
|
assert.match(rail, /if \(now\) li\.setAttribute\('aria-current', 'step'\); else li\.removeAttribute\('aria-current'\);/);
|
|
454
482
|
assert.match(fn('show'), /paintRail\(step\);/, 'show() paints through it');
|
|
455
483
|
assert.doesNotMatch(fn('show'), /classList\.toggle\('done'/, 'and no longer marks the rail on its own');
|
|
456
|
-
assert.
|
|
484
|
+
assert.ok(SCRIPT.includes(`if (state.step === ${DONE}) paintRail(${DONE});`),
|
|
485
|
+
'the done-panel poll repaints when the status moves');
|
|
457
486
|
assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
|
|
458
487
|
});
|
|
459
488
|
|
|
@@ -560,7 +589,10 @@ test('the hub has one main landmark, a heading for the map, a footer, and a skip
|
|
|
560
589
|
});
|
|
561
590
|
|
|
562
591
|
test('completed rail steps are a way back, the repo picker is honest ARIA, the card inerts the page', () => {
|
|
563
|
-
assert.
|
|
592
|
+
assert.ok(fn('paintRail').includes(`if (step < ${DONE} && n < cur) {`),
|
|
593
|
+
'only answered steps become buttons, and none once the project is filed');
|
|
594
|
+
assert.ok(fn('paintRail').includes("li.setAttribute('data-goto', String(n));"),
|
|
595
|
+
'and an answered step carries the goto the delegated handler reads');
|
|
564
596
|
assert.match(SCRIPT, /railEl\.addEventListener\('click'/);
|
|
565
597
|
assert.match(SCRIPT, /railEl\.addEventListener\('keydown'/);
|
|
566
598
|
assert.match(SCRIPT, /show\(Number\(li\.getAttribute\('data-goto'\)\), true\)/, 'a deliberate step: focus moves');
|
|
@@ -582,9 +614,11 @@ test('completed rail steps are a way back, the repo picker is honest ARIA, the c
|
|
|
582
614
|
});
|
|
583
615
|
|
|
584
616
|
test('control nits: orb cursor, caret under reduced motion, no mode switch, home label, held reduce-motion query, target sizes', () => {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
assert.
|
|
617
|
+
/* task 1003345: the row's object is the planet (.worldOrb, docked); only the live
|
|
618
|
+
project's, a link to its site, is a pointer */
|
|
619
|
+
assert.match(COSMOS, /#view-mine \.worldOrb,#view-manage \.worldOrb\{[^}]*cursor:default/, 'the docked planet is not a pointer');
|
|
620
|
+
assert.match(COSMOS, /\n#view-mine a\.worldOrb,#view-manage a\.worldOrb\{cursor:pointer;\}/, 'only the linked one is');
|
|
621
|
+
assert.doesNotMatch(COSMOS, /\n\.orb\{/, 'the sphere is gone');
|
|
588
622
|
assert.doesNotMatch(HUB, /\.shell \.caret/, 'no caret to hide — the hub mark is static (task 1003300)');
|
|
589
623
|
assert.doesNotMatch(SCRIPT, /modeToggle|paintModeToggle|resolvedMode|cb-color-mode/, 'the mode switch left the hub — the apex is dark only (task 1003336)');
|
|
590
624
|
assert.doesNotMatch(HUB, /id="modeToggle"|class="modeBtn"/);
|
package/tests/provision.mjs
CHANGED
|
@@ -1673,6 +1673,26 @@ await ta('federateInstance(--apply): registers the client on the hub bound to th
|
|
|
1673
1673
|
});
|
|
1674
1674
|
});
|
|
1675
1675
|
|
|
1676
|
+
await ta('federateInstance: the project-detail answers ride to the catalog row — and a SKIP stays skipped (task 1002334)', async () => {
|
|
1677
|
+
await withEnv(HUB_ENV, async () => {
|
|
1678
|
+
// The instance row holds what the owner said at creation; the catalog row is
|
|
1679
|
+
// where the publish gate reads it (ADR 0182 D4). Dropping the answer on this
|
|
1680
|
+
// one hop would leave a project that DID describe itself in dark matter,
|
|
1681
|
+
// which would make the whole optional step decorative.
|
|
1682
|
+
const told = { ...coTenantWithDomain, type: 'game', description: 'A co-op farming game.' };
|
|
1683
|
+
const pi = fakePI();
|
|
1684
|
+
const deps = { apply: true, log: () => {}, platformIdentity: pi, hubKeys: hubKeysOn, db: { query: async () => ({ rows: [{ github_id: 'gh-owner-1' }] }) } };
|
|
1685
|
+
await P.federateInstance(told, deps);
|
|
1686
|
+
assert.equal(pi.calls[0].description, 'A co-op farming game.', 'the description reaches the hub registration');
|
|
1687
|
+
assert.equal(pi.calls[0].type, 'game', 'alongside the declared type it follows');
|
|
1688
|
+
|
|
1689
|
+
// the skipped case: absent, never an empty string invented on the way
|
|
1690
|
+
const skipped = fakePI();
|
|
1691
|
+
await P.federateInstance(coTenantWithDomain, { ...deps, platformIdentity: skipped });
|
|
1692
|
+
assert.equal(skipped.calls[0].description, undefined, 'a skipped interview passes nothing — upsertProject COALESCEs, so it nulls nothing either');
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1676
1696
|
await ta('federateInstance(dry-run): registers NOTHING, returns a placeholder block (so webEnvBody can still compose)', async () => {
|
|
1677
1697
|
await withEnv(HUB_ENV, async () => {
|
|
1678
1698
|
const pi = fakePI();
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -33,11 +33,12 @@ test('the platform floors are a pinned list — a floor cannot vanish quietly',
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
test('the shipped states files validate and sit beside the page they name', () => {
|
|
36
|
-
for (const rel of ['public-landing/projects.states.json', 'public-landing/index.states.json', 'modules/hall-ui/public/ranks.states.json']) {
|
|
36
|
+
for (const rel of ['public-landing/projects.states.json', 'public-landing/projects-empty.states.json', 'public-landing/index.states.json', 'modules/hall-ui/public/ranks.states.json']) {
|
|
37
37
|
const json = readJson(rel);
|
|
38
38
|
const v = lib.validateStatesFile(json);
|
|
39
39
|
assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
|
|
40
|
-
assert.equal(path.basename(rel), `${lib.pageName(json.page)}.states.json`, `${rel} is named for its page`);
|
|
40
|
+
if (!json.stub || !json.stub.fixtures) assert.equal(path.basename(rel), `${lib.pageName(json.page)}.states.json`, `${rel} is named for its page`);
|
|
41
|
+
else assert.ok(path.basename(rel).startsWith(`${lib.pageName(json.page)}-`), `${rel}: a fixture-aliased sibling is named <page>-<variant>.states.json`);
|
|
41
42
|
}
|
|
42
43
|
// the apex is dark only (task 1003336): its states files name the one mode, and
|
|
43
44
|
// render.js reads it as the default so a plain `--page /` never renders light
|
|
@@ -66,6 +67,8 @@ test('a states file that breaks the contract is refused with the reason named',
|
|
|
66
67
|
assert.ok(bad.errors.some((e) => /states\.ok\.auth/.test(e)));
|
|
67
68
|
const stub = lib.validateStatesFile({ page: '/x', stub: { prefix: 'builders' }, states: { a: {} } });
|
|
68
69
|
assert.ok(stub.errors.some((e) => /stub\.prefix/.test(e)));
|
|
70
|
+
assert.ok(lib.validateStatesFile({ page: '/x', stub: { fixtures: { a: 'Not Kebab' } }, states: { a: {} } }).errors.some((e) => /stub\.fixtures/.test(e)), 'a fixture alias names a kebab-case file');
|
|
71
|
+
assert.deepEqual(lib.validateStatesFile({ page: '/x', stub: { fixtures: { 'provisioning-instances': 'provisioning-instances-empty' } }, states: { a: {} } }).errors, [], 'the empty-list alias is the shape');
|
|
69
72
|
const modes = lib.validateStatesFile({ page: '/x', modes: ['sepia'], states: { a: {} } });
|
|
70
73
|
assert.ok(modes.errors.some((e) => e.startsWith('modes:')), 'an unknown mode is refused');
|
|
71
74
|
assert.ok(lib.validateStatesFile({ page: '/x', modes: [], states: { a: {} } }).errors.some((e) => e.startsWith('modes:')), 'an empty modes list is refused');
|
|
@@ -146,6 +149,33 @@ test('checkMock: the platform rules catch a page :root, a literal colour, a pinn
|
|
|
146
149
|
assert.ok(f5.some((r) => r.startsWith('every dark rule written twice')), f5.join(' | '));
|
|
147
150
|
});
|
|
148
151
|
|
|
152
|
+
test('serve.js answers the manage flows with the routes\' receipts, signed in only (task 1003345)', async () => {
|
|
153
|
+
const s = await lib.startStub({ surface: 'public-landing', auth: true });
|
|
154
|
+
try {
|
|
155
|
+
const post = (p, method = 'POST', body = '{}') => fetch(s.base + p, { method, headers: { 'content-type': 'application/json' }, body });
|
|
156
|
+
for (const [p, msg] of [['/api/bongos/provisioning/instances/101/teardown', 'Teardown queued.'], ['/api/bongos/provisioning/instances/105/restart', 'Restart queued.'],
|
|
157
|
+
['/api/bongos/provisioning/instances/101/domain', 'Address recorded.'], ['/api/bongos/provisioning/instances', 'Queued. The platform picks it up from here.']]) {
|
|
158
|
+
const r = await post(p); assert.equal(r.status, 200, p); assert.equal((await r.json()).message, msg, p);
|
|
159
|
+
}
|
|
160
|
+
const patched = await post('/api/bongos/provisioning/instances/101/settings', 'PATCH', '{"joinability":"apply"}');
|
|
161
|
+
assert.equal(patched.status, 200); assert.equal((await patched.json()).push.message, 'Saved. The project restarts to apply it.');
|
|
162
|
+
assert.equal((await post('/api/bongos/provisioning/instances/101/settings')).status, 404, 'settings take PATCH, not POST');
|
|
163
|
+
assert.equal((await post('/api/bongos/provisioning/instances/101/nope')).status, 404, 'no invented flow');
|
|
164
|
+
const asked = await (await fetch(s.base + '/api/bongos/provisioning/instances/101')).json();
|
|
165
|
+
assert.deepEqual(asked.open_intent && { action: asked.open_intent.action, state: asked.open_intent.state }, { action: 'teardown', state: 'queued' }, 'a teardown ask is remembered for the run — the page re-reads the project as queued');
|
|
166
|
+
assert.match(String(asked.open_intent.created_at), /^\d{4}-\d{2}-\d{2}T/, 'stamped like the route stamps it');
|
|
167
|
+
const untouched = await (await fetch(s.base + '/api/bongos/provisioning/instances/104')).json();
|
|
168
|
+
assert.equal(untouched.open_intent, null, 'an instance nobody asked about is unchanged');
|
|
169
|
+
const list = await (await fetch(s.base + '/api/bongos/provisioning/instances')).json();
|
|
170
|
+
assert.equal(list.instances.length, 5, 'the fifth row (Mooring) is served');
|
|
171
|
+
assert.equal(list.instances.find((i) => i.id === 105).open_intent.action, 'restart', 'the list carries the asks too');
|
|
172
|
+
} finally { s.stop(); }
|
|
173
|
+
const out = await lib.startStub({ surface: 'public-landing', auth: false });
|
|
174
|
+
try {
|
|
175
|
+
assert.equal((await fetch(out.base + '/api/bongos/provisioning/instances/101/teardown', { method: 'POST' })).status, 401, 'signed out, every ask is refused');
|
|
176
|
+
} finally { out.stop(); }
|
|
177
|
+
});
|
|
178
|
+
|
|
149
179
|
test('checkMock: a taste ban applies only when the instance declared it', () => {
|
|
150
180
|
const dashed = GOOD_MOCK.replace('Plain copy.', 'Plain — copy.');
|
|
151
181
|
const withBan = mock.checkMock(dashed, { bans: new Set(['em-dash']) });
|