@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.
Files changed (60) hide show
  1. package/.bongos-core.json +134 -49
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +4 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/api/openapi.json +43 -1
  8. package/docs/api-reference.md +4 -2
  9. package/docs/copy-inventory.md +992 -935
  10. package/docs/copy-registry.json +1897 -1313
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-inventory.json +12 -2
  13. package/docs/page-readings.json +616 -576
  14. package/modules/platform-identity/builder-of-the-week.js +61 -24
  15. package/modules/platform-identity/community-crowns.js +176 -0
  16. package/modules/platform-identity/community-leaderboard.js +4 -1
  17. package/modules/platform-identity/community-movers.js +152 -0
  18. package/modules/platform-identity/migrations/platform_identity_033_period_features.sql +43 -0
  19. package/modules/platform-identity/routes/community-leaderboard.js +45 -3
  20. package/modules/public-landing/public/account.html +3 -2
  21. package/modules/public-landing/public/community-crowns-first.states.json +15 -0
  22. package/modules/public-landing/public/community-crowns-new-year.states.json +15 -0
  23. package/modules/public-landing/public/community-crowns-none.states.json +15 -0
  24. package/modules/public-landing/public/community-empty.states.json +1 -1
  25. package/modules/public-landing/public/community-movers-none.states.json +15 -0
  26. package/modules/public-landing/public/community-owner-two.states.json +2 -2
  27. package/modules/public-landing/public/community-owner.states.json +1 -1
  28. package/modules/public-landing/public/community-week-none.states.json +1 -1
  29. package/modules/public-landing/public/community.html +188 -22
  30. package/modules/public-landing/public/community.probes.json +20 -3
  31. package/modules/public-landing/public/community.states.json +8 -6
  32. package/modules/public-landing/public/connections.html +3 -2
  33. package/modules/public-landing/public/contact.html +3 -2
  34. package/modules/public-landing/public/guild.html +3 -2
  35. package/modules/public-landing/public/index.html +3 -2
  36. package/modules/public-landing/public/invitations.html +3 -2
  37. package/modules/public-landing/public/privacy.html +3 -2
  38. package/modules/public-landing/public/projects.html +3 -2
  39. package/modules/public-landing/public/terms.html +3 -2
  40. package/modules/ui-design/kit/fixtures/community-crowns-first.json +6 -0
  41. package/modules/ui-design/kit/fixtures/community-crowns-new-year.json +6 -0
  42. package/modules/ui-design/kit/fixtures/community-crowns-none.json +6 -0
  43. package/modules/ui-design/kit/fixtures/community-crowns.json +6 -0
  44. package/modules/ui-design/kit/fixtures/community-movers-none.json +6 -0
  45. package/modules/ui-design/kit/fixtures/community-movers.json +12 -0
  46. package/modules/ui-design/kit/serve.js +22 -0
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/release-notes.json +12 -0
  50. package/scripts/bongos/run-unit-tests.js +8 -0
  51. package/src/module-api.js +1 -1
  52. package/src/platform-server.js +4 -3
  53. package/tests/community_crowns.mjs +369 -0
  54. package/tests/community_crowns_db.mjs +164 -0
  55. package/tests/community_movers.mjs +280 -0
  56. package/tests/community_movers_db.mjs +147 -0
  57. package/tests/hub_account_page.mjs +2 -2
  58. package/tests/hub_community_page.mjs +213 -29
  59. package/tests/landing_legal_pages.mjs +2 -2
  60. package/tests/privacy_matrix.mjs +107 -9
