@bongos/core 1.19.1027 → 1.19.1028

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.
@@ -19,10 +19,11 @@
19
19
  // 2. WHETHER ANYTHING LEAKED. No: every /my-projects* route is sign-in-gated
20
20
  // and reads the caller's OWN identity off the session. This is the half the
21
21
  // audit could not confirm, so it is asserted here rather than described.
22
- // 3. WHAT IS STILL MISSING. The invitations half has no caller anywhere — no
23
- // surface fetches the invite list, so a builder holding an invitation still
24
- // cannot act on it. That is tracked work (task 1002970, idea 1000903), and
25
- // pinning it keeps a known gap from decaying back into an unknown one.
22
+ // 3. WHAT WAS STILL MISSING. The invitations half had no caller anywhere — no
23
+ // surface fetched the invite list, so a builder holding an invitation could
24
+ // not act on it. Closed by task 1003774: the hub's /projects?view=mine now
25
+ // reads GET /my-projects/invites (idea 1000903). The pin below flipped from
26
+ // "no surface calls this" to "some surface does" for the same reason.
26
27
  //
27
28
  // Static reads only (no import of the app, no DB) → runs in the `unit` gate.
28
29
  //
@@ -88,13 +89,11 @@ test('every /my-projects handler reads the caller identity from the session', ()
88
89
  'no /my-projects handler may take the account id from the request body or query');
89
90
  });
90
91
 
