@bongos/core 1.19.1025 → 1.19.1027

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 (38) hide show
  1. package/.bongos-core.json +88 -33
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +40 -0
  4. package/clients/bongos-client/index.cjs +40 -0
  5. package/clients/bongos-client/index.d.ts +63 -0
  6. package/clients/bongos-client/index.mjs +40 -0
  7. package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +4 -2
  8. package/docs/api/openapi.json +1361 -151
  9. package/docs/api-reference.md +32 -4
  10. package/docs/copy-inventory.md +6 -6
  11. package/docs/copy-registry.json +10 -20
  12. package/docs/module-api-changelog.md +4 -0
  13. package/modules/hall-ui/public/goal-map.html +1 -1
  14. package/modules/platform-identity/community-leaderboard.js +235 -0
  15. package/modules/platform-identity/connections.js +1 -0
  16. package/modules/platform-identity/guilds.js +705 -0
  17. package/modules/platform-identity/migrations/platform_identity_025_guilds.sql +100 -0
  18. package/modules/platform-identity/module.json +1 -1
  19. package/modules/platform-identity/read-rate-limit.js +62 -0
  20. package/modules/platform-identity/routes/community-leaderboard.js +69 -0
  21. package/modules/platform-identity/routes/community-search.js +4 -31
  22. package/modules/platform-identity/routes/guilds-public.js +57 -0
  23. package/modules/platform-identity/routes/guilds.js +393 -0
  24. package/modules/platform-identity/routes/projects.js +1 -1
  25. package/modules/platform-identity/terms-acceptance.js +14 -5
  26. package/modules/public-landing/public/terms.html +3 -3
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/scripts/gds/run-unit-tests.js +10 -0
  30. package/src/module-api.js +1 -1
  31. package/tests/community_leaderboard_db.mjs +202 -0
  32. package/tests/community_leaderboard_route.mjs +414 -0
  33. package/tests/goal_map_page.mjs +8 -0
  34. package/tests/guilds_api.mjs +521 -0
  35. package/tests/guilds_db.mjs +343 -0
  36. package/tests/own_scope_guard.mjs +5 -0
  37. package/tests/terms_acceptance.mjs +74 -28
  38. package/tests/terms_grandfather_db.mjs +2 -2
