@bongos/core 1.23.18 → 1.23.20
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 +134 -49
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +43 -1
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +992 -935
- package/docs/copy-registry.json +1897 -1313
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +12 -2
- package/docs/page-readings.json +616 -576
- package/modules/platform-identity/builder-of-the-week.js +61 -24
- package/modules/platform-identity/community-crowns.js +176 -0
- package/modules/platform-identity/community-leaderboard.js +4 -1
- package/modules/platform-identity/community-movers.js +152 -0
- package/modules/platform-identity/migrations/platform_identity_033_period_features.sql +43 -0
- package/modules/platform-identity/routes/community-leaderboard.js +45 -3
- package/modules/public-landing/public/account.html +3 -2
- package/modules/public-landing/public/community-crowns-first.states.json +15 -0
- package/modules/public-landing/public/community-crowns-new-year.states.json +15 -0
- package/modules/public-landing/public/community-crowns-none.states.json +15 -0
- package/modules/public-landing/public/community-empty.states.json +1 -1
- package/modules/public-landing/public/community-movers-none.states.json +15 -0
- package/modules/public-landing/public/community-owner-two.states.json +2 -2
- package/modules/public-landing/public/community-owner.states.json +1 -1
- package/modules/public-landing/public/community-week-none.states.json +1 -1
- package/modules/public-landing/public/community.html +188 -22
- package/modules/public-landing/public/community.probes.json +20 -3
- package/modules/public-landing/public/community.states.json +8 -6
- package/modules/public-landing/public/connections.html +3 -2
- package/modules/public-landing/public/contact.html +3 -2
- package/modules/public-landing/public/guild.html +3 -2
- package/modules/public-landing/public/index.html +3 -2
- package/modules/public-landing/public/invitations.html +3 -2
- package/modules/public-landing/public/privacy.html +3 -2
- package/modules/public-landing/public/projects.html +3 -2
- package/modules/public-landing/public/terms.html +3 -2
- package/modules/ui-design/kit/fixtures/community-crowns-first.json +6 -0
- package/modules/ui-design/kit/fixtures/community-crowns-new-year.json +6 -0
- package/modules/ui-design/kit/fixtures/community-crowns-none.json +6 -0
- package/modules/ui-design/kit/fixtures/community-crowns.json +6 -0
- package/modules/ui-design/kit/fixtures/community-movers-none.json +6 -0
- package/modules/ui-design/kit/fixtures/community-movers.json +12 -0
- package/modules/ui-design/kit/serve.js +22 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/run-unit-tests.js +8 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +4 -3
- package/tests/community_crowns.mjs +369 -0
- package/tests/community_crowns_db.mjs +164 -0
- package/tests/community_movers.mjs +280 -0
- package/tests/community_movers_db.mjs +147 -0
- package/tests/hub_account_page.mjs +2 -2
- package/tests/hub_community_page.mjs +213 -29
- package/tests/landing_legal_pages.mjs +2 -2
- package/tests/privacy_matrix.mjs +107 -9
|
@@ -1,11 +1,12 @@
|
|
|
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
|
|
6
|
-
// /community/
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// One file served as five sub-pages of the bar's Community menu (owner, 2026-10-27):
|
|
5
|
+
// /community the community home (task 1004902: the builders of the week, month and year,
|
|
6
|
+
// and a door to each page), /community/leaderboard the leaderboard, /community/week the
|
|
7
|
+
// builder of the week (task 1002981), /community/find the builder search,
|
|
8
|
+
// /community/recruiters the name-only list of hidden builders open to recruiters. They
|
|
9
|
+
// render existing platform-identity reads and add no rule of their own. What this file pins:
|
|
9
10
|
// • the markup: the hub's bar with the Community menu as the current section, no top
|
|
10
11
|
// block, noindex, dark only, the routes' own windows and query limits
|
|
11
12
|
// • viewing is ungated: the leaderboard and the search read the same routes for every
|
|
@@ -15,11 +16,13 @@
|
|
|
15
16
|
// dropdown and the crafts, the empty, failed and rate-limited board; the search and
|
|
16
17
|
// its refusals; the recruiter list as a door signed out, a refusal on a 403, and rows
|
|
17
18
|
// 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
|
|
19
|
+
// of the week as one line on the board and as its own page, drawn as the server sent it;
|
|
20
|
+
// the home's three crowns, its empty and failed states, and its doors; the row under
|
|
21
|
+
// them (task 1002983), the rising builder and the projects on the move, from its own read
|
|
19
22
|
// • the Community menu on every page that wears the hub's bar (apex-only where the page
|
|
20
23
|
// also serves a child), folded into the Projects band on phones
|
|
21
|
-
// • the routes: the
|
|
22
|
-
// • the kit: the states files, and the stub answering as the
|
|
24
|
+
// • the routes: the five paths served on the hub's apex only
|
|
25
|
+
// • the kit: the states files, and the stub answering as the six reads do
|
|
23
26
|
//
|
|
24
27
|
// DB-free. Run: node --test tests/hub_community_page.mjs
|
|
25
28
|
|
|
@@ -46,8 +49,10 @@ const STYLE = PAGE.split('<style>')[1].split('</style>')[0].replace(/\/\*[\s\S]*
|
|
|
46
49
|
const MAIN = PAGE.split('<main class="acct" id="comm">')[1].split('</main>')[0];
|
|
47
50
|
const BOARD_JS = fs.readFileSync(path.join(PI, 'community-leaderboard.js'), 'utf8');
|
|
48
51
|
const SEARCH_JS = fs.readFileSync(path.join(PI, 'builder-search.js'), 'utf8');
|
|
49
|
-
const SUBPAGES = [['/community', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
52
|
+
const SUBPAGES = [['/community', 'Community home'], ['/community/leaderboard', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
50
53
|
const WEEK_JS = fs.readFileSync(path.join(PI, 'builder-of-the-week.js'), 'utf8');
|
|
54
|
+
const CROWNS_JS = fs.readFileSync(path.join(PI, 'community-crowns.js'), 'utf8');
|
|
55
|
+
const MOVERS_JS = fs.readFileSync(path.join(PI, 'community-movers.js'), 'utf8');
|
|
51
56
|
|
|
52
57
|
// ── 1. the markup ────────────────────────────────────────────────────────────
|
|
53
58
|
|
|
@@ -73,9 +78,9 @@ test('the bar and bands are /connections’ but for the sign-in return and the c
|
|
|
73
78
|
assert.equal(bar(PAGE), bar(read('connections.html')));
|
|
74
79
|
});
|
|
75
80
|
|
|
76
|
-
test('no top block: one visually hidden h1, then one section per sub-page, only the
|
|
81
|
+
test('no top block: one visually hidden h1, then one section per sub-page, only the home shown in the markup', () => {
|
|
77
82
|
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)');
|
|
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));
|
|
83
|
+
const order = ['<section id="home" data-view="home" aria-labelledby="homeH">', '<section id="board" data-view="leaderboard" aria-labelledby="boardH" hidden>', '<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));
|
|
79
84
|
assert.ok(order.every((i) => i > 0), 'every sub-page is real markup the script only shows');
|
|
80
85
|
assert.deepEqual([...order].sort((a, b) => a - b), order);
|
|
81
86
|
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');
|
|
@@ -100,10 +105,44 @@ test('the builder of the week states its rule in full, and the rule is the modul
|
|
|
100
105
|
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
106
|
assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
|
|
102
107
|
assert.match(WEEK_JS, /rollupVisibleSql\('pa'\)/);
|
|
103
|
-
assert.match(rule, /Each week is decided once, the first time
|
|
108
|
+
assert.match(rule, /Each week is decided once, the first time a community page asks for it after it ends, and it does not change after that\./);
|
|
104
109
|
assert.match(WEEK_JS, /ON CONFLICT \(week_start\) DO NOTHING/);
|
|
105
110
|
});
|
|
106
111
|
|
|
112
|
+
test('the home states the month’s and year’s rule, and it is the module’s; its doors are the other pages (task 1004902)', () => {
|
|
113
|
+
const home = MAIN.split('<section id="home"')[1].split('</section>')[0];
|
|
114
|
+
const rule = home.split('<p class="commNote" id="homeRule">')[1].split('</p>')[0];
|
|
115
|
+
assert.match(rule, /the most \{\{currency\}\} across every project in that time, among the builders who shipped at least one work in it\./, 'the week’s formula, which the month and the year reuse');
|
|
116
|
+
assert.match(CROWNS_JS, /require\('\.\/builder-of-the-week'\)/);
|
|
117
|
+
assert.match(CROWNS_JS, /computeWinner\(start, end, pool, opts\)/, 'the month and the crowned year are the week’s statement');
|
|
118
|
+
assert.match(rule, /A tie goes to whoever shipped more works, then to whoever earned more karma\./);
|
|
119
|
+
assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
|
|
120
|
+
assert.match(CROWNS_JS, /rollupVisibleSql\('pa'\)/);
|
|
121
|
+
assert.match(rule, /The week and the month are decided once, when they end; the year shows the leader so far, and is decided on 1 January\./);
|
|
122
|
+
assert.match(CROWNS_JS, /ON CONFLICT \(period, period_start\) DO NOTHING/);
|
|
123
|
+
assert.match(CROWNS_JS, /computeWinner\(start, now, pool, \{ fromFirstSnapshot: true \}\)/, 'the year so far is live');
|
|
124
|
+
for (const id of ['homeFailed', 'homeCrowns', 'crownYearLast', 'homeMovers', 'moversRule', 'moveNone']) assert.match(home, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
125
|
+
assert.ok(home.indexOf('id="homeMovers"') > home.indexOf('id="homeRule"') && home.indexOf('id="homeMovers"') < home.indexOf('id="doorsH"'), 'the row under the crowns and their rule, above the doors (owner decision 5, task 1004902)');
|
|
126
|
+
const doors = [...home.split('<ul class="homeDoors" aria-labelledby="doorsH">')[1].split('</ul>')[0].matchAll(/<li( class="knowCell" hidden)?><a class="btn ghost" href="([^"]+)">([^<]+)<\/a><\/li>/g)];
|
|
127
|
+
assert.deepEqual(doors.map((m) => [m[2], m[3], !!m[1]]), [
|
|
128
|
+
['/community/leaderboard', 'Leaderboard', false], ['/community/week', 'Builder of the week', false], ['/community/find', 'Find a builder', false],
|
|
129
|
+
['/community/recruiters', 'Open to recruiters', false], ['/connections#connSuggest', 'Builders you may know', true],
|
|
130
|
+
], 'one door per page, the menu’s order; Builders you may know signed in only, like its menu item');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('the movers’ rule is the module’s: the base cap, the shipped-work floor, the projects’ order and who is counted (task 1002983)', () => {
|
|
134
|
+
const rule = MAIN.split('<p class="commNote" id="moversRule" hidden>')[1].split('</p>')[0];
|
|
135
|
+
const cap = MOVERS_JS.match(/const RISING_BASE_MAX = (\d+);/)[1];
|
|
136
|
+
assert.match(rule, new RegExp(`the most \\{\\{currency\\}\\} in the last 30 days, among the builders who had earned under ${cap} \\{\\{currency\\}\\} before them and shipped at least one work in them\\.`));
|
|
137
|
+
assert.match(MOVERS_JS, /const WINDOW = '30d';/);
|
|
138
|
+
assert.match(MOVERS_JS, /s\.base < RISING_BASE_MAX && s\.tasks_shipped >= 1/, 'the cap and the floor the page states are the ones applied');
|
|
139
|
+
assert.match(rule, /Projects on the move are the public projects that shipped the most works in the last 30 days, then earned the most \{\{currency\}\}\./);
|
|
140
|
+
assert.match(MOVERS_JS, /\(b\.tasks_shipped - a\.tasks_shipped\) \|\| \(b\.credits - a\.credits\)/);
|
|
141
|
+
assert.match(MOVERS_JS, /onMapSql\('p'\)\} AND p\.visibility = 'public'/, '“public projects”: on the map with the Public door');
|
|
142
|
+
assert.match(rule, /Builders who hide their numbers or keep their account private are not counted in either\./);
|
|
143
|
+
assert.match(MOVERS_JS, /eligibleIdentities/);
|
|
144
|
+
});
|
|
145
|
+
|
|
107
146
|
test('the window dropdown and the query limits are the routes’ own (boundary-forced copies)', () => {
|
|
108
147
|
const days = BOARD_JS.match(/const WINDOW_DAYS = Object\.freeze\(\{([^}]*)\}\);/);
|
|
109
148
|
const windows = [...days[1].matchAll(/'?(\w+)'?:/g)].map((m) => m[1]);
|
|
@@ -121,12 +160,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
|
|
|
121
160
|
assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
|
|
122
161
|
});
|
|
123
162
|
|
|
124
|
-
test('the script builds no markup from strings, reads only the
|
|
163
|
+
test('the script builds no markup from strings, reads only the seven routes, and never re-ranks or reads the ranked tier', () => {
|
|
125
164
|
assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
|
|
126
|
-
// every path literal in the code: the API base, the
|
|
165
|
+
// every path literal in the code: the API base, the seven reads, the five sub-pages, the
|
|
127
166
|
// sign-in door and the hall's invite box the Recruit door opens (task 1002292)
|
|
128
167
|
const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
|
|
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']);
|
|
168
|
+
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/crowns', '/community/find', '/community/leaderboard', '/community/movers', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
|
|
130
169
|
assert.doesNotMatch(CODE, /method:/, 'it only reads');
|
|
131
170
|
assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
|
|
132
171
|
assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
|
|
@@ -172,7 +211,20 @@ const WEEK = () => ({ week: { start: '2026-10-19', end: '2026-10-26' },
|
|
|
172
211
|
past: [{ week_start: '2026-10-12', handle: 'bo', github_login: 'bo', display_name: null, avatar_url: 'data:x', credits: 300, tasks_shipped: 2 },
|
|
173
212
|
{ week_start: '2026-09-28', handle: 'cy', github_login: 'cy', display_name: 'Cy', avatar_url: null, credits: 90, tasks_shipped: 1 },
|
|
174
213
|
{ week_start: '2025-12-29', handle: 'di', github_login: 'di', display_name: 'Di', avatar_url: null, credits: 10, tasks_shipped: 1 }] });
|
|
214
|
+
const WINNER = (handle, credits, ships, extra = {}) => ({ handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: ships, ...extra });
|
|
215
|
+
const CROWNS = () => ({
|
|
216
|
+
week: { start: '2026-10-26', end: '2026-11-02', winner: WINNER('ana', 1240, 4, { display_name: 'Ana <i>L</i>' }) },
|
|
217
|
+
month: { start: '2026-10-01', end: '2026-11-01', first_crown: null, winner: WINNER('bo', 6120, 1, { display_name: null, avatar_url: 'javascript:alert(1)' }) },
|
|
218
|
+
year: { start: '2026-01-01', since: '2026-09-27', last: null, winner: WINNER('cy', 9870, 31) },
|
|
219
|
+
});
|
|
220
|
+
const MOVERS = () => ({
|
|
221
|
+
window: { start: '2026-09-28T09:30:00.000Z', end: '2026-10-28T09:30:00.000Z' },
|
|
222
|
+
rising: WINNER('kofi', 640, 5, { display_name: 'Kofi <b>M</b>' }),
|
|
223
|
+
projects: [{ name: 'Tide <i>pool</i>', origin: 'https://tidepool.example', tasks_shipped: 23, credits: 1840 }, { name: 'Quill', origin: 'javascript:alert(1)', tasks_shipped: 1, credits: 45 }],
|
|
224
|
+
});
|
|
175
225
|
const READS = () => ({
|
|
226
|
+
'/community/crowns': { status: 200, body: CROWNS() },
|
|
227
|
+
'/community/movers': { status: 200, body: MOVERS() },
|
|
176
228
|
'/community/builder-of-the-week': { status: 200, body: WEEK() },
|
|
177
229
|
'/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 })] } },
|
|
178
230
|
'/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
|
|
@@ -182,7 +234,7 @@ const READS = () => ({
|
|
|
182
234
|
'/scouting': { status: 403, body: { error: { code: 'forbidden' } } },
|
|
183
235
|
});
|
|
184
236
|
|
|
185
|
-
async function boot({ pathname = '/community', me = null, reads = READS() } = {}) {
|
|
237
|
+
async function boot({ pathname = '/community/leaderboard', me = null, reads = READS() } = {}) {
|
|
186
238
|
const ids = new Map();
|
|
187
239
|
for (const m of PAGE.matchAll(/<(\w+)([^>]* id="([^"]+)"[^>]*)>/g)) {
|
|
188
240
|
const attrs = attrsOf(m[2]);
|
|
@@ -224,17 +276,123 @@ async function boot({ pathname = '/community', me = null, reads = READS() } = {}
|
|
|
224
276
|
return { ids, requests, document, gets: () => requests.map((r) => r.url), tab, craft, pick, submit, current, $: (id) => ids.get(id) };
|
|
225
277
|
}
|
|
226
278
|
|
|
227
|
-
test('each path shows its one section, marks its menu items current and names itself; any other shape is the
|
|
228
|
-
for (const [pathname,
|
|
279
|
+
test('each path shows its one section, marks its menu items current and names itself; any other shape is the home', async () => {
|
|
280
|
+
for (const [pathname, item, section] of [['/community', 'Community home', 'home'], ['/community/', 'Community home', 'home'], ['/community/everyone', 'Community home', 'home'], ['/community/leaderboard/', 'Leaderboard', 'board'], ['/community/week', 'Builder of the week', 'week'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
|
|
229
281
|
const ui = await boot({ pathname });
|
|
230
|
-
assert.deepEqual(['board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
|
|
231
|
-
const href = SUBPAGES.find(([, t]) => t ===
|
|
282
|
+
assert.deepEqual(['home', 'board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
|
|
283
|
+
const href = SUBPAGES.find(([, t]) => t === item)[0];
|
|
232
284
|
assert.deepEqual(ui.current(), [href, href], `${pathname}: its Community item and its folded twin in the Projects band`);
|
|
233
|
-
assert.equal(ui.document.title, `Cloud Bongos — Community · ${
|
|
285
|
+
assert.equal(ui.document.title, section === 'home' ? 'Cloud Bongos — Community' : `Cloud Bongos — Community · ${item}`, 'the home is the Community page itself');
|
|
234
286
|
assert.equal(ui.$('signinTop').attrs.href, `/api/bongos/auth/web/start?return=${href}&repos=1`, 'signing in comes back to this sub-page');
|
|
235
287
|
}
|
|
236
288
|
});
|
|
237
289
|
|
|
290
|
+
// ── 2c. the community home (task 1004902) ───────────────────────────────────────
|
|
291
|
+
// community-crowns.js decides and records each period; the home draws what it sent, the
|
|
292
|
+
// same for every viewer.
|
|
293
|
+
|
|
294
|
+
test('the home: one read, the three crowns with their periods, names as text, and nothing asked about projects', async () => {
|
|
295
|
+
const ui = await boot({ pathname: '/community' });
|
|
296
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/crowns', '/api/bongos/community/movers']);
|
|
297
|
+
assert.equal(ui.$('homeLoading').hidden, true);
|
|
298
|
+
assert.equal(ui.$('homeFailed').hidden, true);
|
|
299
|
+
assert.equal(ui.$('homeCrowns').hidden, false);
|
|
300
|
+
assert.equal(ui.$('crownWeekWhen').textContent, '26 Oct – 1 Nov 2026');
|
|
301
|
+
assert.equal(find(ui.$('crownWeek'), 'crownName').textContent, 'Ana <i>L</i>', 'a name is text, never markup');
|
|
302
|
+
assert.equal(find(ui.$('crownWeek'), 'crownNums').textContent, (1240).toLocaleString() + 'credits4works shipped');
|
|
303
|
+
assert.equal(ui.$('crownMonthWhen').textContent, 'October 2026');
|
|
304
|
+
assert.equal(find(ui.$('crownMonth'), 'crownName').textContent, 'bo', 'no display name: the handle stands in');
|
|
305
|
+
assert.equal(find(ui.$('crownMonth'), 'connAv').tag, 'span', 'an avatar that is not http(s) is never loaded');
|
|
306
|
+
assert.equal(find(ui.$('crownMonth'), 'crownNums').textContent, (6120).toLocaleString() + 'credits1work shipped', 'one work, singular');
|
|
307
|
+
assert.equal(ui.$('crownYearWhen').textContent, '2026 so far, since 27 Sep 2026', 'the year says when history began while that is inside it');
|
|
308
|
+
assert.equal(find(ui.$('crownYear'), 'mineRow__host').textContent, '@cy');
|
|
309
|
+
assert.equal(ui.$('crownYearLast').hidden, true, 'no year crowned yet');
|
|
310
|
+
const signedIn = await boot({ pathname: '/community', me: { github_login: 'owner' } });
|
|
311
|
+
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/crowns', '/api/bongos/community/movers'], 'signed in, the same two reads: no Recruit door here');
|
|
312
|
+
assert.equal(signedIn.$('homeCrowns').textContent, ui.$('homeCrowns').textContent, 'the same crowns for every viewer');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('the home: last year under the year so far, the first month’s date, and no one to show', async () => {
|
|
316
|
+
const reads = READS();
|
|
317
|
+
reads['/community/crowns'] = { status: 200, body: { ...CROWNS(),
|
|
318
|
+
year: { start: '2027-01-01', since: '2027-01-01', winner: WINNER('di', 310, 2), last: { start: '2026-01-01', winner: WINNER('cy', 11250, 38, { display_name: 'Cy <b>!</b>' }) } } } };
|
|
319
|
+
const next = await boot({ pathname: '/community', reads });
|
|
320
|
+
assert.equal(next.$('crownYearWhen').textContent, '2027 so far', 'history began before the year: no “since”');
|
|
321
|
+
assert.equal(next.$('crownYearLast').hidden, false);
|
|
322
|
+
assert.equal(next.$('crownYearLast').textContent, '2026: Cy <b>!</b>, ' + (11250).toLocaleString() + ' credits');
|
|
323
|
+
assert.equal(kids(next.$('crownYearLast'))[1].tag, 'strong');
|
|
324
|
+
reads['/community/crowns'] = { status: 200, body: {
|
|
325
|
+
week: { start: '2026-10-19', end: '2026-10-26', winner: null },
|
|
326
|
+
month: { start: '2026-09-01', end: '2026-10-01', first_crown: '2026-11-01', winner: null },
|
|
327
|
+
year: { start: '2026-01-01', since: '2026-09-27', winner: null, last: null } } };
|
|
328
|
+
const first = await boot({ pathname: '/community', reads });
|
|
329
|
+
const line = (id) => find(first.$(id), 'commMsg').textContent;
|
|
330
|
+
assert.equal(line('crownWeek'), 'No builder of the week for 19–25 Oct 2026.');
|
|
331
|
+
assert.equal(first.$('crownMonthWhen').textContent, 'September 2026');
|
|
332
|
+
assert.equal(line('crownMonth'), 'The first builder of the month is named on 1 Nov 2026.', 'a month before history is not “nobody”');
|
|
333
|
+
assert.equal(line('crownYear'), 'No builder of 2026 yet.');
|
|
334
|
+
assert.equal(find(first.$('homeCrowns'), 'crownName'), null);
|
|
335
|
+
reads['/community/crowns'] = { status: 200, body: { ...CROWNS(), month: { start: '2026-10-01', end: '2026-11-01', first_crown: null, winner: null } } };
|
|
336
|
+
assert.equal(find((await boot({ pathname: '/community', reads })).$('crownMonth'), 'commMsg').textContent, 'No builder of the month for October 2026.', 'a hidden winner and an empty month read the same (spec D8)');
|
|
337
|
+
reads['/community/crowns'] = { status: 200, body: { week: { start: 'soon' }, month: {}, year: { last: { start: 'x', winner: WINNER('ed', 1, 1) } } } };
|
|
338
|
+
const odd = await boot({ pathname: '/community', reads });
|
|
339
|
+
assert.deepEqual(['crownWeek', 'crownMonth', 'crownYear'].map((id) => find(odd.$(id), 'commMsg').textContent), ['No builder of the week yet.', 'No builder of the month yet.', 'No builder of the year yet.'], 'a date it cannot read is never drawn');
|
|
340
|
+
assert.equal(odd.$('crownYearLast').hidden, true);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('the home: a failed read is an alert, never three empty crowns; a rate limit says to wait', async () => {
|
|
344
|
+
const reads = READS();
|
|
345
|
+
for (const answer of [{ status: 500, body: {} }, { reject: true }]) {
|
|
346
|
+
reads['/community/crowns'] = answer;
|
|
347
|
+
const failed = await boot({ pathname: '/community', reads });
|
|
348
|
+
assert.equal(failed.$('homeFailed').hidden, false);
|
|
349
|
+
assert.equal(failed.$('homeFailed').textContent, 'The top builders could not be read. Reload the page to try again.');
|
|
350
|
+
assert.equal(failed.$('homeCrowns').hidden, true);
|
|
351
|
+
assert.equal(failed.$('homeLoading').hidden, true);
|
|
352
|
+
}
|
|
353
|
+
reads['/community/crowns'] = { status: 429, body: {} };
|
|
354
|
+
assert.match((await boot({ pathname: '/community', reads })).$('homeFailed').textContent, /^Too many requests/);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('the row under the crowns: the rising builder as a crown, the projects in the server’s order, names as text, a link only to a web address (task 1002983)', async () => {
|
|
358
|
+
const ui = await boot({ pathname: '/community' });
|
|
359
|
+
assert.equal(ui.$('homeMovers').hidden, false);
|
|
360
|
+
assert.equal(ui.$('moversRule').hidden, false);
|
|
361
|
+
assert.equal(find(ui.$('moveRising'), 'crownName').textContent, 'Kofi <b>M</b>', 'a name is text, never markup');
|
|
362
|
+
assert.equal(find(ui.$('moveRising'), 'crownNums').textContent, '640credits5works shipped');
|
|
363
|
+
const rows = kids(ui.$('moveProjects'));
|
|
364
|
+
assert.deepEqual(rows.map((r) => find(r, 'commPlace').textContent), ['1', '2'], 'positions in the order sent');
|
|
365
|
+
const first = find(rows[0], 'mineRow__name');
|
|
366
|
+
assert.equal(first.tag, 'a');
|
|
367
|
+
assert.equal(first.attrs.href, 'https://tidepool.example/');
|
|
368
|
+
assert.equal(first.textContent, 'Tide <i>pool</i>');
|
|
369
|
+
assert.equal(find(rows[0], 'mineRow__hint').textContent, (1840).toLocaleString() + ' credits earned');
|
|
370
|
+
assert.equal(find(rows[0], 'commCredits').textContent, '23works shipped');
|
|
371
|
+
assert.equal(find(rows[1], 'mineRow__name').tag, 'div', 'an address that is not http(s) is never a link');
|
|
372
|
+
assert.equal(find(rows[1], 'commCredits').textContent, '1work shipped', 'one work, singular');
|
|
373
|
+
assert.equal(ui.$('moveNone').hidden, true);
|
|
374
|
+
const signedIn = await boot({ pathname: '/community', me: { github_login: 'owner' } });
|
|
375
|
+
assert.equal(signedIn.$('homeMovers').textContent, ui.$('homeMovers').textContent, 'the same row for every viewer');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('the row under the crowns: nobody to show is one line in each card; a failed read drops only the row', async () => {
|
|
379
|
+
const reads = READS();
|
|
380
|
+
reads['/community/movers'] = { status: 200, body: { window: {}, rising: null, projects: [] } };
|
|
381
|
+
const quiet = await boot({ pathname: '/community', reads });
|
|
382
|
+
assert.equal(quiet.$('homeMovers').hidden, false);
|
|
383
|
+
assert.equal(find(quiet.$('moveRising'), 'commMsg').textContent, 'No rising builder in the last 30 days.');
|
|
384
|
+
assert.equal(kids(quiet.$('moveProjects')).length, 0);
|
|
385
|
+
assert.equal(quiet.$('moveNone').hidden, false);
|
|
386
|
+
for (const answer of [{ status: 500, body: {} }, { status: 429, body: {} }, { reject: true }]) {
|
|
387
|
+
reads['/community/movers'] = answer;
|
|
388
|
+
const failed = await boot({ pathname: '/community', reads });
|
|
389
|
+
assert.equal(failed.$('homeMovers').hidden, true);
|
|
390
|
+
assert.equal(failed.$('moversRule').hidden, true, 'no rule for cards that are not there');
|
|
391
|
+
assert.equal(failed.$('homeCrowns').hidden, false, 'the crowns stand on their own');
|
|
392
|
+
assert.equal(failed.$('homeFailed').hidden, true, 'and no alert: the row is the home’s extra');
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
238
396
|
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 () => {
|
|
239
397
|
const ui = await boot();
|
|
240
398
|
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week']);
|
|
@@ -535,7 +693,8 @@ const SERVES_A_CHILD = ['privacy.html', 'terms.html', 'contact.html', 'index.htm
|
|
|
535
693
|
const HUB_ONLY = ['account.html', 'connections.html', 'invitations.html', 'guild.html', 'community.html'];
|
|
536
694
|
const wrap = (apex, s) => (apex ? `<!--apex-only:start-->${s}<!--apex-only:end-->` : s);
|
|
537
695
|
const ITEMS = [
|
|
538
|
-
['/community', '
|
|
696
|
+
['/community', 'Community home', 'the builders of the week, month and year'],
|
|
697
|
+
['/community/leaderboard', 'Leaderboard', 'who earned the most, across every project'],
|
|
539
698
|
['/community/week', 'Builder of the week', 'the top earner of each week, and how it is chosen'],
|
|
540
699
|
['/community/find', 'Find a builder', 'anyone building here, by name or role'],
|
|
541
700
|
['/community/recruiters', 'Open to recruiters', 'who is open to recruiting, for project owners'],
|
|
@@ -557,9 +716,9 @@ test('every page that wears the hub’s bar carries the Community menu before Pr
|
|
|
557
716
|
const at = html.indexOf(apex ? `<!--apex-only:start-->${trig}` : trig);
|
|
558
717
|
assert.ok(at > 0, `${f}: the Community trigger`);
|
|
559
718
|
assert.ok(html.indexOf('id="projTrig"') > at, `${f}: Community comes before Projects`);
|
|
560
|
-
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its
|
|
719
|
+
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its five sub-pages`);
|
|
561
720
|
assert.ok(html.indexOf('id="menuCommunity"') < html.indexOf('id="menuProjects"'), `${f}: the bands in the bar’s order`);
|
|
562
|
-
assert.ok(html.includes(wrap(apex, fold)), `${f}: the
|
|
721
|
+
assert.ok(html.includes(wrap(apex, fold)), `${f}: the five folded into the Projects band for phones`);
|
|
563
722
|
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`);
|
|
564
723
|
}
|
|
565
724
|
});
|
|
@@ -617,7 +776,7 @@ function get(pathname, host) {
|
|
|
617
776
|
});
|
|
618
777
|
}
|
|
619
778
|
|
|
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 () => {
|
|
779
|
+
test('GET /community, /community/leaderboard, /community/week, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
|
|
621
780
|
modules.setModuleOverrides({ 'platform-identity': false });
|
|
622
781
|
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`);
|
|
623
782
|
modules.setModuleOverrides({ 'platform-identity': true });
|
|
@@ -630,7 +789,7 @@ test('GET /community, /community/week, /community/find and /community/recruiters
|
|
|
630
789
|
bodies.push(hub.body);
|
|
631
790
|
}
|
|
632
791
|
assert.ok(bodies.every((b) => b === bodies[0]), 'byte-identical for every sub-page: the script picks the view');
|
|
633
|
-
assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the
|
|
792
|
+
assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the five sub-pages');
|
|
634
793
|
for (const host of ['builders.test.local', 'status.test.local']) {
|
|
635
794
|
assert.doesNotMatch((await get('/community', host)).body, /— Community<\/title>/, `${host} must not serve /community`);
|
|
636
795
|
}
|
|
@@ -643,10 +802,19 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
|
|
|
643
802
|
const states = json('community.states.json');
|
|
644
803
|
assert.equal(states.page, '/community');
|
|
645
804
|
assert.deepEqual(states.modes, ['dark']);
|
|
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}`);
|
|
805
|
+
for (const s of ['out', 'in', 'out-board', 'out-30d', 'out-artists', 'out-week', 'in-week', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in-board']) assert.ok(states.states[s], `kit state ${s}`);
|
|
806
|
+
assert.equal(states.states.out.url, undefined, 'the home is /community itself');
|
|
807
|
+
for (const s of ['out-board', 'out-30d', 'out-artists', 'in-board']) assert.equal(states.states[s].url, '/community/leaderboard', `${s}: the board’s own address`);
|
|
647
808
|
assert.equal(states.states['out-find'].url, '/community/find');
|
|
648
809
|
assert.equal(states.states['out-week'].url, '/community/week');
|
|
649
|
-
assert.ok(states.states
|
|
810
|
+
assert.ok(states.states['out-board'].expect.visible.includes('#boardCrown'), 'the board carries the crown line');
|
|
811
|
+
assert.ok(states.states.out.expect.visible.includes('#homeMovers'), 'the home carries the row under the crowns (task 1002983)');
|
|
812
|
+
assert.equal(json('community-movers-none.states.json').stub.fixtures['community-movers'], 'community-movers-none');
|
|
813
|
+
// the home's other answers (task 1004902): before the first month, nobody, and a new year
|
|
814
|
+
for (const [file, fixture] of [['community-crowns-first.states.json', 'community-crowns-first'], ['community-crowns-none.states.json', 'community-crowns-none'], ['community-crowns-new-year.states.json', 'community-crowns-new-year']]) {
|
|
815
|
+
assert.equal(json(file).stub.fixtures['community-crowns'], fixture);
|
|
816
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
|
|
817
|
+
}
|
|
650
818
|
assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
|
|
651
819
|
assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
|
|
652
820
|
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']]) {
|
|
@@ -685,9 +853,20 @@ async function call(origin, p) {
|
|
|
685
853
|
return { status: res.status, body: await res.json() };
|
|
686
854
|
}
|
|
687
855
|
|
|
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 () => {
|
|
856
|
+
test('the kit stub answers the crowns, the movers, the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
|
|
689
857
|
const s = await stub();
|
|
690
858
|
try {
|
|
859
|
+
const crowns = await call(s.origin, '/community/crowns');
|
|
860
|
+
assert.deepEqual(Object.keys(crowns.body), ['week', 'month', 'year'], 'the route’s three periods');
|
|
861
|
+
const { week: w, month: m, year: y } = crowns.body;
|
|
862
|
+
assert.deepEqual([Object.keys(w).sort(), Object.keys(m).sort(), Object.keys(y).sort()], [['end', 'start', 'winner'], ['end', 'first_crown', 'start', 'winner'], ['last', 'since', 'start', 'winner']], 'community-crowns.js’s shape');
|
|
863
|
+
assert.ok([w.winner, m.winner, y.winner].every((r) => r && Object.keys(r).sort().join() === 'avatar_url,credits,display_name,github_login,handle,tasks_shipped'), 'the module’s projection, no github_id');
|
|
864
|
+
const movers = await call(s.origin, '/community/movers');
|
|
865
|
+
assert.deepEqual(Object.keys(movers.body), ['window', 'rising', 'projects'], 'community-movers.js’s shape');
|
|
866
|
+
assert.equal(Object.keys(movers.body.rising).sort().join(), 'avatar_url,credits,display_name,github_login,handle,tasks_shipped', 'the crown’s projection, no github_id');
|
|
867
|
+
assert.ok(movers.body.projects.length && movers.body.projects.every((p) => Object.keys(p).sort().join() === 'credits,name,origin,tasks_shipped'), 'a project’s name, address and two numbers, no client_id');
|
|
868
|
+
const ships = movers.body.projects.map((p) => p.tasks_shipped);
|
|
869
|
+
assert.deepEqual(ships, [...ships].sort((a, b) => b - a), 'most works first');
|
|
691
870
|
const week = await call(s.origin, '/community/builder-of-the-week');
|
|
692
871
|
assert.deepEqual(Object.keys(week.body).sort(), ['past', 'week', 'winner'], 'the route’s three keys');
|
|
693
872
|
assert.ok(week.body.winner && week.body.past.length, 'a crown and the weeks before it');
|
|
@@ -717,6 +896,11 @@ test('the kit stub answers the board, the builder of the week, the search and th
|
|
|
717
896
|
assert.equal(r.body.winner, null);
|
|
718
897
|
assert.deepEqual(r.body.past, []);
|
|
719
898
|
} finally { none.stop(); }
|
|
899
|
+
const nobody = await stub({ FIXTURE_ALIAS: JSON.stringify({ 'community-crowns': 'community-crowns-none' }) });
|
|
900
|
+
try {
|
|
901
|
+
const r = await call(nobody.origin, '/community/crowns');
|
|
902
|
+
assert.deepEqual([r.body.week.winner, r.body.month.winner, r.body.year.winner, r.body.year.last], [null, null, null, null]);
|
|
903
|
+
} finally { nobody.stop(); }
|
|
720
904
|
const owner = await stub({ AUTH: '1', FIXTURE_ALIAS: JSON.stringify({ scouting: 'scouting-recruiter' }) });
|
|
721
905
|
try {
|
|
722
906
|
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'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
196
|
+
// the Community menu's five sub-pages, folded into the band under 480px (tasks 1003363, 1002981, 1004902); apex-only
|
|
197
|
+
['Community home', '/community'], ['Leaderboard', '/community/leaderboard'], ['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
|
@@ -154,7 +154,15 @@ function identitySentinels(acct) {
|
|
|
154
154
|
// the past weeks (task 1002981, spec D5): a row IS stats, so the board's rule.
|
|
155
155
|
// Every fixture account has won a recorded week, so the read's own predicate
|
|
156
156
|
// decides: 'listed' | 'absent'
|
|
157
|
-
//
|
|
157
|
+
// crowns GET /community/crowns Anon · Builder · Recruiter — the builders of the month,
|
|
158
|
+
// the year so far and last year (task 1004902): a crown IS stats, so the board's
|
|
159
|
+
// rule. Each account in turn is made the winner of all three, so the reads'
|
|
160
|
+
// own predicates decide: 'listed' | 'absent'
|
|
161
|
+
// movers GET /community/movers Anon · Builder · Recruiter — the rising builder and the
|
|
162
|
+
// projects on the move (task 1002983): a card IS stats, so the board's rule, and
|
|
163
|
+
// a project's totals are the sum of what may be shown. Each account in turn is
|
|
164
|
+
// the one who moved, so the reads' own predicates decide: 'listed' | 'absent'
|
|
165
|
+
// directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
|
|
158
166
|
// sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
|
|
159
167
|
// self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
|
|
160
168
|
// guild GET /guilds/:slug Anon · Builder · Recruiter · Self — the guild page's PUBLIC
|
|
@@ -170,13 +178,13 @@ function identitySentinels(acct) {
|
|
|
170
178
|
// people the guild page already shows them — shown_publicly AND existence;
|
|
171
179
|
// hide_stats never hides membership, and private or provisional is never listed
|
|
172
180
|
const MATRIX = {
|
|
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' },
|
|
181
|
+
PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
|
|
182
|
+
PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
183
|
+
SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
184
|
+
PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
185
|
+
PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
186
|
+
PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
187
|
+
OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
180
188
|
};
|
|
181
189
|
|
|
182
190
|
// ===========================================================================
|
|
@@ -296,6 +304,12 @@ function weekOf(key) {
|
|
|
296
304
|
}
|
|
297
305
|
return weeksWon[key];
|
|
298
306
|
}
|
|
307
|
+
// THE ACCOUNT that won the month, the year so far and last year (task 1004902); the
|
|
308
|
+
// crowns test makes each fixture account the winner in turn.
|
|
309
|
+
let CROWNED = 'PUB';
|
|
310
|
+
// THE ACCOUNT that moved in the 30 days, on MX_CLIENT's project (task 1002983); the movers
|
|
311
|
+
// test makes each fixture account the one in turn.
|
|
312
|
+
let MOVED = 'PUB';
|
|
299
313
|
|
|
300
314
|
const pool = {
|
|
301
315
|
query: async (text, params = []) => {
|
|
@@ -370,6 +384,53 @@ const pool = {
|
|
|
370
384
|
return { rows, rowCount: rows.length };
|
|
371
385
|
}
|
|
372
386
|
|
|
387
|
+
// THE CROWNS (task 1004902): history began long ago, every month and year is already
|
|
388
|
+
// recorded, and CROWNED names the account that won all three — so each read's own
|
|
389
|
+
// predicate decides who is shown. The projections are pinned: a new column is a new
|
|
390
|
+
// disclosure.
|
|
391
|
+
if (/^SELECT reported_at FROM platform_identity_activity_snapshots ORDER BY id ASC LIMIT 1$/.test(sql)) return { rows: [{ reported_at: '2020-01-01T00:00:00.000Z' }], rowCount: 1 };
|
|
392
|
+
if (/^SELECT 1 FROM platform_identity_period_features WHERE /.test(sql)) return { rows: [{ '?column?': 1 }], rowCount: 1 };
|
|
393
|
+
if (/FROM platform_identity_period_features f JOIN platform_identity_accounts pa/.test(sql)) {
|
|
394
|
+
const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
|
|
395
|
+
if (cols !== 'f.credits_delta, f.ships_delta, pa.handle, pa.github_login, pa.display_name, pa.avatar_url') throw new Error(`fake pool: the crowned-period read selects something new — ${cols}`);
|
|
396
|
+
const where = lift(sql, /AND f\.period_start = \$2::date AND (.+)$/, 'crowned period');
|
|
397
|
+
const rows = ACCOUNTS.filter((a) => a.key === CROWNED).filter(accountFilter(where, [])).map((a) => ({
|
|
398
|
+
credits_delta: String(ROLLUP[a.github_id].credits), ships_delta: String(ROLLUP[a.github_id].works_shipped),
|
|
399
|
+
handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url,
|
|
400
|
+
}));
|
|
401
|
+
return { rows, rowCount: rows.length };
|
|
402
|
+
}
|
|
403
|
+
// THE MOVERS (task 1002983): the 30 days' rows carry the floor on the act CTE, as the
|
|
404
|
+
// year's; MOVED had nothing before them. The project names are MX_CLIENT's.
|
|
405
|
+
if (/^WITH act AS/.test(sql) && /LEFT JOIN per_project pp/.test(sql)) {
|
|
406
|
+
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'movers');
|
|
407
|
+
const rows = ACCOUNTS.filter((a) => a.key === MOVED).filter(accountFilter(where, [])).map((a) => ({
|
|
408
|
+
client_id: MX_CLIENT.client_id, github_id: a.github_id, credits: String(ROLLUP[a.github_id].credits),
|
|
409
|
+
tasks_shipped: String(ROLLUP[a.github_id].works_shipped), karma: String(ROLLUP[a.github_id].karma), base_credits: '0',
|
|
410
|
+
}));
|
|
411
|
+
return { rows, rowCount: rows.length };
|
|
412
|
+
}
|
|
413
|
+
if (/FROM platform_identity_sso_clients c JOIN platform_identity_projects p ON p\.origin = c\.origin WHERE c\.client_id = ANY\(\$1::text\[\]\)/.test(sql)) {
|
|
414
|
+
const rows = (params[0] || []).includes(MX_CLIENT.client_id) ? [{ client_id: MX_CLIENT.client_id, name: MX_CLIENT.name, origin: MX_CLIENT.origin }] : [];
|
|
415
|
+
return { rows, rowCount: rows.length };
|
|
416
|
+
}
|
|
417
|
+
// ... the year so far: computeWinner's act CTE carries the floor, then the leader's re-read
|
|
418
|
+
if (/^WITH act AS/.test(sql) && /\) f ON true/.test(sql)) {
|
|
419
|
+
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'year so far');
|
|
420
|
+
const rows = ACCOUNTS.filter((a) => a.key === CROWNED).filter(accountFilter(where, [])).map((a) => ({
|
|
421
|
+
github_id: a.github_id, credits: String(ROLLUP[a.github_id].credits), tasks_shipped: String(ROLLUP[a.github_id].works_shipped),
|
|
422
|
+
}));
|
|
423
|
+
return { rows, rowCount: rows.length };
|
|
424
|
+
}
|
|
425
|
+
if (/FROM platform_identity_accounts pa WHERE pa\.github_id = \$1::bigint AND /.test(sql)) {
|
|
426
|
+
const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
|
|
427
|
+
if (cols !== 'pa.handle, pa.github_login, pa.display_name, pa.avatar_url') throw new Error(`fake pool: the year leader read selects something new — ${cols}`);
|
|
428
|
+
const where = lift(sql, /WHERE pa\.github_id = \$1::bigint AND (.+)$/, 'year leader');
|
|
429
|
+
const rows = ACCOUNTS.filter((a) => a.github_id === String(params[0])).filter(accountFilter(where, []))
|
|
430
|
+
.map((a) => ({ handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url }));
|
|
431
|
+
return { rows, rowCount: rows.length };
|
|
432
|
+
}
|
|
433
|
+
|
|
373
434
|
// THE LEADERBOARD's totals: the rollup floor sits on the act CTE's join.
|
|
374
435
|
if (/platform_identity_activity_snapshots/.test(sql)) {
|
|
375
436
|
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'leaderboard totals');
|
|
@@ -809,6 +870,43 @@ test('the builder of the week — only shown, public, active winners, on the cro
|
|
|
809
870
|
assert.doesNotMatch(seen[0].text, /github_id/, 'github_id never crosses');
|
|
810
871
|
});
|
|
811
872
|
|
|
873
|
+
test('the crowns — the month, the year so far and last year only for shown, public, active winners, the same for every viewer', async () => {
|
|
874
|
+
const { clearCrownsMemo } = require('../modules/platform-identity/community-crowns.js');
|
|
875
|
+
try {
|
|
876
|
+
for (const k of KEYS) {
|
|
877
|
+
CROWNED = k;
|
|
878
|
+
clearCrownsMemo(); // the year's leader is memoised: a new winner is a new computation
|
|
879
|
+
const seen = [];
|
|
880
|
+
for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/crowns'));
|
|
881
|
+
for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got different crowns with ${k} crowned`);
|
|
882
|
+
const { month, year } = seen[0].json;
|
|
883
|
+
const shown = { month: month.winner, year: year.winner, last: year.last && year.last.winner };
|
|
884
|
+
for (const [which, w] of Object.entries(shown)) {
|
|
885
|
+
assert.equal(!!w && w.handle === ACCT[k].handle, MATRIX[k].crowns === 'listed', `${k} as ${which}: expected ${MATRIX[k].crowns}`);
|
|
886
|
+
}
|
|
887
|
+
assert.doesNotMatch(seen[0].text, /github_id/, 'github_id never crosses');
|
|
888
|
+
}
|
|
889
|
+
} finally { CROWNED = 'PUB'; }
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test('the movers — the rising builder and a project’s totals only from shown, public, active accounts, the same for every viewer', async () => {
|
|
893
|
+
const { clearMoversMemo } = require('../modules/platform-identity/community-movers.js');
|
|
894
|
+
try {
|
|
895
|
+
for (const k of KEYS) {
|
|
896
|
+
MOVED = k;
|
|
897
|
+
clearMoversMemo(); // the 30 days are memoised: a new mover is a new computation
|
|
898
|
+
const seen = [];
|
|
899
|
+
for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/movers'));
|
|
900
|
+
for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got different movers with ${k} moving`);
|
|
901
|
+
const { rising, projects } = seen[0].json;
|
|
902
|
+
const listed = MATRIX[k].movers === 'listed';
|
|
903
|
+
assert.equal(!!rising && rising.handle === ACCT[k].handle, listed, `${k} as the rising builder: expected ${MATRIX[k].movers}`);
|
|
904
|
+
assert.deepEqual(projects.map((p) => p.tasks_shipped), listed ? [ROLLUP[ACCT[k].github_id].works_shipped] : [], `${k}'s works in a project's totals: expected ${MATRIX[k].movers}`);
|
|
905
|
+
assert.doesNotMatch(seen[0].text, /github_id|client_id/, 'github_id and client_id never cross');
|
|
906
|
+
}
|
|
907
|
+
} finally { MOVED = 'PUB'; }
|
|
908
|
+
});
|
|
909
|
+
|
|
812
910
|
test('the recruiting directory — the recruiter sees the full and name-only tiers; Anon and Builder are refused', async () => {
|
|
813
911
|
const r = await get(RECRUITER, '/api/bongos/scouting');
|
|
814
912
|
assert.equal(r.status, 200);
|
|
@@ -851,7 +949,7 @@ function disclosed(r) {
|
|
|
851
949
|
}
|
|
852
950
|
|
|
853
951
|
test('the walk covered every surface for every row (so the claims below are about something)', () => {
|
|
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']) {
|
|
952
|
+
for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/community/builder-of-the-week', '/community/crowns', '/community/movers', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
|
|
855
953
|
assert.ok(SEEN.some((r) => r.path.includes(p) && r.status === 200), `no 200 from ${p} — the sweep below would be vacuous for it`);
|
|
856
954
|
}
|
|
857
955
|
assert.ok(SEEN.length >= KEYS.length * 6, `only ${SEEN.length} responses recorded`);
|