91
- // ── 3. the gap that is left, named and tracked ─────────────────────────────
92
- test('the invitations API still has no caller — the tracked gap, pinned', () => {
93
- // Deliberately asserts a MISSING feature. Task 1003308 removed the only
94
- // surface that fetched invitations, so a builder who holds one has nowhere to
95
- // accept or decline it. Task 1002970 (blocked on task 1002969) and idea
96
- // 1000903 own the replacement; this pin exists so the gap stays visible until
97
- // one of them lands, and so it is impossible to close this quietly.
92
+ // ── 3. the gap is closed — a surface now calls the invite list ──────────────
93
+ test('an invitations surface exists — the hub reads GET /my-projects/invites (task 1003774)', () => {
94
+ // The quoted form is the actual fetch-call shape (`API + '/my-projects/invites'`);
95
+ // matching on that, rather than the bare path, keeps a doc comment that merely
96
+ // NAMES the route from satisfying this on its own.
98
97
  const surfaces = [];
99
98
  const modules = path.join(ROOT, 'modules');
100
99
  for (const key of fs.readdirSync(modules)) {
@@ -102,11 +101,24 @@ test('the invitations API still has no caller — the tracked gap, pinned', () =
102
101
  if (!fs.existsSync(pub)) continue;
103
102
  for (const f of fs.readdirSync(pub)) {
104
103
  if (!/\.(js|html)$/.test(f)) continue;
105
- if (fs.readFileSync(path.join(pub, f), 'utf8').includes('/my-projects/invites')) {
104
+ if (fs.readFileSync(path.join(pub, f), 'utf8').includes("'/my-projects/invites'")) {
106
105
  surfaces.push(`modules/${key}/public/${f}`);
107
106
  }
108
107
  }
109
108
  }
110
- assert.deepEqual(surfaces, [],
111
- `an invitations surface exists now (${surfaces.join(', ')}) — good: delete this pin and close task 1002970 / idea 1000903`);
109
+ assert.ok(surfaces.length > 0,
110
+ 'no surface calls GET /my-projects/invites — the gap task 1003774 closed is back open');
111
+ });
112
+
113
+ // ── 4. the accept flip stays retired (ADR 0335) ─────────────────────────────
114
+ // Task 1003774 restores the invitations list but deliberately does NOT wire an
115
+ // Accept button to POST /my-projects/invites/:id/accept: that hub-only flip
116
+ // never admits anyone into the project's own instance (ADR 0335), so it is a
117
+ // dead end the moment a caller exists to press it. Each invite renders as a
118
+ // way IN (a link to the project) instead — signing in there is the acceptance
119
+ // act (task 1002969).
120
+ test('the hub never calls the accept route — ADR 0335 retired that flip', () => {
121
+ const html = read('modules/public-landing/public/projects.html');
122
+ assert.doesNotMatch(html, /invites\/[^'"]*\/accept/,
123
+ 'a hub caller of the accept route reopens the dead end ADR 0335 closed — render the invite as a link into the project instead');
112
124
  });
@@ -508,3 +508,43 @@ test('a stamped asset still serves — the query string is not part of the filen
508
508
  assert.equal(res.status, 200, 'express.static must ignore the ?v= when resolving the file');
509
509
  assert.match(res.body, /\.shell\{/, 'and it must be the real stylesheet');
510
510
  });
511
+
512
+ // ── #view-mine: invitations, projects-you-build-on, join-by-URL (task 1003774) ──
513
+ // The hall's my-projects page was deleted in task 1003308 on the owner's own
514
+ // decision that this is a hub surface; these three reads/actions were left with
515
+ // nowhere to live until now. loadMemberships() already fetched GET /my-projects
516
+ // for the map's Apply button (R10, task 1002286) — this just renders the rest
517
+ // of what that same read carries, plus the separate invites list.
518
+ test('#view-mine reads the invitations list, alongside the building-on and join surfaces', async () => {
519
+ const { body } = await request('/projects', 'game.test.local');
520
+ assert.match(body, /\/my-projects\/invites/, 'GET /my-projects/invites must be called');
521
+ assert.match(body, /id="invitesList"/);
522
+ assert.match(body, /id="buildingList"/);
523
+ assert.match(body, /\/my-projects\/join/, 'the join-by-URL field must post to /my-projects/join');
524
+ assert.match(body, /id="joinOrigin"/);
525
+ assert.match(body, /id="joinNote"/, 'the applicant note (task 1002330) rides along, and stays optional');
526
+ assert.match(body, /\/my-projects\/'\s*\+\s*encodeURIComponent/, 'Leave must address the project by its own client id');
527
+ assert.match(body, /origin = 'https:\/\/' \+ origin/,
528
+ 'a bare host ("grove.cloudbongos.com") must be assumed https:// rather than sent as-is — the server\'s origin lookup is an exact match');
529
+ });
530
+
531
+ test('an invite renders as a way INTO the project, never the retired accept flip (ADR 0335)', async () => {
532
+ const { body } = await request('/projects', 'game.test.local');
533
+ // the hub-only accept flip never admits anyone to the project's own instance
534
+ // (ADR 0335) — a caller of it here would reopen the exact dead end that ADR
535
+ // retired, so the invite's action is an outbound link instead
536
+ assert.doesNotMatch(body, /invites\/[^'"]*\/accept/, 'no caller of the retired hub accept route');
537
+ assert.match(body, /Open project/, 'the invite links into the project — signing in there is the acceptance act');
538
+ assert.match(body, /'\/my-projects\/invites\/' \+ encodeURIComponent\([^)]+\) \+ '\/decline'/,
539
+ 'decline still works: it only clears the hub\'s own pending row');
540
+ });
541
+
542
+ test('the map card\'s Apply button reads as truly inert once applied, with a live status region (R10 fixups)', async () => {
543
+ const page = await request('/projects', 'game.test.local');
544
+ assert.match(page.body, /id="cardApplyMsg" role="status" aria-live="polite"/, 'the apply outcome must be announced to assistive tech');
545
+ const stamped = (page.body.match(/\/assets\/cosmos\.css\?v=[0-9a-f]+/) || [])[0];
546
+ assert.ok(stamped, 'need a stamped URL to fetch');
547
+ const css = await request(stamped);
548
+ assert.match(css.body, /\.btn\.ghost:disabled\{/, 'a disabled ghost button (the Applied state) must not keep full-strength styling');
549
+ assert.match(css.body, /\.projCard \.cardActions \.projMsg\{flex-basis:100%;?\}/, 'the outcome must sit under the buttons, not beside them');
550
+ });