@@ -0,0 +1,202 @@
1
+ // tests/community_leaderboard_db.mjs — task 1002978 (community spec D2/D3; real DB).
2
+ //
3
+ // Proves the windowed leaderboard's arithmetic and eligibility against real
4
+ // SQL. The window math is a LATERAL "last snapshot at-or-before the window
5
+ // start" probe per (project, builder) plus a cross-project sum, and a fake pool
6
+ // can only prove the statement's SHAPE (tests/community_leaderboard_route.mjs);
7
+ // only a real Postgres can prove which snapshot becomes the baseline, that a
8
+ // missing baseline counts as zero, and that the privacy matrix really filters.
9
+ //
10
+ // Every fixture carries a discipline no real builder holds, and every read
11
+ // filters on it, so a shared database's own builders never enter the board
12
+ // under test.
13
+ //
14
+ // Real-DB integration test: self-skips without a reachable Postgres; in the
15
+ // INTEGRATION set (scripts/gds/run-unit-tests.js) so the DB-free lane never runs it.
16
+ //
17
+ // Run: DATABASE_URL=... node tests/community_leaderboard_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 { listLeaderboard, clearTotalsMemo } = require('../modules/platform-identity/community-leaderboard.js');
27
+
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
30
+ const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
31
+
32
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
33
+ try {
34
+ await pool.query('SELECT 1');
35
+ } catch (e) {
36
+ console.log(`community_leaderboard_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
37
+ await pool.end().catch(() => {});
38
+ process.exit(0);
39
+ }
40
+
41
+ // High-numbered fixture ids so a shared DB can never collide with real data.
42
+ const A = 90297801, B = 90297802, C = 90297803, D = 90297804, OPTOUT = 90297805;
43
+ const HIDDEN = 90297806, PRIVATE = 90297807, PROV = 90297808, LEGACY = 90297809, BIG = 90297810;
44
+ const IDS = [A, B, C, D, OPTOUT, HIDDEN, PRIVATE, PROV, LEGACY, BIG];
45
+ const P1 = 't1002978-p1', P2 = 't1002978-p2';
46
+ const TAG = 't1002978-board'; // every fixture holds it; every read filters on it
47
+ const TAG_ART = 't1002978-art'; // held by A on P2 only — the union filter
48
+ const TAG_BIG = 't1002978-big'; // BIG's own board, so its totals stay out of the others
49
+ const INT4_MAX = 2147483647;
50
+ const NOW = new Date('2026-06-01T12:00:00.000Z');
51
+ const DAY = 24 * 60 * 60 * 1000;
52
+ const ago = (days, extraMs = 0) => new Date(NOW.getTime() - days * DAY + extraMs);
53
+ const q = (sql, params) => pool.query(sql, params);
54
+
55
+ async function cleanup() {
56
+ await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
57
+ await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
58
+ await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
59
+ }
60
+
61
+ async function account(id, handle, { state = 'active', visibility = 'public', hide = false, accepted = true, reach = null } = {}) {
62
+ await q(
63
+ `INSERT INTO platform_identity_accounts
64
+ (github_id, github_login, display_name, handle, profile_state, account_visibility, hide_stats, recruiter_discoverable, terms_accepted_at)
65
+ VALUES ($1, $2, $2, $2, $3, $4, $5, $6, CASE WHEN $7::boolean THEN now() END)`,
66
+ [id, handle, state, visibility, hide, reach, accepted],
67
+ );
68
+ }
69
+ async function activity(id, client, credits, ships, karma, disciplines = [TAG]) {
70
+ await q(
71
+ `INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma, disciplines)
72
+ VALUES ($1, $2, $3, $4, $5, $6)`,
73
+ [client, id, credits, ships, karma, disciplines],
74
+ );
75
+ }
76
+ async function snapshot(id, client, credits, ships, karma, at) {
77
+ await q(
78
+ `INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
79
+ VALUES ($1, $2, $3, $4, $5, $6)`,
80
+ [client, id, credits, ships, karma, at],
81
+ );
82
+ }
83
+
84
+ const board = async (window, discipline = TAG) =>
85
+ (await listLeaderboard({ window, discipline, limit: 100, now: NOW, pool })).results;
86
+ const row = (results, handle) => results.find((r) => r.handle === handle);
87
+ const totals = (r) => r && [r.credits, r.tasks_shipped, r.karma];
88
+
89
+ let pass = 0;
90
+ const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
91
+ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
92
+
93
+ try {
94
+ // The module's own migrations (idempotent) — every column the NUMBERS
95
+ // predicate and the board read: 007 consent + hide, 008 handle, 012 privacy,
96
+ // 019 the terms half of consent, 005/022 the two tables the window reads.
97
+ for (const f of [
98
+ 'platform_identity_001_tables.sql', 'platform_identity_005_builder_activity.sql',
99
+ 'platform_identity_007_profile_consent.sql', 'platform_identity_008_handle.sql',
100
+ 'platform_identity_012_account_privacy.sql', 'platform_identity_019_terms_acceptance.sql',
101
+ 'platform_identity_022_activity_snapshots.sql',
102
+ ]) await q(migrationSql(f));
103
+ await cleanup();
104
+
105
+ await account(A, 't1002978-a');
106
+ await account(B, 't1002978-b');
107
+ await account(C, 't1002978-c');
108
+ await account(D, 't1002978-d');
109
+ await account(OPTOUT, 't1002978-optout', { reach: false });
110
+ await account(HIDDEN, 't1002978-hidden', { hide: true });
111
+ await account(PRIVATE, 't1002978-private', { visibility: 'private' });
112
+ await account(PROV, 't1002978-prov', { state: 'provisional', accepted: false });
113
+ await account(LEGACY, 't1002978-legacy', { accepted: false }); // active, but never accepted the terms
114
+
115
+ // A: two projects. P1 has history; P2 has none (a builder not seen since
116
+ // snapshots began — the day-one case the spec accepts).
117
+ await activity(A, P1, 200, 9, 3);
118
+ await snapshot(A, P1, 100, 5, 2, ago(40));
119
+ await snapshot(A, P1, 150, 7, 3, ago(10));
120
+ await snapshot(A, P1, 200, 9, 3, ago(2));
121
+ await activity(A, P2, 30, 1, 0, [TAG, TAG_ART]);
122
+ // B: unchanged for the last 20 days.
123
+ await activity(B, P1, 500, 10, 1);
124
+ await snapshot(B, P1, 500, 10, 1, ago(20));
125
+ // C: a snapshot EXACTLY at the 7d start, and one a second after it.
126
+ await activity(C, P1, 45, 2, 0);
127
+ await snapshot(C, P1, 40, 2, 0, ago(7));
128
+ await snapshot(C, P1, 42, 2, 0, ago(7, 1000));
129
+ // D: lost credits inside the week (a clawback).
130
+ await activity(D, P1, 90, 3, 5);
131
+ await snapshot(D, P1, 100, 3, 5, ago(10));
132
+ await activity(OPTOUT, P1, 60, 2, 0);
133
+ // Out of the matrix — every one would top the board if it leaked.
134
+ for (const id of [HIDDEN, PRIVATE, PROV, LEGACY]) await activity(id, P1, 9000, 90, 9);
135
+ await account(BIG, 't1002978-big');
136
+ await activity(BIG, P1, INT4_MAX, 1, 0, [TAG_BIG]);
137
+ await activity(BIG, P2, INT4_MAX, 1, 0, [TAG_BIG]);
138
+ await snapshot(BIG, P1, -INT4_MAX, 0, 0, ago(10));
139
+
140
+ // 1 — all-time: the live rows summed across projects, no baseline.
141
+ const all = await board('all');
142
+ same(all.map((r) => r.handle), ['t1002978-b', 't1002978-a', 't1002978-d', 't1002978-optout', 't1002978-c'],
143
+ 'all-time ranks by summed credits');
144
+ same(totals(row(all, 't1002978-a')), [230, 10, 3], 'all-time sums a builder across both projects');
145
+ same(all.map((r) => r.place), [1, 2, 3, 4, 5], 'place numbers the board from 1');
146
+
147
+ // 2 — the privacy matrix, against the real predicate.
148
+ for (const window of ['all', '7d', '30d', '365d']) {
149
+ const handles = (await board(window)).map((r) => r.handle);
150
+ for (const h of ['t1002978-hidden', 't1002978-private', 't1002978-prov', 't1002978-legacy']) {
151
+ assert.ok(!handles.includes(h), `${h} leaked onto the ${window} board`);
152
+ }
153
+ }
154
+ pass++; console.log(' ok - hidden, private, provisional and never-accepted builders are absent from every window');
155
+ ok(row(all, 't1002978-optout'), 'a builder who opted out of RECRUITING still appears (spec D3)');
156
+
157
+ // 3 — 7d: live minus the last snapshot at-or-before the start, summed.
158
+ const week = await board('7d');
159
+ same(totals(row(week, 't1002978-a')), [80, 3, 0],
160
+ '7d: P1 counts from the 10-day-old snapshot, P2 (no history) counts in full');
161
+ same(totals(row(week, 't1002978-c')), [5, 0, 0],
162
+ '7d: a snapshot exactly at the window start IS the baseline; one a second later is not');
163
+ same(totals(row(week, 't1002978-d')), [-10, 0, 0], '7d: a loss inside the window is a negative delta, not hidden');
164
+ ok(!row(week, 't1002978-b'), '7d: a builder with no movement in the window is left off the board');
165
+ same(week.map((r) => r.handle), ['t1002978-a', 't1002978-optout', 't1002978-c', 't1002978-d'],
166
+ '7d ranks by the window delta, a loss last');
167
+
168
+ // 4 — 30d and 365d.
169
+ const month = await board('30d');
170
+ same(totals(row(month, 't1002978-a')), [130, 5, 1], '30d: P1 counts from the 40-day-old snapshot');
171
+ same(totals(row(month, 't1002978-b')), [500, 10, 1],
172
+ '30d: a builder whose first snapshot is inside the window has no baseline, so counts in full');
173
+ const year = await board('365d');
174
+ same(totals(row(year, 't1002978-a')), [230, 10, 3], '365d: no snapshot is old enough, so everything counts');
175
+
176
+ // 5 — the discipline filter is the builder's union across projects.
177
+ const art = await board('all', TAG_ART);
178
+ same(art.map((r) => r.handle), ['t1002978-a'], 'a discipline held on ONE project admits the builder');
179
+ same(totals(art[0]), [230, 10, 3], 'the filter picks builders, not projects — totals stay whole');
180
+ same(art[0].disciplines, [TAG_ART, TAG].sort(), 'disciplines is the de-duplicated union');
181
+
182
+ // 6 — instance-reported int4 totals cannot overflow the board into a 500.
183
+ same(totals((await board('all', TAG_BIG))[0]), [2 * INT4_MAX, 2, 0], 'all-time: two int4-max projects sum past int4');
184
+ same(totals((await board('7d', TAG_BIG))[0]), [3 * INT4_MAX, 2, 0], '7d: int4-max minus a negative baseline does not overflow');
185
+
186
+ // 7 — paging keeps the board's own place numbers.
187
+ const page2 = (await listLeaderboard({ window: 'all', discipline: TAG, limit: 2, offset: 2, now: NOW, pool })).results;
188
+ same(page2.map((r) => [r.place, r.handle]), [[3, 't1002978-d'], [4, 't1002978-optout']], 'offset pages keep place');
189
+
190
+ // 8 — the memoized path (no `now`) against real SQL: hiding beats the memo.
191
+ clearTotalsMemo();
192
+ const memo = async () => (await listLeaderboard({ window: 'all', discipline: TAG, limit: 100, pool })).results;
193
+ ok(row(await memo(), 't1002978-a'), 'the memoized all-time board lists A');
194
+ await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [A]);
195
+ ok(!row(await memo(), 't1002978-a'), 'A hides inside the memo TTL and is gone on the very next request');
196
+ clearTotalsMemo();
197
+
198
+ console.log(`\ncommunity_leaderboard_db.mjs: ${pass} passed`);
199
+ } finally {
200
+ await cleanup();
201
+ await pool.end().catch(() => {});
202
+ }
@@ -0,0 +1,414 @@
1
+ // tests/community_leaderboard_route.mjs
2
+ //
3
+ // Guards GET /community/leaderboard — the PUBLIC windowed leaderboard (D3,
4
+ // docs/specs/bongos-v1-community-tab.md; task 1002978). DB-free. A fake pool
5
+ // asserts that BOTH real statements — the memoized window totals and the
6
+ // per-request eligibility read — carry the shared NUMBERS predicate
7
+ // (rollupVisibleSql('pa'), imported from account-visibility.js) and NOT the
8
+ // recruiting directory's (scoutingListableSql). Which rows the fake returns is
9
+ // a fixture simulation using the same JS twin (isRollupVisible); the window
10
+ // arithmetic and the matrix against real SQL are tests/community_leaderboard_db.mjs.
11
+ //
12
+ // What this pins:
13
+ // 1. the pure helpers — windows, windowStart, the limit clamp.
14
+ // 2. the statements: predicate present in both, recruiting predicate absent,
15
+ // $1 = the window start (null for all-time).
16
+ // 3. the memo: one totals computation per window per TTL however many
17
+ // requests, a builder who hides mid-TTL gone on the very next request, a
18
+ // failed computation never cached.
19
+ // 4. the route: 400s on a bad window or oversized discipline, the default
20
+ // window, ranking + `place` numbering across pages, github_id never
21
+ // crosses, the same body with or without credentials, no-store, the 500
22
+ // path, and the per-IP rate limit.
23
+ //
24
+ // Run: node tests/community_leaderboard_route.mjs
25
+
26
+ import { strict as assert } from 'node:assert';
27
+ import { createRequire } from 'node:module';
28
+ import http from 'node:http';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const express = require('express');
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+ async function t(name, fn) {
36
+ try { await fn(); passed++; console.log(` ok ${name}`); }
37
+ catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
38
+ }
39
+
40
+ const {
41
+ isRollupVisible, rollupVisibleSql, scoutingListableSql,
42
+ } = require('../modules/platform-identity/account-visibility.js');
43
+
44
+ // ---- fake engine ------------------------------------------------------------
45
+
46
+ const ACCEPTED_AT = '2026-08-29T00:00:00.000Z';
47
+ const account = (id, handle, extra = {}) => ({
48
+ github_id: id, handle, github_login: handle.replace(/-/g, ''), display_name: handle, avatar_url: null,
49
+ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false,
50
+ recruiter_discoverable: null, ...extra,
51
+ });
52
+
53
+ // Deliberately NOT in rank order — the module ranks, the fake does not.
54
+ const BOARD = [
55
+ { acct: account('205', 'third-builder'), credits: 50, tasks_shipped: 1, karma: 0, disciplines: ['artist', 'engineer'] },
56
+ { acct: account('201', 'hidden-builder', { hide_stats: true }), credits: 250, tasks_shipped: 8, karma: 1, disciplines: ['artist'] },
57
+ { acct: account('200', 'top-builder'), credits: 300, tasks_shipped: 9, karma: 4, disciplines: ['engineer'] },
58
+ { acct: account('202', 'private-builder', { account_visibility: 'private' }), credits: 240, tasks_shipped: 7, karma: 1, disciplines: ['engineer'] },
59
+ { acct: account('203', 'prov-builder', { profile_state: 'provisional', terms_accepted_at: null }), credits: 230, tasks_shipped: 7, karma: 1, disciplines: ['engineer'] },
60
+ // Opted out of RECRUITING only — spec D3: that opt-out does not touch a leaderboard.
61
+ { acct: account('204', 'optout-builder', { recruiter_discoverable: false }), credits: 120, tasks_shipped: 3, karma: 0, disciplines: ['ui'] },
62
+ // Ties top-builder on every number: the handle decides, so it ranks second.
63
+ { acct: account('206', 'twin-builder'), credits: 300, tasks_shipped: 9, karma: 4, disciplines: ['ui'] },
64
+ ];
65
+ const byId = (id) => BOARD.find((r) => r.acct.github_id === String(id));
66
+
67
+ const isTotals = (sql) => /platform_identity_activity_snapshots/.test(sql);
68
+ const engine = {
69
+ calls: [],
70
+ throwOnQuery: null,
71
+ query: async (text, params = []) => {
72
+ const sql = String(text);
73
+ engine.calls.push({ sql, params });
74
+ if (engine.throwOnQuery) throw engine.throwOnQuery;
75
+ if (isTotals(sql)) {
76
+ // node-postgres hands int8 back as a string — the shape the real sums arrive in.
77
+ return {
78
+ rows: BOARD.filter((r) => isRollupVisible(r.acct)).map((r) => ({
79
+ github_id: r.acct.github_id, credits: String(r.credits), tasks_shipped: String(r.tasks_shipped),
80
+ karma: String(r.karma), disciplines: r.disciplines,
81
+ })),
82
+ };
83
+ }
84
+ assert.match(sql, /github_id = ANY\(\$1::bigint\[\]\)/, 'the only other statement is the eligibility read');
85
+ const [ids] = params;
86
+ return {
87
+ rows: ids.map(byId).filter((r) => r && isRollupVisible(r.acct)).map((r) => ({
88
+ github_id: r.acct.github_id, handle: r.acct.handle, github_login: r.acct.github_login,
89
+ display_name: r.acct.display_name, avatar_url: r.acct.avatar_url,
90
+ })),
91
+ };
92
+ },
93
+ };
94
+ const totalsCalls = () => engine.calls.filter((c) => isTotals(c.sql));
95
+ const lastTotalsCall = () => totalsCalls()[totalsCalls().length - 1];
96
+
97
+ // Pin the module doorway (ADR 0083) BEFORE the modules load.
98
+ const moduleApiPath = require.resolve('../src/module-api.js');
99
+ require.cache[moduleApiPath] = {
100
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
101
+ exports: {
102
+ pool: engine,
103
+ corsPublicGet: () => (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); next(); },
104
+ logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
105
+ },
106
+ };
107
+
108
+ const lb = require('../modules/platform-identity/community-leaderboard.js');
109
+ const { createReadRateLimit } = require('../modules/platform-identity/read-rate-limit.js');
110
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
111
+ const buildLeaderboardRoutes = require('../modules/platform-identity/routes/community-leaderboard.js');
112
+
113
+ // ---- pure helpers -------------------------------------------------------------
114
+
115
+ await t('the four spec windows, and nothing else', () => {
116
+ assert.deepEqual([...lb.WINDOWS], ['7d', '30d', '365d', 'all']);
117
+ for (const w of lb.WINDOWS) assert.ok(lb.isWindow(w), w);
118
+ for (const w of ['1d', '7D', '', 'toString', '__proto__', undefined, ['7d']]) assert.ok(!lb.isWindow(w), String(w));
119
+ assert.equal(lb.DEFAULT_WINDOW, 'all');
120
+ });
121
+
122
+ await t('windowStart: exactly N days before now; all-time has no start', () => {
123
+ const now = new Date('2026-09-25T12:00:00.000Z');
124
+ assert.equal(lb.windowStart('7d', now).toISOString(), '2026-09-18T12:00:00.000Z');
125
+ assert.equal(lb.windowStart('30d', now).toISOString(), '2026-08-26T12:00:00.000Z');
126
+ assert.equal(lb.windowStart('365d', now).toISOString(), '2025-09-25T12:00:00.000Z');
127
+ assert.equal(lb.windowStart('all', now), null);
128
+ });
129
+
130
+ await t('clampLimit: default, cap, garbage', () => {
131
+ assert.equal(lb.clampLimit(undefined), lb.DEFAULT_LIMIT);
132
+ assert.equal(lb.clampLimit('x'), lb.DEFAULT_LIMIT);
133
+ assert.equal(lb.clampLimit(0), lb.DEFAULT_LIMIT);
134
+ assert.equal(lb.clampLimit(10), 10);
135
+ assert.equal(lb.clampLimit(99999), lb.MAX_LIMIT);
136
+ });
137
+
138
+ await t('listLeaderboard refuses an unknown window rather than guessing one', async () => {
139
+ await assert.rejects(() => lb.listLeaderboard({ window: '2d', pool: engine }), /unknown window/);
140
+ });
141
+
142
+ // ---- the statements -------------------------------------------------------------
143
+
144
+ await t('both statements join rollupVisibleSql — the NUMBERS predicate — and neither the recruiting one', async () => {
145
+ lb.clearTotalsMemo();
146
+ engine.calls = [];
147
+ await lb.listLeaderboard({ window: 'all', pool: engine });
148
+ assert.equal(engine.calls.length, 2, 'the window totals, then the eligibility read');
149
+ for (const { sql } of engine.calls) {
150
+ assert.ok(sql.includes(rollupVisibleSql('pa')), "must join rollupVisibleSql('pa') — active + public + not hidden");
151
+ assert.ok(!sql.includes(scoutingListableSql('pa')),
152
+ 'must NOT join scoutingListableSql — the recruiting opt-out governs the directory only (spec D3)');
153
+ }
154
+ const { sql } = lastTotalsCall();
155
+ assert.match(sql, /s\.reported_at <= \$1::timestamptz/, 'the baseline is the last snapshot AT-OR-BEFORE the window start');
156
+ assert.match(sql, /COALESCE\(b\.credits, 0\)/, 'a missing baseline counts as zero (spec D2)');
157
+ assert.match(sql, /sum\(credits\)::bigint/, 'sums are bigint, so an extreme report cannot overflow the board');
158
+ });
159
+
160
+ await t('all-time binds no window start; a window binds exactly its start', async () => {
161
+ const now = new Date('2026-09-25T12:00:00.000Z');
162
+ await lb.listLeaderboard({ window: 'all', now, pool: engine });
163
+ assert.equal(lastTotalsCall().params[0], null);
164
+ await lb.listLeaderboard({ window: '7d', now, pool: engine });
165
+ assert.equal(lastTotalsCall().params[0].toISOString(), '2026-09-18T12:00:00.000Z');
166
+ });
167
+
168
+ await t('ranked credits → ships → karma → handle, with a trimmed discipline filter and clamped paging', async () => {
169
+ const all = await lb.listLeaderboard({ window: 'all', pool: engine });
170
+ assert.deepEqual(all.results.map((r) => r.handle), ['top-builder', 'twin-builder', 'optout-builder', 'third-builder']);
171
+ const eng = await lb.listLeaderboard({ window: 'all', discipline: ' engineer ', limit: 5000, offset: 1, pool: engine });
172
+ assert.deepEqual(eng.results.map((r) => [r.place, r.handle]), [[2, 'third-builder']]);
173
+ assert.equal(eng.limit, lb.MAX_LIMIT);
174
+ });
175
+
176
+ // ---- the memo -------------------------------------------------------------------
177
+
178
+ await t('many requests for one window compute its totals once; another window computes its own', async () => {
179
+ lb.clearTotalsMemo();
180
+ engine.calls = [];
181
+ for (let i = 0; i < 5; i++) await lb.listLeaderboard({ window: '30d', pool: engine });
182
+ assert.equal(totalsCalls().length, 1, 'the expensive half ran once for five requests');
183
+ assert.equal(engine.calls.length - totalsCalls().length, 5, 'the eligibility read ran on every request');
184
+ await lb.listLeaderboard({ window: '7d', pool: engine });
185
+ assert.equal(totalsCalls().length, 2);
186
+ });
187
+
188
+ await t('a builder who hides mid-TTL leaves the board on the very next request (hide beats the memo)', async () => {
189
+ lb.clearTotalsMemo();
190
+ const top = byId('200');
191
+ try {
192
+ const before = await lb.listLeaderboard({ window: 'all', pool: engine });
193
+ assert.ok(before.results.some((r) => r.handle === 'top-builder'));
194
+ const computed = totalsCalls().length;
195
+ top.acct.hide_stats = true;
196
+ const after = await lb.listLeaderboard({ window: 'all', pool: engine });
197
+ assert.equal(totalsCalls().length, computed, 'served from the memo — the totals were not recomputed');
198
+ assert.ok(!after.results.some((r) => r.handle === 'top-builder'), 'yet the hidden builder is gone');
199
+ assert.deepEqual(after.results.map((r) => r.place), [1, 2, 3], 'and place closes the gap');
200
+ } finally {
201
+ top.acct.hide_stats = false;
202
+ }
203
+ });
204
+
205
+ await t('a failed totals computation is not cached — the next request recomputes', async () => {
206
+ lb.clearTotalsMemo();
207
+ engine.throwOnQuery = new Error('transient');
208
+ await assert.rejects(() => lb.listLeaderboard({ window: '365d', pool: engine }), /transient/);
209
+ engine.throwOnQuery = null;
210
+ const ok = await lb.listLeaderboard({ window: '365d', pool: engine });
211
+ assert.ok(ok.results.length > 0);
212
+ });
213
+
214
+ // ---- the route ------------------------------------------------------------------
215
+
216
+ function buildApp() {
217
+ const app = express();
218
+ app.use(attachFail);
219
+ app.use('/api/bongos', buildLeaderboardRoutes());
220
+ return app;
221
+ }
222
+
223
+ function get(port, path, headers = {}) {
224
+ return new Promise((resolve, reject) => {
225
+ const req = http.request({ host: '127.0.0.1', port, method: 'GET', path, headers }, (res) => {
226
+ const chunks = [];
227
+ res.on('data', (c) => chunks.push(c));
228
+ res.on('end', () => {
229
+ const text = Buffer.concat(chunks).toString('utf8');
230
+ let json = null; try { json = JSON.parse(text); } catch (_) {}
231
+ resolve({ status: res.statusCode, json, text, headers: res.headers });
232
+ });
233
+ });
234
+ req.on('error', reject);
235
+ req.end();
236
+ });
237
+ }
238
+
239
+ const server = await new Promise((resolve) => { const s = buildApp().listen(0, '127.0.0.1', () => resolve(s)); });
240
+ const port = server.address().port;
241
+ const PATH = '/api/bongos/community/leaderboard';
242
+
243
+ lb.clearTotalsMemo();
244
+
245
+ await t('no window → the all-time board, ranked and numbered by place', async () => {
246
+ const r = await get(port, PATH);
247
+ assert.equal(r.status, 200);
248
+ assert.equal(r.json.window, 'all');
249
+ assert.equal(r.json.since, null);
250
+ assert.deepEqual(r.json.results.map((x) => x.handle), ['top-builder', 'twin-builder', 'optout-builder', 'third-builder']);
251
+ assert.deepEqual(r.json.results.map((x) => x.place), [1, 2, 3, 4]);
252
+ assert.equal(r.json.count, 4);
253
+ assert.deepEqual(Object.keys(r.json.results[0]).sort(),
254
+ ['avatar_url', 'credits', 'disciplines', 'display_name', 'github_login', 'handle', 'karma', 'place', 'tasks_shipped']);
255
+ const top = r.json.results[0];
256
+ assert.deepEqual([top.credits, top.tasks_shipped, top.karma], [300, 9, 4], 'int8 totals serialize as JSON numbers, not strings');
257
+ });
258
+
259
+ await t('hidden, private and provisional builders never appear; a recruiting opt-out does', async () => {
260
+ const r = await get(port, PATH);
261
+ const handles = r.json.results.map((x) => x.handle);
262
+ for (const h of ['hidden-builder', 'private-builder', 'prov-builder']) assert.ok(!handles.includes(h), h);
263
+ assert.ok(handles.includes('optout-builder'), 'recruiter_discoverable=false does not remove a builder from the leaderboard');
264
+ });
265
+
266
+ await t('a window echoes its start', async () => {
267
+ const before = Date.now();
268
+ const r = await get(port, `${PATH}?window=7d`);
269
+ assert.equal(r.status, 200);
270
+ assert.equal(r.json.window, '7d');
271
+ const since = Date.parse(r.json.since);
272
+ assert.ok(Math.abs((before - 7 * 86400000) - since) < 5000, 'since is ~7 days before the request');
273
+ });
274
+
275
+ await t('a discipline filter narrows the board and is echoed', async () => {
276
+ const r = await get(port, `${PATH}?window=30d&discipline=artist`);
277
+ assert.equal(r.status, 200);
278
+ assert.equal(r.json.discipline, 'artist');
279
+ assert.deepEqual(r.json.results.map((x) => x.handle), ['third-builder'],
280
+ 'the hidden builder holds artist too, and still does not appear');
281
+ });
282
+
283
+ await t('place counts from the page offset, and page echoes the clamped values', async () => {
284
+ const r = await get(port, `${PATH}?limit=1&offset=1`);
285
+ assert.equal(r.status, 200);
286
+ assert.deepEqual(r.json.results.map((x) => [x.place, x.handle]), [[2, 'twin-builder']]);
287
+ assert.deepEqual(r.json.page, { limit: 1, offset: 1 });
288
+ const deep = await get(port, `${PATH}?offset=${lb.MAX_OFFSET + 50}&limit=100000`);
289
+ assert.deepEqual(deep.json.page, { limit: lb.MAX_LIMIT, offset: lb.MAX_OFFSET });
290
+ });
291
+
292
+ await t('github_id never crosses the wire', async () => {
293
+ const r = await get(port, PATH);
294
+ assert.ok(!r.text.includes('github_id'), 'the internal join key must not appear');
295
+ assert.ok(!r.text.includes('"200"'));
296
+ });
297
+
298
+ await t('the response is identical for an anonymous and a credentialed viewer', async () => {
299
+ const anon = await get(port, `${PATH}?window=365d`);
300
+ const signedIn = await get(port, `${PATH}?window=365d`, { cookie: 'otb_session=abc', authorization: 'Bearer xyz' });
301
+ assert.equal(signedIn.status, 200);
302
+ const strip = (j) => ({ ...j, since: null });
303
+ assert.deepEqual(strip(signedIn.json), strip(anon.json));
304
+ });
305
+
306
+ await t('an unknown window → 400 bad_window, naming the allowed values', async () => {
307
+ for (const qs of ['window=1d', 'window=7D', 'window=toString', 'window=7d&window=30d']) {
308
+ const r = await get(port, `${PATH}?${qs}`);
309
+ assert.equal(r.status, 400, qs);
310
+ assert.equal(r.json.error.code, 'bad_window');
311
+ assert.match(r.json.error.message, /7d, 30d, 365d, all/);
312
+ }
313
+ });
314
+
315
+ await t('an oversized discipline → 400 discipline_too_long', async () => {
316
+ const r = await get(port, `${PATH}?discipline=${'a'.repeat(lb.MAX_DISCIPLINE_CHARS + 1)}`);
317
+ assert.equal(r.status, 400);
318
+ assert.equal(r.json.error.code, 'discipline_too_long');
319
+ });
320
+
321
+ await t('Cache-Control: no-store (hide must beat the cache, ADR 0171 D5) and CORS-open', async () => {
322
+ const r = await get(port, PATH);
323
+ assert.equal(r.headers['cache-control'], 'no-store');
324
+ assert.equal(r.headers['access-control-allow-origin'], '*');
325
+ });
326
+
327
+ await t('a query failure → 500 leaderboard_failed, with no internal detail in the body', async () => {
328
+ engine.throwOnQuery = new Error('DO-NOT-LEAK connection detail');
329
+ try {
330
+ const r = await get(port, PATH);
331
+ assert.equal(r.status, 500);
332
+ assert.equal(r.json.error.code, 'leaderboard_failed');
333
+ assert.ok(!r.text.includes('DO-NOT-LEAK'));
334
+ } finally {
335
+ engine.throwOnQuery = null;
336
+ }
337
+ });
338
+
339
+ server.close();
340
+
341
+ // ---- the rate limit -------------------------------------------------------------
342
+
343
+ function mkRes() {
344
+ const r = { _status: 200, _json: null, _headers: {} };
345
+ r.set = (k, v) => { r._headers[k] = v; return r; };
346
+ r.status = (s) => { r._status = s; return r; };
347
+ r.json = (j) => { r._json = j; return r; };
348
+ attachFail({}, r, () => {});
349
+ return r;
350
+ }
351
+
352
+ await t('the route mounts a read cap sized by its own constants', () => {
353
+ const { READ_RATE_LIMIT, READ_RATE_SCOPE } = buildLeaderboardRoutes._internals;
354
+ assert.ok(Number.isInteger(READ_RATE_LIMIT) && READ_RATE_LIMIT > 0);
355
+ assert.equal(READ_RATE_SCOPE, 'community-leaderboard-read');
356
+ });
357
+
358
+ await t('createReadRateLimit: allows up to the cap, then 429 with Retry-After and the scope', () => {
359
+ const limit = createReadRateLimit({ scope: 'test-read', limit: 3 });
360
+ const req = { ip: '203.0.113.90' };
361
+ let allowed = 0;
362
+ for (let i = 0; i < 3; i++) limit(req, mkRes(), () => { allowed++; });
363
+ assert.equal(allowed, 3);
364
+ const res = mkRes();
365
+ let nexted = false;
366
+ limit(req, res, () => { nexted = true; });
367
+ assert.equal(nexted, false);
368
+ assert.equal(res._status, 429);
369
+ assert.equal(res._json.error.code, 'rate_limited');
370
+ assert.ok(Number(res._headers['Retry-After']) >= 1);
371
+ assert.equal(res._headers['X-RateLimit-Scope'], 'test-read');
372
+ assert.equal(res._headers['X-RateLimit-Window-Seconds'], '60', 'the core limiter headers, one for one');
373
+ });
374
+
375
+ await t('createReadRateLimit: at the tracking cap the longest-idle IP is forgotten, never an active one', () => {
376
+ const limit = createReadRateLimit({ scope: 'cap', limit: 1, maxTrackedIps: 2 });
377
+ const hit = (ip) => { let passedThrough = false; limit({ ip }, mkRes(), () => { passedThrough = true; }); return passedThrough; };
378
+ hit('198.51.100.1');
379
+ hit('198.51.100.2');
380
+ assert.equal(hit('198.51.100.1'), false, '.1 is capped — and is now the most recently seen');
381
+ hit('198.51.100.3'); // a third IP at the cap evicts the longest idle: .2, not .1
382
+ assert.equal(hit('198.51.100.1'), false, 'the active, capped IP kept its bucket through the flood');
383
+ assert.equal(hit('198.51.100.2'), true, 'the idle IP was the one forgotten');
384
+ });
385
+
386
+ await t('createReadRateLimit keys on IP, and each budget is its own', () => {
387
+ const a = createReadRateLimit({ scope: 'a', limit: 1 });
388
+ const b = createReadRateLimit({ scope: 'b', limit: 1 });
389
+ a({ ip: '203.0.113.91' }, mkRes(), () => {});
390
+ let otherIp = false; a({ ip: '203.0.113.92' }, mkRes(), () => { otherIp = true; });
391
+ assert.ok(otherIp, 'another IP is unaffected');
392
+ let otherBudget = false; b({ ip: '203.0.113.91' }, mkRes(), () => { otherBudget = true; });
393
+ assert.ok(otherBudget, 'another limiter keeps its own buckets');
394
+ });
395
+
396
+ await t('createReadRateLimit: a request older than the window no longer counts', () => {
397
+ const realNow = Date.now;
398
+ try {
399
+ let clock = 1_000_000;
400
+ Date.now = () => clock;
401
+ const limit = createReadRateLimit({ scope: 'w', limit: 1, windowMs: 1000 });
402
+ limit({ ip: '203.0.113.93' }, mkRes(), () => {});
403
+ let blocked = true; limit({ ip: '203.0.113.93' }, mkRes(), () => { blocked = false; });
404
+ assert.ok(blocked, 'second call inside the window is capped');
405
+ clock += 1001;
406
+ let passedAgain = false; limit({ ip: '203.0.113.93' }, mkRes(), () => { passedAgain = true; });
407
+ assert.ok(passedAgain, 'the window slid past the first call');
408
+ } finally {
409
+ Date.now = realNow;
410
+ }
411
+ });
412
+
413
+ console.log(`\n${passed} passed, ${failed} failed`);
414
+ process.exit(failed === 0 ? 0 : 1);
@@ -412,5 +412,13 @@ await test('the page is the sky\'s own surface — the same sheet and the same s
412
412
  assert.equal(/goal-map-lib|goal-map\.js|goal-map\.css/.test(html), false);
413
413
  });
414
414
 
415
+ await test('the ledger drawer accessible name matches its visible heading (docs/voice.md V3, task 1004251)', () => {
416
+ const html = read('goal-map.html');
417
+ const heading = html.match(/<h2 id="roomTitle">([^<]+)<\/h2>/)[1];
418
+ const label = html.match(/<aside class="room" id="room"[^>]*aria-label="([^"]+)"/)[1];
419
+ assert.equal(label.toLowerCase(), heading.toLowerCase(), 'one thing, one name');
420
+ assert.equal(label, 'The ledger');
421
+ });
422
+
415
423
  console.log('');
416
424
  summary();