@@ -0,0 +1,369 @@
1
+ // tests/community_crowns.mjs — the builders of the week, the month and the year behind the
2
+ // hub's /community page (task 1004902).
3
+ //
4
+ // DB-free. A fake pool answers each statement by its shape. Which snapshots open and close
5
+ // a month or a year, and that the year's first-snapshot baseline really counts, need a real
6
+ // Postgres: tests/community_crowns_db.mjs.
7
+ //
8
+ // What this pins:
9
+ // 1. the month: the last FULL calendar month (UTC) before now, at every edge.
10
+ // 2. history: a month that began before snapshot history is not crowned, and the answer
11
+ // names the day the first month will be; no history at all crowns nothing.
12
+ // 3. the month crowned like the week: the week's statement, written once and never
13
+ // overwritten, crowned once however many reads arrive, read back through the
14
+ // leaderboard's numbers predicate.
15
+ // 4. the year: live from 1 January with the first-snapshot baseline, memoised for
16
+ // LEADER_TTL_MS, `since` while history starts inside the year, the leader shown only if
17
+ // still eligible, asked on every read; the year that ended
18
+ // crowned on the first read of the next, by the same rule, and shown as `last`.
19
+ // 5. the answer: the three periods, no github_id anywhere.
20
+ // 6. the route: no-store, the same body with or without credentials, the 500 envelope,
21
+ // and its own rate-limit scope.
22
+ //
23
+ // Run: node tests/community_crowns.mjs
24
+
25
+ import { strict as assert } from 'node:assert';
26
+ import { createRequire } from 'node:module';
27
+ import http from 'node:http';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const express = require('express');
31
+
32
+ let passed = 0;
33
+ let failed = 0;
34
+ async function t(name, fn) {
35
+ try { await fn(); passed++; console.log(` ok ${name}`); }
36
+ catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
37
+ }
38
+
39
+ const { rollupVisibleSql } = require('../modules/platform-identity/account-visibility.js');
40
+ const NUMBERS = rollupVisibleSql('pa').replace(/\s+/g, ' ').trim();
41
+
42
+ // ---- fake engine ------------------------------------------------------------
43
+
44
+ const WEEK_PROBE = /^SELECT 1 FROM platform_identity_weekly_features WHERE week_start = \$1::date$/;
45
+ const isWeekInsert = (sql) => /^INSERT INTO platform_identity_weekly_features/.test(sql);
46
+ const isWeekRead = (sql) => /FROM platform_identity_weekly_features f\s+JOIN platform_identity_accounts pa/.test(sql);
47
+ const HISTORY = /^SELECT reported_at FROM platform_identity_activity_snapshots ORDER BY id ASC LIMIT 1$/;
48
+ const PERIOD_PROBE = /^SELECT 1 FROM platform_identity_period_features WHERE period = \$1 AND period_start = \$2::date$/;
49
+ const isWinner = (sql) => /^WITH act AS/.test(sql);
50
+ const isYearRule = (sql) => /\) f ON true/.test(sql);
51
+ const isPeriodInsert = (sql) => /^INSERT INTO platform_identity_period_features/.test(sql);
52
+ const isPeriodRead = (sql) => /FROM platform_identity_period_features f JOIN platform_identity_accounts pa/.test(sql);
53
+ const isLeaderRead = (sql) => /FROM platform_identity_accounts pa WHERE pa\.github_id = \$1::bigint/.test(sql);
54
+ const iso = (d) => (d instanceof Date ? d.toISOString() : String(d));
55
+
56
+ const engine = {
57
+ calls: [],
58
+ history: null, // the first snapshot's reported_at, or null
59
+ periods: new Set(), // 'month:2026-10-01' rows the probe finds
60
+ winners: {}, // 'month' | 'year' | 'week' → the winner statement's row
61
+ periodShown: {}, // 'month:2026-10-01' → the read's row (already filtered)
62
+ accounts: {}, // github_id → the leader read's row (already filtered)
63
+ weekShown: [],
64
+ throwOn: null,
65
+ slowWinner: false,
66
+ query: async (text, params = []) => {
67
+ const sql = String(text).replace(/\s+/g, ' ').trim();
68
+ engine.calls.push({ sql, params });
69
+ if (engine.throwOn && engine.throwOn(sql)) { engine.throwOn = null; throw new Error('boom'); }
70
+ if (WEEK_PROBE.test(sql)) return { rows: [{ '?column?': 1 }] };
71
+ if (isWeekRead(sql)) return { rows: engine.weekShown };
72
+ if (isWeekInsert(sql)) return { rows: [], rowCount: 1 };
73
+ if (HISTORY.test(sql)) return { rows: engine.history ? [{ reported_at: engine.history }] : [] };
74
+ if (PERIOD_PROBE.test(sql)) return { rows: engine.periods.has(`${params[0]}:${params[1]}`) ? [{ '?column?': 1 }] : [] };
75
+ if (isWinner(sql)) {
76
+ if (engine.slowWinner) await new Promise((r) => setTimeout(r, 10));
77
+ const w = engine.winners[isYearRule(sql) ? 'year' : 'month'];
78
+ return { rows: w ? [w] : [] };
79
+ }
80
+ if (isPeriodInsert(sql)) { engine.periods.add(`${params[0]}:${params[1]}`); return { rows: [], rowCount: 1 }; }
81
+ if (isPeriodRead(sql)) { const r = engine.periodShown[`${params[0]}:${params[1]}`]; return { rows: r ? [r] : [] }; }
82
+ if (isLeaderRead(sql)) { const r = engine.accounts[params[0]]; return { rows: r ? [r] : [] }; }
83
+ throw new Error(`fake pool: unexpected statement ${sql.slice(0, 80)}`);
84
+ },
85
+ };
86
+ function reset() {
87
+ Object.assign(engine, {
88
+ calls: [], history: null, periods: new Set(), winners: {}, periodShown: {}, accounts: {},
89
+ weekShown: [], throwOn: null, slowWinner: false,
90
+ });
91
+ botw.clearCrownMemo();
92
+ crowns.clearCrownsMemo();
93
+ }
94
+ const callsOf = (pred) => engine.calls.filter((c) => pred(c.sql));
95
+
96
+ // Pin the module doorway (ADR 0083) BEFORE the modules load.
97
+ const moduleApiPath = require.resolve('../src/module-api.js');
98
+ require.cache[moduleApiPath] = {
99
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
100
+ exports: {
101
+ pool: engine,
102
+ corsPublicGet: () => (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); next(); },
103
+ logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
104
+ },
105
+ };
106
+
107
+ const botw = require('../modules/platform-identity/builder-of-the-week.js');
108
+ const crowns = require('../modules/platform-identity/community-crowns.js');
109
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
110
+ const buildRoutes = require('../modules/platform-identity/routes/community-leaderboard.js');
111
+
112
+ const HISTORY_BEGAN = '2026-09-27T14:05:00.000Z';
113
+ const row = (handle, credits, ships) => ({ handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: null, credits_delta: String(credits), ships_delta: String(ships) });
114
+ const acct = (handle) => ({ handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: null });
115
+ const read = (now) => crowns.readCrowns({ now: new Date(now), pool: engine });
116
+
117
+ // ---- 1. the month -----------------------------------------------------------
118
+
119
+ await t('the crowned month is the last full calendar month (UTC) before now, at every edge', () => {
120
+ const month = (now) => { const m = crowns.crownedMonth(new Date(now)); return [m.start.toISOString(), m.end.toISOString()]; };
121
+ const oct = ['2026-10-01T00:00:00.000Z', '2026-11-01T00:00:00.000Z'];
122
+ assert.deepEqual(month('2026-11-17T09:30:00.000Z'), oct, 'mid-month: the month before this one');
123
+ assert.deepEqual(month('2026-11-01T00:00:00.000Z'), oct, 'the very first instant of the 1st: the month that just ended');
124
+ assert.deepEqual(month('2026-11-30T23:59:59.999Z'), oct, 'the last instant of the month: still the same one');
125
+ assert.deepEqual(month('2026-10-31T23:59:59.999Z'), ['2026-09-01T00:00:00.000Z', '2026-10-01T00:00:00.000Z'], 'the month ending tonight is not over yet');
126
+ assert.deepEqual(month('2027-01-04T12:00:00.000Z'), ['2026-12-01T00:00:00.000Z', '2027-01-01T00:00:00.000Z'], 'across a year');
127
+ });
128
+
129
+ // ---- 2. history ----------------------------------------------------------------
130
+
131
+ await t('a month that began before snapshot history is not crowned; the answer names the first crown', async () => {
132
+ reset();
133
+ engine.history = HISTORY_BEGAN;
134
+ const out = await read('2026-10-28T09:30:00.000Z');
135
+ assert.deepEqual(out.month, { start: '2026-09-01', end: '2026-10-01', winner: null, first_crown: '2026-11-01' });
136
+ assert.equal(callsOf((s) => PERIOD_PROBE.test(s) && /month/.test(s)).length, 0);
137
+ assert.equal(callsOf(isPeriodInsert).length, 0, 'nothing recorded: no month is "nobody" for want of data');
138
+ assert.equal(callsOf((s) => isWinner(s) && !isYearRule(s)).length, 0);
139
+ });
140
+
141
+ await t('history that begins on the 1st makes that month the first one', async () => {
142
+ reset();
143
+ engine.history = '2026-10-01T00:00:00.000Z';
144
+ assert.equal((await read('2026-10-28T09:30:00.000Z')).month.first_crown, '2026-11-01');
145
+ reset();
146
+ engine.history = '2026-10-01T00:00:00.000Z';
147
+ const nov = await read('2026-11-02T00:00:00.000Z');
148
+ assert.equal(nov.month.first_crown, null);
149
+ assert.deepEqual(callsOf(isPeriodInsert).map((c) => c.params.slice(0, 2)), [['month', '2026-10-01']]);
150
+ });
151
+
152
+ await t('no history at all: nothing computed, nothing written, no first crown to name', async () => {
153
+ reset();
154
+ const out = await read('2026-11-02T00:00:00.000Z');
155
+ assert.deepEqual(out.month, { start: '2026-10-01', end: '2026-11-01', winner: null, first_crown: null });
156
+ assert.deepEqual(out.year, { start: '2026-01-01', since: '2026-01-01', winner: null, last: null });
157
+ assert.equal(callsOf(isWinner).length, 0);
158
+ assert.equal(callsOf(isPeriodInsert).length, 0);
159
+ });
160
+
161
+ await t('history is asked once per process once it exists, and again while it does not', async () => {
162
+ reset();
163
+ await read('2026-10-28T09:30:00.000Z');
164
+ await read('2026-10-28T09:31:00.000Z');
165
+ assert.equal(callsOf((s) => HISTORY.test(s)).length, 2, 'no history yet: asked every time');
166
+ engine.history = HISTORY_BEGAN;
167
+ await read('2026-10-28T09:32:00.000Z');
168
+ await read('2026-10-28T09:33:00.000Z');
169
+ assert.equal(callsOf((s) => HISTORY.test(s)).length, 3);
170
+ });
171
+
172
+ // ---- 3. the month crowned like the week -------------------------------------------
173
+
174
+ await t('the month: the week’s statement and rule, written once and never overwritten', async () => {
175
+ reset();
176
+ engine.history = HISTORY_BEGAN;
177
+ engine.winners.month = { github_id: '42', credits: '900', tasks_shipped: '3' };
178
+ await read('2026-11-03T08:00:00.000Z');
179
+ const [w] = callsOf((s) => isWinner(s) && !isYearRule(s));
180
+ assert.ok(w.sql.includes(NUMBERS), 'the leaderboard’s numbers predicate');
181
+ assert.match(w.sql, /HAVING sum\(tasks_shipped\) >= 1/);
182
+ assert.match(w.sql, /\) b ON true WHERE b\.credits IS NOT NULL OR act\.first_seen_at >= \$1::timestamptz \)/, 'the week’s baseline rule: no lifetime in one month');
183
+ assert.deepEqual(w.params.map(iso), ['2026-10-01T00:00:00.000Z', '2026-11-01T00:00:00.000Z']);
184
+ const [ins] = callsOf(isPeriodInsert);
185
+ assert.match(ins.sql, /ON CONFLICT \(period, period_start\) DO NOTHING$/, 'the first decision stands');
186
+ assert.doesNotMatch(ins.sql, /DO UPDATE/);
187
+ assert.deepEqual(ins.params, ['month', '2026-10-01', '42', '900', '3']);
188
+ });
189
+
190
+ await t('a month nobody won is written with no winner, so it stays empty', async () => {
191
+ reset();
192
+ engine.history = HISTORY_BEGAN;
193
+ const out = await read('2026-11-03T08:00:00.000Z');
194
+ assert.deepEqual(callsOf(isPeriodInsert)[0].params, ['month', '2026-10-01', null, 0, 0]);
195
+ assert.equal(out.month.winner, null);
196
+ });
197
+
198
+ await t('a recorded month is never recomputed, and an unrecorded one is crowned once however many reads arrive', async () => {
199
+ reset();
200
+ engine.history = HISTORY_BEGAN;
201
+ engine.periods.add('month:2026-10-01');
202
+ await read('2026-11-03T08:00:00.000Z');
203
+ assert.equal(callsOf((s) => isWinner(s) && !isYearRule(s)).length, 0);
204
+ reset();
205
+ engine.history = HISTORY_BEGAN;
206
+ engine.slowWinner = true;
207
+ engine.winners.month = { github_id: '7', credits: '50', tasks_shipped: '1' };
208
+ await Promise.all([1, 2, 3, 4].map(() => read('2026-11-03T08:00:00.000Z')));
209
+ assert.equal(callsOf((s) => isWinner(s) && !isYearRule(s)).length, 1);
210
+ assert.equal(callsOf((s) => isPeriodInsert(s)).length, 1);
211
+ assert.equal(callsOf(isPeriodRead).length, 4, 'every read still reads the crown');
212
+ });
213
+
214
+ await t('the month read: the numbers predicate at read time; a winner who hid since is not drawn', async () => {
215
+ reset();
216
+ engine.history = HISTORY_BEGAN;
217
+ engine.periods.add('month:2026-10-01');
218
+ engine.periodShown['month:2026-10-01'] = row('ana', 1240, 4);
219
+ const out = await read('2026-11-03T08:00:00.000Z');
220
+ const [r] = callsOf(isPeriodRead);
221
+ assert.ok(r.sql.includes(NUMBERS));
222
+ assert.doesNotMatch(r.sql.match(/^SELECT (.+?) FROM /)[1], /github_id/, 'github_id never crosses');
223
+ assert.deepEqual(out.month.winner, { handle: 'ana', github_login: 'ana', display_name: 'ANA', avatar_url: null, credits: 1240, tasks_shipped: 4 });
224
+ delete engine.periodShown['month:2026-10-01'];
225
+ assert.equal((await read('2026-11-03T08:00:00.000Z')).month.winner, null);
226
+ });
227
+
228
+ // ---- 4. the year ----------------------------------------------------------------
229
+
230
+ await t('the year so far: live from 1 January, counting a row with no earlier history from its first snapshot', async () => {
231
+ reset();
232
+ engine.history = HISTORY_BEGAN;
233
+ engine.winners.year = { github_id: '9', credits: '3000', tasks_shipped: '12' };
234
+ engine.accounts['9'] = acct('cy');
235
+ const out = await read('2026-10-28T09:30:00.000Z');
236
+ const [y] = callsOf(isYearRule);
237
+ assert.ok(y.sql.includes(NUMBERS));
238
+ assert.match(y.sql, /COALESCE\(e\.credits, 0\)::bigint - COALESCE\(b\.credits, CASE WHEN act\.first_seen_at >= \$1::timestamptz THEN 0 ELSE f\.credits END, 0\) AS credits/);
239
+ assert.match(y.sql, /AND s\.reported_at > \$1::timestamptz AND s\.reported_at <= \$2::timestamptz ORDER BY s\.reported_at ASC, s\.id ASC LIMIT 1 \) f ON true/, 'the first snapshot inside the year');
240
+ assert.match(y.sql, /WHERE b\.credits IS NOT NULL OR act\.first_seen_at >= \$1::timestamptz OR f\.credits IS NOT NULL \)/);
241
+ assert.deepEqual(y.params.map(iso), ['2026-01-01T00:00:00.000Z', '2026-10-28T09:30:00.000Z']);
242
+ const [l] = callsOf(isLeaderRead);
243
+ assert.ok(l.sql.includes(NUMBERS), 'the leader is shown only while eligible');
244
+ assert.deepEqual(out.year, {
245
+ start: '2026-01-01', since: '2026-09-27', last: null,
246
+ winner: { handle: 'cy', github_login: 'cy', display_name: 'CY', avatar_url: null, credits: 3000, tasks_shipped: 12 },
247
+ });
248
+ assert.equal(callsOf((s) => isPeriodInsert(s)).length, 0, 'a year so far is never written');
249
+ });
250
+
251
+ await t('the year so far is recomputed once its memo is older than LEADER_TTL_MS, and a leader who cannot be shown is no leader', async () => {
252
+ reset();
253
+ engine.history = HISTORY_BEGAN;
254
+ engine.winners.year = { github_id: '9', credits: '3000', tasks_shipped: '12' };
255
+ assert.equal((await read('2026-10-28T09:30:00.000Z')).year.winner, null);
256
+ engine.accounts['9'] = acct('cy');
257
+ assert.equal((await read('2026-10-28T09:31:00.000Z')).year.winner.handle, 'cy');
258
+ assert.equal(callsOf(isYearRule).length, 2);
259
+ });
260
+
261
+ await t('inside the memo the leader is not recomputed, but whether they may be shown is asked every read', async () => {
262
+ reset();
263
+ engine.history = HISTORY_BEGAN;
264
+ engine.winners.year = { github_id: '9', credits: '3000', tasks_shipped: '12' };
265
+ engine.accounts['9'] = acct('cy');
266
+ const t0 = Date.parse('2026-10-28T09:30:00.000Z');
267
+ assert.equal((await read(t0)).year.winner.handle, 'cy');
268
+ delete engine.accounts['9']; // the leader hides
269
+ assert.equal((await read(t0 + crowns.LEADER_TTL_MS - 1)).year.winner, null, 'a hide beats the memo');
270
+ assert.equal(callsOf(isYearRule).length, 1, 'one computation inside the memo');
271
+ assert.equal(callsOf(isLeaderRead).length, 2, 'the visibility read every time');
272
+ await read(t0 + crowns.LEADER_TTL_MS);
273
+ assert.equal(callsOf(isYearRule).length, 2, 'and a fresh one once it is stale');
274
+ await read(t0 - 1000);
275
+ assert.equal(callsOf(isYearRule).length, 3, 'a clock that runs backwards is never served from the memo');
276
+ });
277
+
278
+ await t('the year that ended is crowned on the first read of the next, by the year’s rule, and shown as last', async () => {
279
+ reset();
280
+ engine.history = HISTORY_BEGAN;
281
+ engine.winners.year = { github_id: '9', credits: '3000', tasks_shipped: '12' };
282
+ engine.accounts['9'] = acct('cy');
283
+ engine.periodShown['year:2026-01-01'] = row('cy', 3100, 13);
284
+ const out = await read('2027-01-02T10:00:00.000Z');
285
+ const crowned = callsOf(isYearRule).find((c) => iso(c.params[1]) === '2027-01-01T00:00:00.000Z');
286
+ assert.deepEqual(crowned.params.map(iso), ['2026-01-01T00:00:00.000Z', '2027-01-01T00:00:00.000Z']);
287
+ assert.deepEqual(callsOf(isPeriodInsert).map((c) => c.params.slice(0, 2)), [['month', '2026-12-01'], ['year', '2026-01-01']]);
288
+ assert.equal(out.year.start, '2027-01-01');
289
+ assert.equal(out.year.since, '2027-01-01', 'history began before this year');
290
+ assert.deepEqual(out.year.last, { start: '2026-01-01', winner: { handle: 'cy', github_login: 'cy', display_name: 'CY', avatar_url: null, credits: 3100, tasks_shipped: 13 } });
291
+ });
292
+
293
+ await t('a year that ended before history began is never crowned', async () => {
294
+ reset();
295
+ engine.history = HISTORY_BEGAN;
296
+ await read('2026-10-28T09:30:00.000Z');
297
+ assert.equal(callsOf((s) => PERIOD_PROBE.test(s)).filter((c) => c.params[0] === 'year').length, 0);
298
+ });
299
+
300
+ // ---- 5. the answer -----------------------------------------------------------------
301
+
302
+ await t('the three periods, the week as builder-of-the-week.js decides it, and no github_id anywhere', async () => {
303
+ reset();
304
+ engine.history = HISTORY_BEGAN;
305
+ engine.weekShown = [{ week_start: '2026-10-19', ...row('ana', 120, 2) }];
306
+ engine.winners.year = { github_id: '9', credits: '3000', tasks_shipped: '12' };
307
+ engine.accounts['9'] = acct('cy');
308
+ const out = await read('2026-10-28T09:30:00.000Z');
309
+ assert.deepEqual(Object.keys(out), ['week', 'month', 'year']);
310
+ assert.deepEqual(out.week, { start: '2026-10-19', end: '2026-10-26', winner: { handle: 'ana', github_login: 'ana', display_name: 'ANA', avatar_url: null, credits: 120, tasks_shipped: 2 } });
311
+ assert.doesNotMatch(JSON.stringify(out), /github_id|"9"/);
312
+ });
313
+
314
+ // ---- 6. the route -----------------------------------------------------------------
315
+
316
+ function serve() {
317
+ const app = express();
318
+ app.use(attachFail);
319
+ app.use('/api/bongos', buildRoutes());
320
+ const server = app.listen(0, '127.0.0.1');
321
+ return new Promise((resolve) => server.once('listening', () => resolve(server)));
322
+ }
323
+ function get(server, path_, headers = {}) {
324
+ return new Promise((resolve, reject) => {
325
+ http.get({ host: '127.0.0.1', port: server.address().port, path: path_, headers }, (res) => {
326
+ let body = '';
327
+ res.on('data', (c) => (body += c));
328
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body, json: body ? JSON.parse(body) : null }));
329
+ }).on('error', reject);
330
+ });
331
+ }
332
+
333
+ const server = await serve();
334
+ const URL_ = '/api/bongos/community/crowns';
335
+
336
+ await t('GET /community/crowns: the three periods; no-store; CORS-open; the same for every viewer', async () => {
337
+ reset();
338
+ engine.history = HISTORY_BEGAN;
339
+ engine.winners.year = { github_id: '9', credits: '3000', tasks_shipped: '12' };
340
+ engine.accounts['9'] = acct('cy');
341
+ const anon = await get(server, URL_);
342
+ assert.equal(anon.status, 200);
343
+ assert.equal(anon.headers['cache-control'], 'no-store');
344
+ assert.equal(anon.headers['access-control-allow-origin'], '*');
345
+ assert.deepEqual(Object.keys(anon.json), ['week', 'month', 'year']);
346
+ assert.equal(anon.json.year.winner.handle, 'cy');
347
+ assert.doesNotMatch(anon.body, /github_id/);
348
+ const signedIn = await get(server, URL_, { Cookie: 'bongos_session=x', Authorization: 'Bearer y' });
349
+ assert.equal(signedIn.body, anon.body, 'the route reads no session');
350
+ });
351
+
352
+ await t('a failure is the 500 envelope, never a stack', async () => {
353
+ reset();
354
+ engine.throwOn = (sql) => HISTORY.test(sql);
355
+ const r = await get(server, URL_);
356
+ assert.equal(r.status, 500);
357
+ assert.equal(r.json.error.code, 'community_crowns_failed');
358
+ assert.doesNotMatch(r.body, /boom/);
359
+ });
360
+
361
+ await t('its own rate-limit budget, not the leaderboard’s or the week’s', () => {
362
+ const { _internals } = buildRoutes;
363
+ assert.equal(_internals.CROWNS_READ_RATE_SCOPE, 'community-crowns-read');
364
+ assert.equal(new Set([_internals.CROWNS_READ_RATE_SCOPE, _internals.READ_RATE_SCOPE, _internals.WEEK_READ_RATE_SCOPE]).size, 3);
365
+ });
366
+
367
+ server.close();
368
+ console.log(`\n${passed} passed, ${failed} failed`);
369
+ if (failed) process.exit(1);
@@ -0,0 +1,164 @@
1
+ // tests/community_crowns_db.mjs — task 1004902 (the builders of the month and the year;
2
+ // real DB).
3
+ //
4
+ // Proves against real SQL what a fake pool cannot (tests/community_crowns.mjs): the month
5
+ // keeps the week's rule (a lifetime brought into the month never counts), the year counts
6
+ // that same row from its first snapshot inside the year and never its lifetime, a row first
7
+ // seen in the year counts from zero, the month is pinned against a later report, a hide
8
+ // and a race, and the year that ended is crowned on the next year's first read.
9
+ //
10
+ // The crowns are global (every eligible builder on the hub), so every fixture period is in
11
+ // 2099: a shared database's real builders have no snapshot there, their delta is zero, and
12
+ // the shipped-work floor keeps them out.
13
+ //
14
+ // Real-DB integration test: self-skips without a reachable Postgres; in the INTEGRATION set
15
+ // (scripts/bongos/run-unit-tests.js) so the DB-free lane never runs it.
16
+ //
17
+ // Run: DATABASE_URL=... node tests/community_crowns_db.mjs
18
+
19
+ import { createRequire } from 'node:module';
20
+ import { readFileSync } from 'node:fs';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { dirname, join } from 'node:path';
23
+ import { strict as assert } from 'node:assert';
24
+ const require = createRequire(import.meta.url);
25
+ const { Pool } = require('pg');
26
+ const { clearCrownMemo } = require('../modules/platform-identity/builder-of-the-week.js');
27
+ const { readCrowns, clearCrownsMemo } = require('../modules/platform-identity/community-crowns.js');
28
+
29
+ const here = dirname(fileURLToPath(import.meta.url));
30
+ const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
31
+ const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
32
+
33
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
34
+ try {
35
+ await pool.query('SELECT 1');
36
+ } catch (e) {
37
+ console.log(`community_crowns_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
38
+ await pool.end().catch(() => {});
39
+ process.exit(0);
40
+ }
41
+
42
+ const M = 90490201, L = 90490202, N = 90490203, Y = 90490204, HIDDEN = 90490205;
43
+ const IDS = [M, L, N, Y, HIDDEN];
44
+ const P1 = 't1004902-p1';
45
+ const NOW = new Date('2099-03-12T10:00:00.000Z');
46
+ const NEXT_YEAR = new Date('2100-01-02T10:00:00.000Z');
47
+ const d = (iso) => new Date(iso);
48
+ const q = (sql, params) => pool.query(sql, params);
49
+
50
+ async function cleanup() {
51
+ await q(`DELETE FROM platform_identity_period_features WHERE period_start >= '2099-01-01'`).catch(() => {});
52
+ await q(`DELETE FROM platform_identity_weekly_features WHERE week_start >= '2099-01-01'`).catch(() => {});
53
+ await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
54
+ await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
55
+ await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
56
+ }
57
+ async function account(id, handle, { hide = false } = {}) {
58
+ await q(
59
+ `INSERT INTO platform_identity_accounts
60
+ (github_id, github_login, display_name, handle, profile_state, account_visibility, hide_stats, terms_accepted_at)
61
+ VALUES ($1, $2, $2, $2, 'active', 'public', $3, now())`,
62
+ [id, handle, hide],
63
+ );
64
+ }
65
+ async function builder(id, history, firstSeen) {
66
+ await q(`INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma, first_seen_at) VALUES ($1, $2, 0, 0, 0, $3)`, [P1, id, firstSeen]);
67
+ for (const [when, credits, ships] of history) {
68
+ await q(
69
+ `INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, $3, $4, 0, $5)`,
70
+ [P1, id, credits, ships, d(when)],
71
+ );
72
+ }
73
+ }
74
+ const recorded = async (period, start) => (await q(
75
+ `SELECT github_id::text AS github_id, credits_delta::int AS credits, ships_delta::int AS ships
76
+ FROM platform_identity_period_features WHERE period = $1 AND period_start = $2::date`,
77
+ [period, start],
78
+ )).rows;
79
+ const read = (now = NOW, p = pool) => { clearCrownMemo(); clearCrownsMemo(); return readCrowns({ now, pool: p }); };
80
+ const who = (w) => w && [w.handle, w.credits, w.tasks_shipped];
81
+
82
+ let pass = 0;
83
+ const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
84
+ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
85
+
86
+ try {
87
+ for (const f of [
88
+ 'platform_identity_001_tables.sql', 'platform_identity_005_builder_activity.sql',
89
+ 'platform_identity_007_profile_consent.sql', 'platform_identity_008_handle.sql',
90
+ 'platform_identity_012_account_privacy.sql', 'platform_identity_019_terms_acceptance.sql',
91
+ 'platform_identity_022_activity_snapshots.sql', 'platform_identity_032_weekly_features.sql',
92
+ 'platform_identity_033_period_features.sql',
93
+ ]) await q(migrationSql(f));
94
+ await cleanup();
95
+
96
+ await account(M, 't1004902-m');
97
+ await account(L, 't1004902-l');
98
+ await account(N, 't1004902-n');
99
+ await account(Y, 't1004902-y');
100
+ await account(HIDDEN, 't1004902-hidden', { hide: true });
101
+
102
+ // Y: history before the year — 400 credits and 3 works in it, all in March.
103
+ await builder(Y, [['2098-12-20T00:00:00Z', 0, 0], ['2099-03-05T00:00:00Z', 400, 3]], d('2098-06-01T00:00:00Z'));
104
+ // M: history before February — 250 credits and 2 works in February. In the year it has
105
+ // no snapshot before 1 January, so it counts from its first one inside (0).
106
+ await builder(M, [['2099-01-20T00:00:00Z', 0, 0], ['2099-02-10T00:00:00Z', 250, 2]], d('2098-06-01T00:00:00Z'));
107
+ // L: a lifetime brought into February — no history before it, seen before first_seen_at
108
+ // existed (NULL). Its first snapshot is 90000; it then earns 500 and two works. The month
109
+ // never counts it; the year counts 500, never 90500.
110
+ await builder(L, [['2099-02-03T00:00:00Z', 90000, 40], ['2099-02-12T00:00:00Z', 90500, 42]], null);
111
+ // N: first seen in the year, so counted from zero: 300 and a work.
112
+ await builder(N, [['2099-02-02T00:00:00Z', 300, 1]], d('2099-02-01T00:00:00Z'));
113
+ // HIDDEN out-earns everyone in both, and the leaderboard may not show them.
114
+ await builder(HIDDEN, [['2098-12-20T00:00:00Z', 0, 0], ['2099-02-05T00:00:00Z', 9000, 9]], d('2098-06-01T00:00:00Z'));
115
+
116
+ // 1 — the month: the week's rule
117
+ const first = await read();
118
+ same([first.month.start, first.month.end, first.month.first_crown], ['2099-02-01', '2099-03-01', null], 'the month is the last full calendar month');
119
+ same(who(first.month.winner), ['t1004902-n', 300, 1], 'the month: a row first seen in it counts from zero; a lifetime brought into it never counts; hidden never wins');
120
+ ok(!('github_id' in first.month.winner), 'github_id never crosses');
121
+ same(await recorded('month', '2099-02-01'), [{ github_id: String(N), credits: 300, ships: 1 }], 'the month is recorded on the first read');
122
+
123
+ // 2 — the year so far: the first snapshot inside the year is the baseline
124
+ same([first.year.start, first.year.since], ['2099-01-01', '2099-01-01'], 'history began before the year');
125
+ same(who(first.year.winner), ['t1004902-l', 500, 2], 'the year counts L from its first snapshot inside it — 500, not its 90500 lifetime — and that beats Y’s 400 and N’s 300');
126
+ same(await recorded('year', '2099-01-01'), [], 'a year so far is never written');
127
+
128
+ // 3 — pinned: a later report into the past month, or a builder who goes public, changes nothing
129
+ await q(`INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, 99999, 50, 0, $3)`, [P1, M, d('2099-02-20T00:00:00Z')]);
130
+ await q(`UPDATE platform_identity_accounts SET hide_stats = false WHERE github_id = $1`, [HIDDEN]);
131
+ same(who((await read()).month.winner), ['t1004902-n', 300, 1], 'the first decision stands');
132
+ await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [HIDDEN]);
133
+
134
+ // 4 — a winner who hides is not shown, and their month goes to nobody else
135
+ await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [N]);
136
+ same((await read()).month.winner, null, 'a hidden winner is not drawn');
137
+ same(await recorded('month', '2099-02-01'), [{ github_id: String(N), credits: 300, ships: 1 }], 'and the row is never re-crowned');
138
+ await q(`UPDATE platform_identity_accounts SET hide_stats = false WHERE github_id = $1`, [N]);
139
+
140
+ // 5 — the year that ended, crowned on the next year's first read by the year's rule
141
+ // (M's late February report now leads it), and shown as last
142
+ const later = await read(NEXT_YEAR);
143
+ same(await recorded('year', '2099-01-01'), [{ github_id: String(M), credits: 99999, ships: 50 }], 'the year is recorded on the next year’s first read');
144
+ same(later.year.last && [later.year.last.start, ...who(later.year.last.winner)], ['2099-01-01', 't1004902-m', 99999, 50], 'and shown as last year');
145
+ same(later.year.winner, null, 'nobody has earned in the new year yet');
146
+
147
+ // 6 — two hub processes crowning the same month at once: one row, no error
148
+ const other = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
149
+ try {
150
+ clearCrownMemo();
151
+ clearCrownsMemo();
152
+ const at = d('2099-05-03T00:00:00Z');
153
+ const both = await Promise.all([readCrowns({ now: at, pool }), readCrowns({ now: at, pool: other })]);
154
+ same(both.map((b) => b.month.winner), [null, null], 'both answer');
155
+ same((await recorded('month', '2099-04-01')).length, 1, 'one row: ON CONFLICT DO NOTHING');
156
+ } finally { await other.end().catch(() => {}); }
157
+
158
+ console.log(`\ncommunity_crowns_db.mjs: ${pass} passed`);
159
+ } finally {
160
+ clearCrownMemo();
161
+ clearCrownsMemo();
162
+ await cleanup();
163
+ await pool.end().catch(() => {});
164
+ }