@bongos/core 1.21.63 → 1.21.64
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 +30 -25
- package/docs/copy-inventory.md +42 -42
- package/docs/copy-registry.json +48 -48
- package/docs/module-api-changelog.md +7 -0
- package/docs/page-readings.json +67 -59
- package/modules/hall-ui/public/watch.css +15 -0
- package/modules/hall-ui/public/watch.html +3 -0
- package/modules/hall-ui/public/watch.js +87 -1
- package/modules/hall-ui/records/oversight-family.md +6 -0
- package/modules/onboarding/routes/access-requests.js +10 -4
- package/package-lock.json +9 -5
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/auth-admission.js +33 -2
- package/src/bongos/auth.js +2 -1
- package/src/module-api.js +7 -1
- package/tests/applicant_view_guild.mjs +101 -0
- package/tests/application_lifecycle.mjs +38 -1
- package/tests/module_api.mjs +1 -0
- package/tests/watch_applications_queue.mjs +112 -2
|
@@ -93,7 +93,8 @@ async function boot(opts = {}) {
|
|
|
93
93
|
rows = { pending: WAITING, invited: ADMITTED, dismissed: DECLINED },
|
|
94
94
|
rank = 'archon',
|
|
95
95
|
queueStatus = null, // non-200 for the queue read (403 seals it)
|
|
96
|
-
patchResult = null, // a scripted answer for PATCH /access-requests/:id
|
|
96
|
+
patchResult = null, // a scripted answer for PATCH /access-requests/:id (or a function of the id)
|
|
97
|
+
hub = null, // the hub origin OTB.hubOrigin() answers (task 1002304); null = no hub
|
|
97
98
|
search = '', // the page's query string (?invite=<login>, task 1002818)
|
|
98
99
|
} = opts;
|
|
99
100
|
|
|
@@ -114,7 +115,9 @@ async function boot(opts = {}) {
|
|
|
114
115
|
return ok({ requests: rows[status] || [] });
|
|
115
116
|
}
|
|
116
117
|
if (method === 'PATCH' && route.startsWith('/access-requests/')) {
|
|
117
|
-
|
|
118
|
+
const id = route.split('/').pop();
|
|
119
|
+
const scripted = typeof patchResult === 'function' ? patchResult(id) : patchResult;
|
|
120
|
+
return scripted || ok({ ok: true, request: { id } });
|
|
118
121
|
}
|
|
119
122
|
if (method === 'POST' && route === '/access-requests/invite') return ok({ ok: true });
|
|
120
123
|
// Every other panel on this page loads independently and degrades on its
|
|
@@ -175,6 +178,7 @@ async function boot(opts = {}) {
|
|
|
175
178
|
fmtDate: (iso) => String(iso).slice(0, 10),
|
|
176
179
|
rankLabel: (r) => String(r),
|
|
177
180
|
toast: (m) => toasts.push(m),
|
|
181
|
+
hubOrigin: () => hub,
|
|
178
182
|
},
|
|
179
183
|
OTBKit: kit,
|
|
180
184
|
OTBBranding: { currencyLabel: () => 'Credits' },
|
|
@@ -522,4 +526,110 @@ await test('?invite= that is not a GitHub username fills nothing', async () => {
|
|
|
522
526
|
assert.equal(at.documentObj.getElementById('gate-invite-login').value, 'octo', 'a pasted @ is dropped, as the form itself does');
|
|
523
527
|
});
|
|
524
528
|
|
|
529
|
+
// ===========================================================================
|
|
530
|
+
// A guild's applicants, together (task 1002304, R28; ADR 0365 D5)
|
|
531
|
+
// ===========================================================================
|
|
532
|
+
//
|
|
533
|
+
// The hub labels an applicant who applied WITH their guild, and core passes the
|
|
534
|
+
// label through as `guild` beside `profile` (tests/application_lifecycle.mjs
|
|
535
|
+
// proves that half). What a reviewer sees is asserted here: the rows gathered
|
|
536
|
+
// under the guild's name, "Invite all" as N ordinary invites, and the queue
|
|
537
|
+
// exactly as it was when no row carries a guild.
|
|
538
|
+
|
|
539
|
+
const LANTERN = { slug: 'lantern-works', name: 'Lantern Works' };
|
|
540
|
+
const guildRow = (id, login, guild, over = {}) => ({
|
|
541
|
+
id, github_login: login, display_name: null, note: null, status: 'pending',
|
|
542
|
+
created_at: '2026-09-03T10:00:00Z', resolved_at: null, vouched: true, resolved_by_login: null,
|
|
543
|
+
profile: null, guild, ...over,
|
|
544
|
+
});
|
|
545
|
+
const GUILD_QUEUE = [
|
|
546
|
+
guildRow('21', 'ada', LANTERN),
|
|
547
|
+
guildRow('22', 'kel', LANTERN),
|
|
548
|
+
guildRow('23', 'fox', { slug: null, name: 'Night Shift' }),
|
|
549
|
+
WAITING[1],
|
|
550
|
+
];
|
|
551
|
+
const groupsOf = (h) => h.gate().querySelectorAll('section').filter((s) => String(s.className).split(/\s+/).includes('ov-group'));
|
|
552
|
+
const groupNamed = (h, name) => groupsOf(h).find((g) => g.querySelector('.ov-group__name').textContent === name);
|
|
553
|
+
const rowIdsIn = (el) => el.querySelectorAll('li').map((r) => r.getAttribute('data-req-id'));
|
|
554
|
+
|
|
555
|
+
await test('applicants who applied with a guild are gathered under its name, the rest on their own', async () => {
|
|
556
|
+
const h = await boot({ rows: { pending: GUILD_QUEUE, invited: [], dismissed: [] } });
|
|
557
|
+
const lantern = groupNamed(h, 'Lantern Works');
|
|
558
|
+
assert.ok(lantern, 'the guild has a head of its own');
|
|
559
|
+
assert.deepEqual(rowIdsIn(lantern), ['21', '22'], 'both members, in the server order');
|
|
560
|
+
assert.equal(lantern.querySelector('.ov-group__n').textContent, '2');
|
|
561
|
+
assert.deepEqual(rowIdsIn(groupNamed(h, 'Night Shift')), ['23'], 'an unlisted guild is named too');
|
|
562
|
+
assert.deepEqual(rowIdsIn(groupNamed(h, 'On their own')), ['8'], 'a row with no guild stays out of every guild');
|
|
563
|
+
assert.equal(h.rows().length, 4, 'grouping moves rows, it never drops one');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
await test('no row carries a guild (an older hub, no hub): the queue is exactly the list it was', async () => {
|
|
567
|
+
const h = await boot();
|
|
568
|
+
assert.equal(groupsOf(h).length, 0, 'no group heads at all when there is nothing to group');
|
|
569
|
+
assert.equal(h.gate().querySelectorAll('ul').filter((u) => u.className === 'ov-rows').length, 1);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await test('an UNVOUCHED row naming a guild is not filed under it — the label is the vouch’s to give', async () => {
|
|
573
|
+
const impostor = guildRow('31', 'mallory', LANTERN, { vouched: false });
|
|
574
|
+
const h = await boot({ rows: { pending: [guildRow('21', 'ada', LANTERN), impostor], invited: [], dismissed: [] } });
|
|
575
|
+
assert.deepEqual(rowIdsIn(groupNamed(h, 'Lantern Works')), ['21']);
|
|
576
|
+
assert.deepEqual(rowIdsIn(groupNamed(h, 'On their own')), ['31']);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await test('a grouped row keeps its own pill and its own two decisions (ADR 0016)', async () => {
|
|
580
|
+
const h = await boot({ rows: { pending: GUILD_QUEUE, invited: [], dismissed: [] } });
|
|
581
|
+
const verbs = h.rowFor('21').querySelectorAll('[data-gate-action]').map((b) => b.getAttribute('data-gate-action'));
|
|
582
|
+
assert.deepEqual(verbs, ['invited', 'dismissed']);
|
|
583
|
+
assert.match(h.rowFor('21').querySelector('.fact-pill').textContent, /vouched/i);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await test('"Invite all" is N ordinary invites — one PATCH per waiting member, then one re-read', async () => {
|
|
587
|
+
const h = await boot({ rows: { pending: GUILD_QUEUE, invited: [], dismissed: [] } });
|
|
588
|
+
const btn = groupNamed(h, 'Lantern Works').querySelector('[data-gate-bulk]');
|
|
589
|
+
assert.ok(btn, 'a guild of two offers Invite all');
|
|
590
|
+
assert.match(btn.textContent, /Invite all 2/);
|
|
591
|
+
const reads = h.queueReads().length;
|
|
592
|
+
btn.fire('click', {});
|
|
593
|
+
await h.settle();
|
|
594
|
+
assert.deepEqual(h.patches().map((p) => p.path).sort(),
|
|
595
|
+
['/api/bongos/access-requests/21', '/api/bongos/access-requests/22'], 'each member, and nobody else');
|
|
596
|
+
for (const p of h.patches()) assert.deepEqual(plain(p.body), { status: 'invited' }, 'the very write the row button sends');
|
|
597
|
+
assert.equal(h.queueReads().length, reads + 1, 'the queue is re-read once, after every invite settled');
|
|
598
|
+
assert.match(h.toasts.at(-1), /Invited all 2/);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
await test('one refused invite does not strand the rest, and the toast says how many landed', async () => {
|
|
602
|
+
const h = await boot({
|
|
603
|
+
rows: { pending: GUILD_QUEUE, invited: [], dismissed: [] },
|
|
604
|
+
patchResult: (id) => (id === '22' ? fail(409, 'already_member', 'They have already signed in.') : null),
|
|
605
|
+
});
|
|
606
|
+
groupNamed(h, 'Lantern Works').querySelector('[data-gate-bulk]').fire('click', {});
|
|
607
|
+
await h.settle();
|
|
608
|
+
assert.equal(h.patches().length, 2, 'both invites were sent');
|
|
609
|
+
assert.match(h.toasts.at(-1), /Invited 1 of 2/);
|
|
610
|
+
assert.match(h.toasts.at(-1), /already signed in/, 'the server sentence, not [object Object]');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await test('a lone member has no "Invite all", and the decided views offer no bulk move', async () => {
|
|
614
|
+
const h = await boot({ rows: { pending: GUILD_QUEUE, invited: GUILD_QUEUE.map((r) => ({ ...r, status: 'invited' })), dismissed: [] } });
|
|
615
|
+
assert.equal(groupNamed(h, 'Night Shift').querySelector('[data-gate-bulk]'), null, 'one applicant already has their own Invite');
|
|
616
|
+
await h.switchView('invited');
|
|
617
|
+
assert.ok(groupNamed(h, 'Lantern Works'), 'the history is still grouped by guild');
|
|
618
|
+
assert.equal(h.gate().querySelector('[data-gate-bulk]'), null, 'a mass rescind is not a gesture this queue makes');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await test('the guild name links to its hub page only when it has one, and is always escaped', async () => {
|
|
622
|
+
const hostile = { slug: 'lantern-works', name: '<img src=x onerror=alert(1)>Lantern' };
|
|
623
|
+
const rows = [guildRow('21', 'ada', hostile), guildRow('23', 'fox', { slug: null, name: 'Night Shift' })];
|
|
624
|
+
const linked = await boot({ hub: 'https://hub.example', rows: { pending: rows, invited: [], dismissed: [] } });
|
|
625
|
+
const a = linked.gate().querySelectorAll('a').find((x) => String(x.className).includes('gate-guild__link'));
|
|
626
|
+
assert.ok(a, 'a public guild links to its page on the hub');
|
|
627
|
+
assert.equal(a.getAttribute('href'), 'https://hub.example/g/lantern-works');
|
|
628
|
+
assert.equal(linked.gate().querySelectorAll('img').length, 0, 'a guild name is user text on a page an Archon reads');
|
|
629
|
+
assert.equal(groupNamed(linked, 'Night Shift').querySelectorAll('a').length, 0, 'an unlisted guild has no page to open');
|
|
630
|
+
const noHub = await boot({ rows: { pending: rows, invited: [], dismissed: [] } });
|
|
631
|
+
assert.equal(noHub.gate().querySelectorAll('a').filter((x) => String(x.className).includes('gate-guild__link')).length, 0,
|
|
632
|
+
'no hub, no link — never a same-host URL claiming to be the hub');
|
|
633
|
+
});
|
|
634
|
+
|
|
525
635
|
summary();
|