@bongos/core 1.23.13 → 1.23.15
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 +90 -55
- 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 +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +22 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +1000 -946
- package/docs/copy-registry.json +2009 -1465
- package/docs/module-api-changelog.md +4 -2
- package/docs/page-inventory.json +10 -4
- package/docs/page-readings.json +593 -573
- package/modules/copy-desk/page-status.js +2 -2
- package/modules/copy-desk/tests/copy_page_status.mjs +1 -1
- package/modules/hall-ui/public/approval-queue-lib.js +1 -1
- package/modules/hall-ui/public/approval-queue.css +26 -12
- package/modules/hall-ui/public/approval-queue.js +40 -16
- package/modules/hall-ui/public/studio-lib.js +5 -3
- package/modules/hall-ui/public/studio.html +8 -8
- package/modules/hall-ui/public/studio.js +25 -5
- package/modules/hall-ui/public/studio.states.json +54 -4
- package/modules/hall-ui/public/welcome-boot.js +62 -12
- package/modules/platform-identity/builder-of-the-week.js +180 -0
- package/modules/platform-identity/migrations/platform_identity_032_weekly_features.sql +53 -0
- package/modules/platform-identity/routes/community-leaderboard.js +25 -1
- package/modules/public-landing/public/account.html +2 -1
- package/modules/public-landing/public/community-week-none.states.json +16 -0
- package/modules/public-landing/public/community.html +142 -13
- package/modules/public-landing/public/community.states.json +6 -4
- package/modules/public-landing/public/connections.html +2 -1
- package/modules/public-landing/public/contact.html +2 -1
- package/modules/public-landing/public/guild.html +2 -1
- package/modules/public-landing/public/index.html +2 -1
- package/modules/public-landing/public/invitations.html +2 -1
- package/modules/public-landing/public/privacy.html +2 -1
- package/modules/public-landing/public/projects.html +2 -1
- package/modules/public-landing/public/terms.html +2 -1
- package/modules/ui-design/kit/fixtures/community-week-none.json +6 -0
- package/modules/ui-design/kit/fixtures/community-week.json +11 -0
- package/modules/ui-design/kit/serve.js +8 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +15 -1
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +5 -5
- package/tests/builder_of_the_week.mjs +266 -0
- package/tests/builder_of_the_week_db.mjs +166 -0
- package/tests/hall_approval_queue.mjs +33 -9
- package/tests/hall_studio_home.mjs +74 -7
- package/tests/hub_account_page.mjs +2 -2
- package/tests/hub_community_page.mjs +136 -25
- package/tests/landing_legal_pages.mjs +2 -2
- package/tests/privacy_matrix.mjs +53 -8
- package/tests/welcome_boot_browser.mjs +45 -3
|
@@ -129,7 +129,7 @@ function heldClock(h, m) {
|
|
|
129
129
|
return Held;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY } = {}) {
|
|
132
|
+
function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY, rank = 'metic', queue = null } = {}) {
|
|
133
133
|
const root = augment(makeEl('body', {}));
|
|
134
134
|
root.innerHTML = body;
|
|
135
135
|
const byId = (id) => walk(root, (c) => (c.id === id ? c : null));
|
|
@@ -156,7 +156,7 @@ function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY } =
|
|
|
156
156
|
const body = opts ? opts.body : undefined;
|
|
157
157
|
requests.push({ method, url, body });
|
|
158
158
|
const key = `${method} ${url.replace(/^\/api\/bongos/, '')}`;
|
|
159
|
-
if (key === 'GET /me') return signedIn ? ok({ builder: { id: ME, display_name: 'Artist A', rank
|
|
159
|
+
if (key === 'GET /me') return signedIn ? ok({ builder: { id: ME, display_name: 'Artist A', rank } }) : { ok: false, status: 401, data: {} };
|
|
160
160
|
const a = answers[key];
|
|
161
161
|
if (typeof a === 'function') return a(body);
|
|
162
162
|
if (a) return a;
|
|
@@ -170,6 +170,7 @@ function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY } =
|
|
|
170
170
|
localStorage: { getItem: () => null, setItem() {} },
|
|
171
171
|
addEventListener() {},
|
|
172
172
|
scrollTo() {},
|
|
173
|
+
...(queue ? { OTBApprovalQueue: queue.api, OTBApprovalQueueLib: require('../modules/hall-ui/public/approval-queue-lib.js') } : {}),
|
|
173
174
|
};
|
|
174
175
|
// setInterval is held, never real (a real one would keep the run alive): the
|
|
175
176
|
// test fires each registered tick by hand, as a minute passing.
|
|
@@ -243,8 +244,8 @@ try {
|
|
|
243
244
|
]);
|
|
244
245
|
assert.equal(lib.landingHost('https://cloudbongos.com/', 'builders.x.test'), 'cloudbongos.com', 'the stamped origin wins');
|
|
245
246
|
assert.equal(lib.landingHost('{{publicOrigin}}', 'builders.x.test'), 'x.test', 'an unstamped token is no address');
|
|
246
|
-
assert.equal(lib.askedLine(NEXT.asked_pages), '1 page has been asked for. It comes up first in
|
|
247
|
-
assert.equal(lib.askedLine(4), '4 pages have been asked for. They come up first in
|
|
247
|
+
assert.equal(lib.askedLine(NEXT.asked_pages), '1 page has been asked for. It comes up first in Work through tweaks.');
|
|
248
|
+
assert.equal(lib.askedLine(4), '4 pages have been asked for. They come up first in Work through tweaks.');
|
|
248
249
|
assert.equal(lib.askedLine(0), '');
|
|
249
250
|
});
|
|
250
251
|
|
|
@@ -260,7 +261,7 @@ try {
|
|
|
260
261
|
assert.deepEqual(rows.map((r) => [r.title, r.state, r.age, r.stuck]), [
|
|
261
262
|
['Contact', 'Approved, going live', 'for 17 days', true],
|
|
262
263
|
['Tasks', 'Submitted, waiting to be applied', 'for 1 day', false],
|
|
263
|
-
['Status', 'Needs
|
|
264
|
+
['Status', 'Needs to be reviewed', 'since today', true],
|
|
264
265
|
]);
|
|
265
266
|
assert.deepEqual(lib.inFlightRows(null, NOW), []);
|
|
266
267
|
});
|
|
@@ -309,13 +310,79 @@ try {
|
|
|
309
310
|
assert.equal(s.requests.filter((r) => /copy-desk\/(tally|next|pages)/.test(r.url)).length, 0);
|
|
310
311
|
});
|
|
311
312
|
|
|
313
|
+
// A stand-in for the Approval queue panel: what the studio asks of it is the point here.
|
|
314
|
+
const stubQueue = ({ count = 2, pending = 0 } = {}) => {
|
|
315
|
+
const calls = { open: [], refresh: 0 };
|
|
316
|
+
const api = { mount: (o) => ({
|
|
317
|
+
open: async (...a) => { calls.open.push(a); },
|
|
318
|
+
refresh: async () => { calls.refresh += 1; o.onCount(count); return count; },
|
|
319
|
+
count: () => count,
|
|
320
|
+
notAppliedCount: () => pending,
|
|
321
|
+
}) };
|
|
322
|
+
return { calls, api };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
await test('the approve door shows for metic and up, and never mounts the queue below that', async () => {
|
|
326
|
+
const q = stubQueue();
|
|
327
|
+
const metic = bootStudio({ answers: HOME, queue: q });
|
|
328
|
+
await metic.settle();
|
|
329
|
+
assert.equal(metic.el('studio-approvals').hidden, false);
|
|
330
|
+
assert.equal(q.calls.refresh, 1, 'the door hears how many wait');
|
|
331
|
+
const low = stubQueue();
|
|
332
|
+
const thetes = bootStudio({ answers: HOME, queue: low, rank: 'thetes' });
|
|
333
|
+
await thetes.settle();
|
|
334
|
+
assert.equal(thetes.el('studio-approvals').hidden, true, 'below metic the door stays hidden');
|
|
335
|
+
assert.equal(low.calls.refresh, 0, 'and its queue is never read');
|
|
336
|
+
const gone = bootStudio({ answers: HOME, rank: 'archon' });
|
|
337
|
+
await gone.settle();
|
|
338
|
+
assert.equal(gone.el('studio-approvals').hidden, true, 'no panel loaded: no door to nowhere');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await test('an empty approval queue says so in a toast; a submitted-not-applied round still opens the dialog', async () => {
|
|
342
|
+
const empty = stubQueue({ count: 0 });
|
|
343
|
+
const a = bootStudio({ answers: HOME, queue: empty });
|
|
344
|
+
await a.settle();
|
|
345
|
+
await a.click('studio-approvals');
|
|
346
|
+
assert.equal(empty.calls.open.length, 0, 'no blank dialog');
|
|
347
|
+
assert.match(a.el('toast').textContent, /Nothing is waiting for your sign-off/);
|
|
348
|
+
const pending = stubQueue({ count: 0, pending: 1 });
|
|
349
|
+
const b = bootStudio({ answers: HOME, queue: pending });
|
|
350
|
+
await b.settle();
|
|
351
|
+
await b.click('studio-approvals');
|
|
352
|
+
assert.equal(pending.calls.open.length, 1, 'the not-applied list is still reachable');
|
|
353
|
+
const full = stubQueue({ count: 2 });
|
|
354
|
+
const c = bootStudio({ answers: HOME, queue: full });
|
|
355
|
+
await c.settle();
|
|
356
|
+
await c.click('studio-approvals');
|
|
357
|
+
assert.equal(full.calls.open.length, 1);
|
|
358
|
+
assert.equal(full.calls.open[0][1].reload, false, 'one read, then open without re-reading');
|
|
359
|
+
assert.equal(full.calls.refresh, 2, 'the page load and the click, nothing more');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await test('a page that Needs to be reviewed opens its own approval card from the status panel', async () => {
|
|
363
|
+
const q = stubQueue();
|
|
364
|
+
const waiting = { ...PAGES, in_flight: [{ page_id: 'landing:contact', title: 'Contact', surface: 'landing', state: 'waiting_for_artist', state_word: 'Waiting for your review', since: day(1), stuck: false }] };
|
|
365
|
+
const s = bootStudio({ answers: { ...HOME, 'GET /copy-desk/pages': ok(waiting) }, queue: q });
|
|
366
|
+
await s.settle();
|
|
367
|
+
await s.click('st-status');
|
|
368
|
+
const overlay = s.el('st-panel');
|
|
369
|
+
overlay.fire('click', { target: overlay.querySelector('[data-review-page]'), preventDefault() {} });
|
|
370
|
+
await s.settle();
|
|
371
|
+
assert.equal(s.el('st-panel').hidden, true, 'the panel gives way to the card');
|
|
372
|
+
assert.deepEqual(JSON.parse(JSON.stringify(q.calls.open)).map((c) => c[0]), ['landing:contact']);
|
|
373
|
+
const low = bootStudio({ answers: { ...HOME, 'GET /copy-desk/pages': ok(waiting) }, queue: stubQueue(), rank: 'thetes' });
|
|
374
|
+
await low.settle();
|
|
375
|
+
await low.click('st-status');
|
|
376
|
+
assert.equal(low.el('st-panel-body').querySelector('[data-review-page]'), null, 'below metic the row opens the editor instead');
|
|
377
|
+
});
|
|
378
|
+
|
|
312
379
|
await test('the Tweak/CopyWrite Flow: the recommendation, pick another swaps it, take it claims THAT page and opens it', async () => {
|
|
313
380
|
const s = bootStudio({ answers: { ...HOME, 'POST /copy-desk/pages/builders:studio/claim': ok({ ok: true, outcome: 'created' }) } });
|
|
314
381
|
await s.settle();
|
|
315
382
|
await s.click('st-flow');
|
|
316
383
|
assert.equal(s.el('st-panel').hidden, false);
|
|
317
384
|
assert.ok(s.html.classList.contains('st-open'));
|
|
318
|
-
assert.equal(s.el('st-panel-title').textContent, '
|
|
385
|
+
assert.equal(s.el('st-panel-title').textContent, 'Work through tweaks');
|
|
319
386
|
const body = () => s.el('st-panel-body').innerHTML;
|
|
320
387
|
assert.match(body(), /st-next__page">Projects</);
|
|
321
388
|
assert.match(body(), /landing · 2 people asked for it/);
|
|
@@ -346,7 +413,7 @@ try {
|
|
|
346
413
|
await s.settle();
|
|
347
414
|
await s.click('st-status');
|
|
348
415
|
const body = s.el('st-panel-body').innerHTML;
|
|
349
|
-
assert.equal(s.el('st-panel-title').textContent, '
|
|
416
|
+
assert.equal(s.el('st-panel-title').textContent, 'Needs to be reviewed');
|
|
350
417
|
assert.deepEqual([...body.matchAll(/st-surface__name">([^<]*)</g)].map((m) => m[1]), ['Landing', 'Builders hall', 'Status']);
|
|
351
418
|
assert.match(body, /st-surface__where">cloudbongos\.com</);
|
|
352
419
|
assert.match(body, /1 of 2 tweaked/);
|
|
@@ -135,8 +135,8 @@ test('the bands say what the hub’s say and lead into /projects; Account is the
|
|
|
135
135
|
assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProjects')), [
|
|
136
136
|
['Sky', '/projects?view=map'], ['Table', '/projects?view=map&sub=table'],
|
|
137
137
|
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
138
|
-
// the Community menu's
|
|
139
|
-
['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
138
|
+
// the Community menu's four sub-pages, folded into the band under 480px (tasks 1003363, 1002981)
|
|
139
|
+
['Leaderboard', '/community'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
140
140
|
['Builders you may know', '/connections#connSuggest'],
|
|
141
141
|
], 'this page has no views of its own, so every item names /projects in full');
|
|
142
142
|
assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProfile')), [
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// tests/hub_community_page.mjs — the hub's community pages (task 1003363; ADR 0217 D1,
|
|
2
2
|
// ADR 0337 D1/D3/D4.1, docs/specs/bongos-v1-community-tab.md D1/D3/D4/D8).
|
|
3
3
|
//
|
|
4
|
-
// One file served as
|
|
5
|
-
// /community the leaderboard, /community/
|
|
6
|
-
// the name-only list of hidden
|
|
7
|
-
// platform-identity reads and add no
|
|
4
|
+
// One file served as four sub-pages of the bar's Community menu (owner, 2026-10-27):
|
|
5
|
+
// /community the leaderboard, /community/week the builder of the week (task 1002981),
|
|
6
|
+
// /community/find the builder search, /community/recruiters the name-only list of hidden
|
|
7
|
+
// builders open to recruiters. They render existing platform-identity reads and add no
|
|
8
|
+
// rule of their own. What this file pins:
|
|
8
9
|
// • the markup: the hub's bar with the Community menu as the current section, no top
|
|
9
10
|
// block, noindex, dark only, the routes' own windows and query limits
|
|
10
11
|
// • viewing is ungated: the leaderboard and the search read the same routes for every
|
|
@@ -13,11 +14,12 @@
|
|
|
13
14
|
// item marked current; the board in the server's order with its `place`, the window
|
|
14
15
|
// dropdown and the crafts, the empty, failed and rate-limited board; the search and
|
|
15
16
|
// its refusals; the recruiter list as a door signed out, a refusal on a 403, and rows
|
|
16
|
-
// only from a 200 — name, face and login, never the ranked half's numbers
|
|
17
|
+
// only from a 200 — name, face and login, never the ranked half's numbers; the builder
|
|
18
|
+
// of the week as one line on the board and as its own page, drawn as the server sent it
|
|
17
19
|
// • the Community menu on every page that wears the hub's bar (apex-only where the page
|
|
18
20
|
// also serves a child), folded into the Projects band on phones
|
|
19
|
-
// • the routes: the
|
|
20
|
-
// • the kit: the states files, and the stub answering as the
|
|
21
|
+
// • the routes: the four paths served on the hub's apex only
|
|
22
|
+
// • the kit: the states files, and the stub answering as the four reads do
|
|
21
23
|
//
|
|
22
24
|
// DB-free. Run: node --test tests/hub_community_page.mjs
|
|
23
25
|
|
|
@@ -44,7 +46,8 @@ const STYLE = PAGE.split('<style>')[1].split('</style>')[0].replace(/\/\*[\s\S]*
|
|
|
44
46
|
const MAIN = PAGE.split('<main class="acct" id="comm">')[1].split('</main>')[0];
|
|
45
47
|
const BOARD_JS = fs.readFileSync(path.join(PI, 'community-leaderboard.js'), 'utf8');
|
|
46
48
|
const SEARCH_JS = fs.readFileSync(path.join(PI, 'builder-search.js'), 'utf8');
|
|
47
|
-
const SUBPAGES = [['/community', 'Leaderboard'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
49
|
+
const SUBPAGES = [['/community', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
50
|
+
const WEEK_JS = fs.readFileSync(path.join(PI, 'builder-of-the-week.js'), 'utf8');
|
|
48
51
|
|
|
49
52
|
// ── 1. the markup ────────────────────────────────────────────────────────────
|
|
50
53
|
|
|
@@ -72,7 +75,7 @@ test('the bar and bands are /connections’ but for the sign-in return and the c
|
|
|
72
75
|
|
|
73
76
|
test('no top block: one visually hidden h1, then one section per sub-page, only the leaderboard shown in the markup', () => {
|
|
74
77
|
assert.match(MAIN, /^\s*<h1 class="vh">Community<\/h1>\s*(<!--[\s\S]*?-->\s*)?<div class="mineExtra">/, 'nothing is drawn above the first section (the /design floor)');
|
|
75
|
-
const order = ['<section id="board" data-view="leaderboard" aria-labelledby="boardH">', '<section id="find" data-view="find" aria-labelledby="findH" hidden>', '<section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>'].map((s) => MAIN.indexOf(s));
|
|
78
|
+
const order = ['<section id="board" data-view="leaderboard" aria-labelledby="boardH">', '<section id="week" data-view="week" aria-labelledby="weekH" hidden>', '<section id="find" data-view="find" aria-labelledby="findH" hidden>', '<section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>'].map((s) => MAIN.indexOf(s));
|
|
76
79
|
assert.ok(order.every((i) => i > 0), 'every sub-page is real markup the script only shows');
|
|
77
80
|
assert.deepEqual([...order].sort((a, b) => a - b), order);
|
|
78
81
|
assert.match(MAIN, /<ol class="commRows" id="boardList" data-unit="\{\{currency\}\}" aria-labelledby="boardH"><\/ol>/, 'the unit is the instance’s own word, stamped by the server');
|
|
@@ -82,6 +85,23 @@ test('no top block: one visually hidden h1, then one section per sub-page, only
|
|
|
82
85
|
assert.match(MAIN, /<h2 id="recruitH">Open to recruiters<\/h2>/, 'the hide flag is "hidden", never "private" (ADR 0337)');
|
|
83
86
|
assert.match(MAIN, /<a class="btn" id="recruitSignin" href="\/api\/bongos\/auth\/web\/start\?return=\/community\/recruiters&repos=1">/);
|
|
84
87
|
for (const id of ['recruitDoor', 'recruitClosed', 'recruitFailed', 'recruitEmpty']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
88
|
+
// the builder of the week (task 1002981): its line on the board and its page wait for the read
|
|
89
|
+
assert.match(MAIN, /<h2 id="boardH">Leaderboard<\/h2>\n\s*<p class="commCrownLine" id="boardCrown" hidden><\/p>/, 'the crown line sits under the board’s heading, above the rows');
|
|
90
|
+
for (const id of ['weekCrown', 'weekNone', 'weekFailed', 'pastH']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
91
|
+
assert.match(MAIN, /<ol class="commRows weekRows" id="weekPast" aria-labelledby="pastH"><\/ol>/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('the builder of the week states its rule in full, and the rule is the module’s (spec D5: the formula is public)', () => {
|
|
95
|
+
const rule = MAIN.split('<h3 class="commSub" id="weekRuleH">How it is chosen</h3>')[1].split('<h3 ')[0];
|
|
96
|
+
assert.match(rule, /the most \{\{currency\}\} across every project in one week, Monday to Sunday \(UTC\)/, 'the instance’s own unit, the week the module counts');
|
|
97
|
+
assert.match(rule, /shipped at least one work that week/);
|
|
98
|
+
assert.match(WEEK_JS, /HAVING sum\(tasks_shipped\) >= 1/, 'the floor the page states is the one the query applies');
|
|
99
|
+
assert.match(rule, /A tie goes to whoever shipped more works, then to whoever earned more karma\./);
|
|
100
|
+
assert.match(WEEK_JS, /ORDER BY sum\(credits\) DESC, sum\(tasks_shipped\) DESC, sum\(karma\) DESC,/, 'the tie order the page states is the query’s');
|
|
101
|
+
assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
|
|
102
|
+
assert.match(WEEK_JS, /rollupVisibleSql\('pa'\)/);
|
|
103
|
+
assert.match(rule, /Each week is decided once, the first time anyone opens this page after it ends, and it does not change after that\./);
|
|
104
|
+
assert.match(WEEK_JS, /ON CONFLICT \(week_start\) DO NOTHING/);
|
|
85
105
|
});
|
|
86
106
|
|
|
87
107
|
test('the window dropdown and the query limits are the routes’ own (boundary-forced copies)', () => {
|
|
@@ -101,12 +121,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
|
|
|
101
121
|
assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
|
|
102
122
|
});
|
|
103
123
|
|
|
104
|
-
test('the script builds no markup from strings, reads only the
|
|
124
|
+
test('the script builds no markup from strings, reads only the five routes, and never re-ranks or reads the ranked tier', () => {
|
|
105
125
|
assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
|
|
106
|
-
// every path literal in the code: the API base, the
|
|
126
|
+
// every path literal in the code: the API base, the five reads, the four sub-pages, the
|
|
107
127
|
// sign-in door and the hall's invite box the Recruit door opens (task 1002292)
|
|
108
128
|
const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
|
|
109
|
-
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/my-projects', '/scouting']);
|
|
129
|
+
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
|
|
110
130
|
assert.doesNotMatch(CODE, /method:/, 'it only reads');
|
|
111
131
|
assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
|
|
112
132
|
assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
|
|
@@ -147,7 +167,13 @@ const find = (n, cls) => {
|
|
|
147
167
|
};
|
|
148
168
|
|
|
149
169
|
const ROW = (place, handle, credits, extra = {}) => ({ place, handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: 2, karma: 1, disciplines: ['engineer'], ...extra });
|
|
170
|
+
const WEEK = () => ({ week: { start: '2026-10-19', end: '2026-10-26' },
|
|
171
|
+
winner: { week_start: '2026-10-19', handle: 'ana', github_login: 'ana', display_name: 'Ana <i>L</i>', avatar_url: 'https://avatars.example/ana', credits: 1240, tasks_shipped: 1 },
|
|
172
|
+
past: [{ week_start: '2026-10-12', handle: 'bo', github_login: 'bo', display_name: null, avatar_url: 'data:x', credits: 300, tasks_shipped: 2 },
|
|
173
|
+
{ week_start: '2026-09-28', handle: 'cy', github_login: 'cy', display_name: 'Cy', avatar_url: null, credits: 90, tasks_shipped: 1 },
|
|
174
|
+
{ week_start: '2025-12-29', handle: 'di', github_login: 'di', display_name: 'Di', avatar_url: null, credits: 10, tasks_shipped: 1 }] });
|
|
150
175
|
const READS = () => ({
|
|
176
|
+
'/community/builder-of-the-week': { status: 200, body: WEEK() },
|
|
151
177
|
'/community/leaderboard?window=all': { status: 200, body: { window: 'all', count: 99, results: [ROW(26, 'ana', 1200), ROW(27, 'bo', 1500, { tasks_shipped: 1, karma: 0, avatar_url: 'javascript:alert(1)', display_name: null })] } },
|
|
152
178
|
'/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
|
|
153
179
|
'/community/leaderboard?window=all&discipline=artist': { status: 200, body: { window: 'all', results: [] } },
|
|
@@ -199,9 +225,9 @@ async function boot({ pathname = '/community', me = null, reads = READS() } = {}
|
|
|
199
225
|
}
|
|
200
226
|
|
|
201
227
|
test('each path shows its one section, marks its menu items current and names itself; any other shape is the leaderboard', async () => {
|
|
202
|
-
for (const [pathname, title, section] of [['/community', 'Leaderboard', 'board'], ['/community/', 'Leaderboard', 'board'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
|
|
228
|
+
for (const [pathname, title, section] of [['/community', 'Leaderboard', 'board'], ['/community/', 'Leaderboard', 'board'], ['/community/week', 'Builder of the week', 'week'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
|
|
203
229
|
const ui = await boot({ pathname });
|
|
204
|
-
assert.deepEqual(['board', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
|
|
230
|
+
assert.deepEqual(['board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
|
|
205
231
|
const href = SUBPAGES.find(([, t]) => t === title)[0];
|
|
206
232
|
assert.deepEqual(ui.current(), [href, href], `${pathname}: its Community item and its folded twin in the Projects band`);
|
|
207
233
|
assert.equal(ui.document.title, `Cloud Bongos — Community · ${title}`);
|
|
@@ -211,7 +237,7 @@ test('each path shows its one section, marks its menu items current and names it
|
|
|
211
237
|
|
|
212
238
|
test('the leaderboard, signed out: the all-time board in the server’s order with the server’s places, and nothing asked about the viewer', async () => {
|
|
213
239
|
const ui = await boot();
|
|
214
|
-
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all']);
|
|
240
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week']);
|
|
215
241
|
assert.ok(ui.requests.every((r) => r.credentials === 'same-origin'));
|
|
216
242
|
assert.equal(ui.$('boardLoading').hidden, true);
|
|
217
243
|
assert.equal(ui.$('boardFailed').hidden, true);
|
|
@@ -229,7 +255,7 @@ test('the leaderboard, signed out: the all-time board in the server’s order wi
|
|
|
229
255
|
assert.equal(find(bo, 'mineRow__hint').textContent, '1 work shipped · 0 karma');
|
|
230
256
|
assert.equal(ui.$('boardNote').textContent, 'Credits earned across every project, since each builder started.');
|
|
231
257
|
const signedIn = await boot({ me: { github_login: 'someone' } });
|
|
232
|
-
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
|
|
258
|
+
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
|
|
233
259
|
assert.equal(signedIn.$('boardList').textContent, ui.$('boardList').textContent, 'the same rows for every viewer');
|
|
234
260
|
});
|
|
235
261
|
|
|
@@ -325,6 +351,76 @@ test('recruiters: a door signed out, a refusal on a 403, rows only from a 200 as
|
|
|
325
351
|
assert.equal(failed.$('recruitFailed').textContent, 'This list could not be read. Reload the page to try again.');
|
|
326
352
|
});
|
|
327
353
|
|
|
354
|
+
// ── 2a. the builder of the week (task 1002981; spec D5) ──────────────────────
|
|
355
|
+
// The server decides each week and records it; the page draws what it sent, the same for
|
|
356
|
+
// every viewer: one line on the board, and its own page with the crown and the weeks before.
|
|
357
|
+
|
|
358
|
+
test('the board’s crown line: the winner, their credits and the week, linking to its page; nothing on no winner or a failed read', async () => {
|
|
359
|
+
const ui = await boot();
|
|
360
|
+
const line = ui.$('boardCrown');
|
|
361
|
+
assert.equal(line.hidden, false);
|
|
362
|
+
assert.equal(line.textContent, 'Builder of the week: Ana <i>L</i>, ' + (1240).toLocaleString() + ' credits, 19–25 Oct', 'a name is text, never markup');
|
|
363
|
+
const link = kids(line)[0];
|
|
364
|
+
assert.equal(link.tag, 'a');
|
|
365
|
+
assert.equal(link.attrs.href, '/community/week');
|
|
366
|
+
assert.equal(kids(line)[2].tag, 'strong', 'the name stands out of the line');
|
|
367
|
+
const signedIn = await boot({ me: { github_login: 'someone' } });
|
|
368
|
+
assert.equal(signedIn.$('boardCrown').textContent, line.textContent, 'the same crown for every viewer');
|
|
369
|
+
for (const answer of [{ status: 200, body: { ...WEEK(), winner: null } }, { status: 500, body: {} }, { status: 429, body: {} }, { reject: true }]) {
|
|
370
|
+
const reads = READS();
|
|
371
|
+
reads['/community/builder-of-the-week'] = answer;
|
|
372
|
+
const quiet = await boot({ reads });
|
|
373
|
+
assert.equal(quiet.$('boardCrown').hidden, true, 'no winner to show, or no answer: no line');
|
|
374
|
+
assert.equal(quiet.$('boardCrown').textContent, '');
|
|
375
|
+
assert.equal(quiet.$('boardFailed').hidden, true, 'and never the board’s failure');
|
|
376
|
+
assert.equal(kids(quiet.$('boardList')).length, 2);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('the week page: the crown, then the past weeks in the server’s order with their dates; nothing about projects asked', async () => {
|
|
381
|
+
const ui = await boot({ pathname: '/community/week', me: { github_login: 'owner' } });
|
|
382
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/builder-of-the-week'], 'one read: no board, and no Recruit door here');
|
|
383
|
+
assert.equal(ui.$('weekLoading').hidden, true);
|
|
384
|
+
assert.equal(ui.$('weekFailed').hidden, true);
|
|
385
|
+
assert.equal(ui.$('weekNone').hidden, true);
|
|
386
|
+
const crown = ui.$('weekCrown');
|
|
387
|
+
assert.equal(crown.hidden, false);
|
|
388
|
+
assert.equal(find(crown, 'crownWhen').textContent, '19–25 Oct 2026');
|
|
389
|
+
assert.equal(find(crown, 'crownName').textContent, 'Ana <i>L</i>');
|
|
390
|
+
assert.equal(find(crown, 'mineRow__host').textContent, '@ana');
|
|
391
|
+
assert.equal(find(crown, 'connAv').src, 'https://avatars.example/ana');
|
|
392
|
+
assert.equal(find(crown, 'crownNums').textContent, (1240).toLocaleString() + 'credits1work shipped', 'one work, singular');
|
|
393
|
+
assert.equal(ui.$('pastH').hidden, false);
|
|
394
|
+
const rows = kids(ui.$('weekPast'));
|
|
395
|
+
assert.deepEqual(rows.map((r) => find(r, 'commPlace').textContent), ['12–18 Oct', '28 Sep – 4 Oct', '29 Dec – 4 Jan'], 'newest first, as sent; a week across a month says both');
|
|
396
|
+
assert.equal(find(rows[0], 'mineRow__name').textContent, 'bo', 'no display name: the handle stands in');
|
|
397
|
+
assert.equal(find(rows[0], 'connAv').tag, 'span', 'an avatar that is not http(s) is never loaded');
|
|
398
|
+
assert.equal(find(rows[0], 'mineRow__hint').textContent, '2 works shipped');
|
|
399
|
+
assert.equal(find(rows[0], 'commNum').textContent, '300');
|
|
400
|
+
assert.equal(find(rows[0], 'commUnit').textContent, 'credits');
|
|
401
|
+
const out = await boot({ pathname: '/community/week' });
|
|
402
|
+
assert.equal(out.$('weekCrown').textContent, crown.textContent, 'the same page signed out');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('the week page with no one to show, with no past weeks, and when the read fails', async () => {
|
|
406
|
+
const reads = READS();
|
|
407
|
+
reads['/community/builder-of-the-week'] = { status: 200, body: { week: { start: '2026-12-28', end: '2027-01-04' }, winner: null, past: [] } };
|
|
408
|
+
const none = await boot({ pathname: '/community/week', reads });
|
|
409
|
+
assert.equal(none.$('weekCrown').hidden, true);
|
|
410
|
+
assert.equal(none.$('weekNone').hidden, false);
|
|
411
|
+
assert.equal(none.$('weekNone').textContent, 'No builder of the week for 28 Dec 2026 – 3 Jan 2027.', 'a hidden winner and an empty week read the same (spec D8)');
|
|
412
|
+
assert.equal(none.$('pastH').hidden, true, 'no past weeks: no heading over nothing');
|
|
413
|
+
reads['/community/builder-of-the-week'] = { status: 200, body: { week: { start: 'soon' }, winner: null, past: [] } };
|
|
414
|
+
assert.equal((await boot({ pathname: '/community/week', reads })).$('weekNone').textContent, 'No builder of the week yet.', 'a date it cannot read is never drawn');
|
|
415
|
+
reads['/community/builder-of-the-week'] = { status: 500, body: {} };
|
|
416
|
+
const failed = await boot({ pathname: '/community/week', reads });
|
|
417
|
+
assert.equal(failed.$('weekFailed').hidden, false);
|
|
418
|
+
assert.equal(failed.$('weekFailed').textContent, 'The builder of the week could not be read. Reload the page to try again.');
|
|
419
|
+
assert.equal(failed.$('weekNone').hidden, true, 'a failure never reads as nobody this week');
|
|
420
|
+
reads['/community/builder-of-the-week'] = { status: 429, body: {} };
|
|
421
|
+
assert.match((await boot({ pathname: '/community/week', reads })).$('weekFailed').textContent, /^Too many requests/);
|
|
422
|
+
});
|
|
423
|
+
|
|
328
424
|
// ── 2b. the Recruit door (task 1002292; spec D6, owner 2026-10-27) ───────────
|
|
329
425
|
// A project OWNER gets a Recruit link on each row: the hub cannot read a project's
|
|
330
426
|
// `project.recruit` (ADR 0335 D1), so GET /my-projects' `owned` stands in. The link is the
|
|
@@ -342,7 +438,7 @@ test('recruit: an owner gets a Recruit link on every row but their own, opening
|
|
|
342
438
|
const reads = READS();
|
|
343
439
|
reads['/my-projects'] = OWNED(['mercury', 'https://mercury.example/x?y=1', 'Mercury']);
|
|
344
440
|
const ui = await boot({ me: { github_login: 'BO' }, reads });
|
|
345
|
-
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects']);
|
|
441
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week', '/api/bongos/my-projects']);
|
|
346
442
|
const [ana, bo] = kids(ui.$('boardList'));
|
|
347
443
|
const a = recruitOf(ana);
|
|
348
444
|
assert.equal(a.tag, 'a');
|
|
@@ -380,7 +476,7 @@ test('recruit: none for a builder who owns no project, signed out, on a failed r
|
|
|
380
476
|
assert.equal(member.$('boardFor').hidden, true);
|
|
381
477
|
assert.doesNotMatch(member.$('boardList').className, /canRecruit/);
|
|
382
478
|
const out = await boot({ reads });
|
|
383
|
-
assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all'], 'signed out, nobody is asked about projects');
|
|
479
|
+
assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week'], 'signed out, nobody is asked about projects');
|
|
384
480
|
for (const answer of [{ status: 500, body: {} }, { status: 401, body: {} }, { reject: true }, OWNED(['evil', 'javascript:alert(1)', 'Evil'], ['none', null, 'None'])]) {
|
|
385
481
|
reads['/my-projects'] = answer;
|
|
386
482
|
const ui = await boot({ me: { github_login: 'someone' }, reads });
|
|
@@ -440,6 +536,7 @@ const HUB_ONLY = ['account.html', 'connections.html', 'invitations.html', 'guild
|
|
|
440
536
|
const wrap = (apex, s) => (apex ? `<!--apex-only:start-->${s}<!--apex-only:end-->` : s);
|
|
441
537
|
const ITEMS = [
|
|
442
538
|
['/community', 'Leaderboard', 'who earned the most, across every project'],
|
|
539
|
+
['/community/week', 'Builder of the week', 'the top earner of each week, and how it is chosen'],
|
|
443
540
|
['/community/find', 'Find a builder', 'anyone building here, by name or role'],
|
|
444
541
|
['/community/recruiters', 'Open to recruiters', 'who is open to recruiting, for project owners'],
|
|
445
542
|
];
|
|
@@ -460,9 +557,9 @@ test('every page that wears the hub’s bar carries the Community menu before Pr
|
|
|
460
557
|
const at = html.indexOf(apex ? `<!--apex-only:start-->${trig}` : trig);
|
|
461
558
|
assert.ok(at > 0, `${f}: the Community trigger`);
|
|
462
559
|
assert.ok(html.indexOf('id="projTrig"') > at, `${f}: Community comes before Projects`);
|
|
463
|
-
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its
|
|
560
|
+
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its four sub-pages`);
|
|
464
561
|
assert.ok(html.indexOf('id="menuCommunity"') < html.indexOf('id="menuProjects"'), `${f}: the bands in the bar’s order`);
|
|
465
|
-
assert.ok(html.includes(wrap(apex, fold)), `${f}: the
|
|
562
|
+
assert.ok(html.includes(wrap(apex, fold)), `${f}: the four folded into the Projects band for phones`);
|
|
466
563
|
if (apex) assert.doesNotMatch(stripApexOnly(html, false).split('<body>')[1], /\/community|commTrig|menuCommunity|connSuggest/, `${f}: a child has no community pages, so no trace of the menu`);
|
|
467
564
|
}
|
|
468
565
|
});
|
|
@@ -520,7 +617,7 @@ function get(pathname, host) {
|
|
|
520
617
|
});
|
|
521
618
|
}
|
|
522
619
|
|
|
523
|
-
test('GET /community, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
|
|
620
|
+
test('GET /community, /community/week, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
|
|
524
621
|
modules.setModuleOverrides({ 'platform-identity': false });
|
|
525
622
|
for (const [p] of SUBPAGES) assert.equal((await get(p, 'game.test.local')).status, 404, `${p}: a child instance has none of the routes these pages read`);
|
|
526
623
|
modules.setModuleOverrides({ 'platform-identity': true });
|
|
@@ -533,7 +630,7 @@ test('GET /community, /community/find and /community/recruiters: one stamped pag
|
|
|
533
630
|
bodies.push(hub.body);
|
|
534
631
|
}
|
|
535
632
|
assert.ok(bodies.every((b) => b === bodies[0]), 'byte-identical for every sub-page: the script picks the view');
|
|
536
|
-
assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the
|
|
633
|
+
assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the four sub-pages');
|
|
537
634
|
for (const host of ['builders.test.local', 'status.test.local']) {
|
|
538
635
|
assert.doesNotMatch((await get('/community', host)).body, /— Community<\/title>/, `${host} must not serve /community`);
|
|
539
636
|
}
|
|
@@ -546,15 +643,18 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
|
|
|
546
643
|
const states = json('community.states.json');
|
|
547
644
|
assert.equal(states.page, '/community');
|
|
548
645
|
assert.deepEqual(states.modes, ['dark']);
|
|
549
|
-
for (const s of ['out', 'out-30d', 'out-artists', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in']) assert.ok(states.states[s], `kit state ${s}`);
|
|
646
|
+
for (const s of ['out', 'out-30d', 'out-artists', 'out-week', 'in-week', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in']) assert.ok(states.states[s], `kit state ${s}`);
|
|
550
647
|
assert.equal(states.states['out-find'].url, '/community/find');
|
|
648
|
+
assert.equal(states.states['out-week'].url, '/community/week');
|
|
649
|
+
assert.ok(states.states.out.expect.visible.includes('#boardCrown'), 'the board carries the crown line');
|
|
551
650
|
assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
|
|
552
651
|
assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
|
|
553
|
-
for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter'], ['community-owner.states.json', 'my-projects', 'my-projects-owner'], ['community-owner-two.states.json', 'my-projects', 'my-projects-owner-two']]) {
|
|
652
|
+
for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter'], ['community-owner.states.json', 'my-projects', 'my-projects-owner'], ['community-owner-two.states.json', 'my-projects', 'my-projects-owner-two'], ['community-week-none.states.json', 'community-week', 'community-week-none']]) {
|
|
554
653
|
assert.equal(json(file).stub.fixtures[name], fixture);
|
|
555
654
|
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
|
|
556
655
|
}
|
|
557
656
|
assert.equal(json('community-recruiter.states.json').states['in-recruiter'].url, '/community/recruiters');
|
|
657
|
+
assert.equal(json('community-week-none.states.json').states['out-week-none'].url, '/community/week');
|
|
558
658
|
// the Recruit door (task 1002292): an owner of one project on the board and the search, and of two with the choice
|
|
559
659
|
assert.ok(json('community-owner.states.json').states['in-owner'].expect.count.some(([sel, n]) => sel === '#boardList a.commRecruit' && n === 6));
|
|
560
660
|
assert.equal(json('community-owner.states.json').states['in-owner-find'].url, '/community/find');
|
|
@@ -585,9 +685,14 @@ async function call(origin, p) {
|
|
|
585
685
|
return { status: res.status, body: await res.json() };
|
|
586
686
|
}
|
|
587
687
|
|
|
588
|
-
test('the kit stub answers the board, the search and the recruiting door the way the routes do', async () => {
|
|
688
|
+
test('the kit stub answers the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
|
|
589
689
|
const s = await stub();
|
|
590
690
|
try {
|
|
691
|
+
const week = await call(s.origin, '/community/builder-of-the-week');
|
|
692
|
+
assert.deepEqual(Object.keys(week.body).sort(), ['past', 'week', 'winner'], 'the route’s three keys');
|
|
693
|
+
assert.ok(week.body.winner && week.body.past.length, 'a crown and the weeks before it');
|
|
694
|
+
assert.ok([week.body.winner, ...week.body.past].every((r) => !('github_id' in r) && Object.keys(r).sort().join() === 'avatar_url,credits,display_name,github_login,handle,tasks_shipped,week_start'), 'the module’s projection, no github_id');
|
|
695
|
+
assert.deepEqual(week.body.past.map((p) => p.week_start), [...week.body.past.map((p) => p.week_start)].sort().reverse(), 'newest first');
|
|
591
696
|
const all = await call(s.origin, '/community/leaderboard');
|
|
592
697
|
assert.equal(all.body.window, 'all');
|
|
593
698
|
assert.deepEqual(all.body.results.map((r) => r.place), all.body.results.map((_, i) => i + 1));
|
|
@@ -606,6 +711,12 @@ test('the kit stub answers the board, the search and the recruiting door the way
|
|
|
606
711
|
try {
|
|
607
712
|
assert.equal((await call(member.origin, '/scouting')).status, 403, 'signed in, most builders are refused');
|
|
608
713
|
} finally { member.stop(); }
|
|
714
|
+
const none = await stub({ FIXTURE_ALIAS: JSON.stringify({ 'community-week': 'community-week-none' }) });
|
|
715
|
+
try {
|
|
716
|
+
const r = await call(none.origin, '/community/builder-of-the-week');
|
|
717
|
+
assert.equal(r.body.winner, null);
|
|
718
|
+
assert.deepEqual(r.body.past, []);
|
|
719
|
+
} finally { none.stop(); }
|
|
609
720
|
const owner = await stub({ AUTH: '1', FIXTURE_ALIAS: JSON.stringify({ scouting: 'scouting-recruiter' }) });
|
|
610
721
|
try {
|
|
611
722
|
const r = await call(owner.origin, '/scouting');
|
|
@@ -193,8 +193,8 @@ for (const p of PAGES) {
|
|
|
193
193
|
assert.deepEqual(bandLinks(bandOf(html, 'menuProjects')), [
|
|
194
194
|
['Sky', '/projects?view=map'], ['Table', '/projects?view=map&sub=table'],
|
|
195
195
|
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
196
|
-
// the Community menu's
|
|
197
|
-
['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
196
|
+
// the Community menu's four sub-pages, folded into the band under 480px (tasks 1003363, 1002981); apex-only
|
|
197
|
+
['Leaderboard', '/community'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
198
198
|
['Builders you may know', '/connections#connSuggest'],
|
|
199
199
|
], 'this page has no views of its own, so every item names /projects in full');
|
|
200
200
|
assert.deepEqual(bandLinks(bandOf(stripApexOnly(html, false), 'menuProjects')).map((l) => l[0]), ['Sky', 'Table', 'Your projects', 'New project'], 'a child has no /community');
|
package/tests/privacy_matrix.mjs
CHANGED
|
@@ -150,6 +150,10 @@ function identitySentinels(acct) {
|
|
|
150
150
|
// 'listed' | 'name-only' (listed, disciplines null) | 'absent'
|
|
151
151
|
// board GET /community/leaderboard Anon · Builder — the cross-project leaderboard
|
|
152
152
|
// 'listed' | 'absent'
|
|
153
|
+
// week GET /community/builder-of-the-week Anon · Builder · Recruiter — the crown and
|
|
154
|
+
// the past weeks (task 1002981, spec D5): a row IS stats, so the board's rule.
|
|
155
|
+
// Every fixture account has won a recorded week, so the read's own predicate
|
|
156
|
+
// decides: 'listed' | 'absent'
|
|
153
157
|
// directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
|
|
154
158
|
// sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
|
|
155
159
|
// self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
|
|
@@ -166,13 +170,13 @@ function identitySentinels(acct) {
|
|
|
166
170
|
// people the guild page already shows them — shown_publicly AND existence;
|
|
167
171
|
// hide_stats never hides membership, and private or provisional is never listed
|
|
168
172
|
const MATRIX = {
|
|
169
|
-
PROV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
|
|
170
|
-
PUB: { profile: 'full', search: 'listed', board: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
171
|
-
SHY: { profile: 'identity', search: 'name-only', board: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
172
|
-
PRIV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
173
|
-
PRIVD: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
174
|
-
PRIVDH: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
175
|
-
OPTOUT: { profile: 'full', search: 'listed', board: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
173
|
+
PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
|
|
174
|
+
PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
175
|
+
SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
176
|
+
PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
177
|
+
PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
178
|
+
PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
179
|
+
OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
176
180
|
};
|
|
177
181
|
|
|
178
182
|
// ===========================================================================
|
|
@@ -280,6 +284,19 @@ function guildRosterFilter(sql, params) {
|
|
|
280
284
|
// origin the hall links point at.
|
|
281
285
|
const MX_CLIENT = { client_id: 'mx-proj', secret_hash: '', name: 'MX Project', redirect_uris: [], origin: 'https://mx-proj.example', owner_github_id: null, status: 'active' };
|
|
282
286
|
|
|
287
|
+
// THE WEEKS each fixture account won (task 1002981): PUB the week now crowned, every
|
|
288
|
+
// other account a week before it, so the crown and the past list both carry a cell.
|
|
289
|
+
// Lazy: the module may only load after the pool is pinned below, through the routes.
|
|
290
|
+
let weeksWon = null;
|
|
291
|
+
function weekOf(key) {
|
|
292
|
+
if (!weeksWon) {
|
|
293
|
+
const { crownedWeek } = require('../modules/platform-identity/builder-of-the-week.js');
|
|
294
|
+
weeksWon = Object.fromEntries(['PUB', ...KEYS.filter((k) => k !== 'PUB')].map((k, i) =>
|
|
295
|
+
[k, new Date(crownedWeek().start.getTime() - i * 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)]));
|
|
296
|
+
}
|
|
297
|
+
return weeksWon[key];
|
|
298
|
+
}
|
|
299
|
+
|
|
283
300
|
const pool = {
|
|
284
301
|
query: async (text, params = []) => {
|
|
285
302
|
const sql = String(text).replace(/\s+/g, ' ').trim();
|
|
@@ -338,6 +355,21 @@ const pool = {
|
|
|
338
355
|
// ... and the weekly totals: never computed on a read (guild-totals.js); none stored here.
|
|
339
356
|
if (/FROM platform_identity_guild_totals WHERE guild_id = \$1$/.test(sql)) return { rows: [], rowCount: 0 };
|
|
340
357
|
|
|
358
|
+
// BUILDER OF THE WEEK (task 1002981): every week is already recorded (no crowning
|
|
359
|
+
// here), one per fixture account, PUB the current one — so the read's own predicate
|
|
360
|
+
// decides who is shown. The projection is pinned: a new column is a new disclosure.
|
|
361
|
+
if (/^SELECT 1 FROM platform_identity_weekly_features WHERE week_start = \$1::date$/.test(sql)) return { rows: [{ '?column?': 1 }], rowCount: 1 };
|
|
362
|
+
if (/FROM platform_identity_weekly_features f JOIN platform_identity_accounts pa/.test(sql)) {
|
|
363
|
+
const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
|
|
364
|
+
if (cols !== "to_char(f.week_start, 'YYYY-MM-DD') AS week_start, f.credits_delta, f.ships_delta, pa.handle, pa.github_login, pa.display_name, pa.avatar_url") throw new Error(`fake pool: the builder-of-the-week read selects something new — ${cols}`);
|
|
365
|
+
const where = lift(sql, /WHERE f\.week_start <= \$1::date AND (.+?) ORDER BY/, 'builder of the week');
|
|
366
|
+
const rows = ACCOUNTS.filter(accountFilter(where, [])).map((a) => ({
|
|
367
|
+
week_start: weekOf(a.key), credits_delta: String(ROLLUP[a.github_id].credits), ships_delta: String(ROLLUP[a.github_id].works_shipped),
|
|
368
|
+
handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url,
|
|
369
|
+
})).sort((x, y) => (x.week_start < y.week_start ? 1 : -1)).slice(0, params[1]);
|
|
370
|
+
return { rows, rowCount: rows.length };
|
|
371
|
+
}
|
|
372
|
+
|
|
341
373
|
// THE LEADERBOARD's totals: the rollup floor sits on the act CTE's join.
|
|
342
374
|
if (/platform_identity_activity_snapshots/.test(sql)) {
|
|
343
375
|
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'leaderboard totals');
|
|
@@ -764,6 +796,19 @@ test('the cross-project leaderboard — only shown, public, active accounts, the
|
|
|
764
796
|
}
|
|
765
797
|
});
|
|
766
798
|
|
|
799
|
+
test('the builder of the week — only shown, public, active winners, on the crown and the past weeks, the same for every viewer', async () => {
|
|
800
|
+
const seen = [];
|
|
801
|
+
for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/builder-of-the-week'));
|
|
802
|
+
for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got a different builder of the week`);
|
|
803
|
+
const { winner, past } = seen[0].json;
|
|
804
|
+
assert.equal(winner && winner.handle, ACCT.PUB.handle, 'control: a shown winner is crowned, or absence below is vacuous');
|
|
805
|
+
const listed = new Set([winner, ...past].filter(Boolean).map((x) => x.handle));
|
|
806
|
+
for (const k of KEYS) {
|
|
807
|
+
assert.equal(listed.has(ACCT[k].handle), MATRIX[k].week === 'listed', `${k}: expected ${MATRIX[k].week}`);
|
|
808
|
+
}
|
|
809
|
+
assert.doesNotMatch(seen[0].text, /github_id/, 'github_id never crosses');
|
|
810
|
+
});
|
|
811
|
+
|
|
767
812
|
test('the recruiting directory — the recruiter sees the full and name-only tiers; Anon and Builder are refused', async () => {
|
|
768
813
|
const r = await get(RECRUITER, '/api/bongos/scouting');
|
|
769
814
|
assert.equal(r.status, 200);
|
|
@@ -806,7 +851,7 @@ function disclosed(r) {
|
|
|
806
851
|
}
|
|
807
852
|
|
|
808
853
|
test('the walk covered every surface for every row (so the claims below are about something)', () => {
|
|
809
|
-
for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
|
|
854
|
+
for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/community/builder-of-the-week', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
|
|
810
855
|
assert.ok(SEEN.some((r) => r.path.includes(p) && r.status === 200), `no 200 from ${p} — the sweep below would be vacuous for it`);
|
|
811
856
|
}
|
|
812
857
|
assert.ok(SEEN.length >= KEYS.length * 6, `only ${SEEN.length} responses recorded`);
|