@bongos/core 1.19.420 → 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.
Files changed (34) hide show
  1. package/.bongos-core.json +68 -33
  2. package/docs/copy-inventory.md +282 -281
  3. package/docs/copy-registry.json +375 -366
  4. package/docs/design/mocks/hub/mine.html +377 -0
  5. package/docs/design/projects-hub-direction-v2.md +25 -1
  6. package/docs/file-map.md +1 -0
  7. package/docs/module-api-changelog.md +2 -0
  8. package/modules/hall-ui/public/settings.html +3 -2
  9. package/modules/hall-ui/public/settings.js +10 -6
  10. package/modules/platform-identity/account-visibility.js +94 -3
  11. package/modules/platform-identity/platform-identity.js +45 -50
  12. package/modules/platform-identity/routes/connections.js +3 -3
  13. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -0
  14. package/modules/ui-design/kit/fixtures/provisioning-instances-empty.json +4 -0
  15. package/modules/ui-design/kit/fixtures/provisioning-instances.json +99 -0
  16. package/modules/ui-design/kit/lib.js +6 -2
  17. package/modules/ui-design/kit/probe.js +1 -1
  18. package/modules/ui-design/kit/render.js +1 -1
  19. package/modules/ui-design/kit/serve.js +63 -2
  20. package/package-lock.json +2 -2
  21. package/package.json +1 -1
  22. package/public-landing/assets/cosmos.css +224 -18
  23. package/public-landing/projects-empty.states.json +49 -0
  24. package/public-landing/projects-floor.states.json +63 -0
  25. package/public-landing/projects.html +112 -51
  26. package/public-landing/projects.probes.json +32 -7
  27. package/public-landing/projects.states.json +10 -6
  28. package/src/module-api.js +1 -1
  29. package/tests/account_privacy_flags.mjs +17 -10
  30. package/tests/effective_visibility_predicate.mjs +891 -0
  31. package/tests/profile_route.mjs +22 -11
  32. package/tests/projects_hub.mjs +33 -5
  33. package/tests/projects_hub_pre_uat.mjs +18 -6
  34. package/tests/ui_design_kit.mjs +32 -2
@@ -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 two that consent excludes.
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
- if (/SELECT profile_state, hide_stats/.test(sql)) {
395
- return { rows: [{ profile_state: hubAccount.profile_state, hide_stats: hubAccount.hide_stats }] };
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 provisional builder', async () => {
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'], 'only the consented, non-hidden builder is listed');
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
  });
@@ -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
- const glows = (SCRIPT.match(/var ORB_GLOWS = ([^;]*);/) || [, ''])[1];
176
- assert.doesNotMatch(glows, /rgba?\(/, 'orb glows must derive from the accents, not literal rgba');
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
@@ -1242,7 +1270,7 @@ test('the delete section is live markup with its body slot (C7)', () => {
1242
1270
  assert.match(HUB, /id="mDel"/, 'the volatile slot is real markup, not JS-built');
1243
1271
  assert.match(HUB, /id="mDelWhat"/, 'the teardown-vs-delete fold ships with the page');
1244
1272
  // the fold is a SIBLING of the volatile slot, so no re-render can drop it
1245
- 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];
1246
1274
  assert.ok(card, 'the delete card must be extractable');
1247
1275
  const slotAt = card.indexOf('id="mDel"');
1248
1276
  const foldAt = card.indexOf('id="mDelWhat"');
@@ -1322,7 +1350,7 @@ const fnHub = (name) => {
1322
1350
  test('the visibility section is live markup with its body slot and a sibling fold (C7)', () => {
1323
1351
  assert.match(HUB, /id="mVis"/, 'the volatile slot is real markup, not JS-built');
1324
1352
  assert.match(HUB, /id="mVisWhat"/, 'the what-changes fold ships with the page');
1325
- 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];
1326
1354
  assert.ok(card, 'the visibility card must be extractable');
1327
1355
  const slotAt = card.indexOf('id="mVis"');
1328
1356
  const foldAt = card.indexOf('id="mVisWhat"');
@@ -1404,7 +1432,7 @@ test('the settings reads fire on entry + tab return only — never on the in-fli
1404
1432
  test('the joinability section is live markup with its body slot and a sibling fold', () => {
1405
1433
  assert.match(HUB, /id="mJoin"/, 'the volatile slot is real markup, not JS-built');
1406
1434
  assert.match(HUB, /id="mJoinWhat"/, 'the what-changes fold ships with the page');
1407
- 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];
1408
1436
  assert.ok(card, 'the joinability card must be extractable');
1409
1437
  const slotAt = card.indexOf('id="mJoin"');
1410
1438
  const foldAt = card.indexOf('id="mJoinWhat"');
@@ -304,9 +304,19 @@ test('the bar is the page ground closed by a hairline, holds one row on a phone,
304
304
  assert.match(HUB, /#view-mine \.hero,#view-manage \.hero\{max-width:none;\}/, 'titles align with their cards');
305
305
  });
306
306
 
307
- 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', () => {
308
308
  assert.match(fn('renderCard'), /meta\.map\(function \(m\) \{ return '<span>' \+ m \+ '<\/span>'; \}\)\.join\(''\)/);
309
- assert.match(fn('renderManage'), /meta\.map\(function \(m\) \{ return '<span>' \+ m \+ '<\/span>'; \}\)\.join\(''\)/);
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');
310
320
  });
311
321
 
312
322
  test('an open teardown confirm survives the minute re-render', () => {
@@ -393,7 +403,7 @@ test('a heartbeat re-render keeps an open fold open and a half-typed domain type
393
403
 
394
404
  /* ── the owner's five calls of 2026-08-24 (task 1003250) ── */
395
405
  test('decision 6 — the manage section is "Take it offline", and the teardown truth ends by saying nothing erases', () => {
396
- 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];
397
407
  assert.ok(del, '#mDelete exists');
398
408
  assert.match(del, /<h2[^>]*>Take it offline<\/h2>/, 'the heading says what the control does');
399
409
  assert.doesNotMatch(HUB, /Delete the project/, 'the word "Delete" no longer promises an erase anywhere on the page');
@@ -604,9 +614,11 @@ test('completed rail steps are a way back, the repo picker is honest ARIA, the c
604
614
  });
605
615
 
606
616
  test('control nits: orb cursor, caret under reduced motion, no mode switch, home label, held reduce-motion query, target sizes', () => {
607
- assert.doesNotMatch(COSMOS, /\.orb\{flex:0 0 54px;[^}]*cursor:pointer/, 'the base orb is not a pointer');
608
- assert.match(COSMOS, /\na\.orb\{cursor:pointer;\}/, 'only the linked orb is');
609
- assert.doesNotMatch(COSMOS, /\.orb\.ghost\{[^}]*cursor:default/);
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');
610
622
  assert.doesNotMatch(HUB, /\.shell \.caret/, 'no caret to hide — the hub mark is static (task 1003300)');
611
623
  assert.doesNotMatch(SCRIPT, /modeToggle|paintModeToggle|resolvedMode|cb-color-mode/, 'the mode switch left the hub — the apex is dark only (task 1003336)');
612
624
  assert.doesNotMatch(HUB, /id="modeToggle"|class="modeBtn"/);
@@ -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']) });