@bongos/core 1.23.17 → 1.23.19
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 +109 -54
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +22 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +985 -934
- package/docs/copy-registry.json +1829 -1309
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +10 -2
- package/docs/page-readings.json +601 -576
- package/modules/copy-desk/auto-close.js +9 -3
- package/modules/copy-desk/page-status.js +25 -13
- package/modules/copy-desk/tests/copy_page_auto_close.mjs +8 -0
- package/modules/copy-desk/tests/copy_page_status.mjs +2 -2
- package/modules/platform-identity/builder-of-the-week.js +48 -20
- package/modules/platform-identity/community-crowns.js +176 -0
- package/modules/platform-identity/community-leaderboard.js +1 -1
- package/modules/platform-identity/migrations/platform_identity_033_period_features.sql +43 -0
- package/modules/platform-identity/routes/community-leaderboard.js +25 -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-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 +130 -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/serve.js +14 -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 +4 -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/copy_stuck_page_tweaks.mjs +39 -2
- package/tests/hub_account_page.mjs +2 -2
- package/tests/hub_community_page.mjs +144 -29
- package/tests/landing_legal_pages.mjs +2 -2
- package/tests/privacy_matrix.mjs +68 -9
|
@@ -858,7 +858,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
858
858
|
<!--apex-only:start--><div class="menuBand" id="menuCommunity" data-menu="community" hidden>
|
|
859
859
|
<div class="bandIn">
|
|
860
860
|
<ul class="bandRow">
|
|
861
|
-
<li class="bandCell"><a class="bandT" href="/community">
|
|
861
|
+
<li class="bandCell"><a class="bandT" href="/community">Community home</a><span class="bandC">the builders of the week, month and year</span></li>
|
|
862
|
+
<li class="bandCell"><a class="bandT" href="/community/leaderboard">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
|
|
862
863
|
<li class="bandCell"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li>
|
|
863
864
|
<li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
|
|
864
865
|
<li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
|
|
@@ -875,7 +876,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
875
876
|
<li class="bandCell"><a class="bandT" href="?view=map&sub=table" data-nav="map" data-sub="table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
876
877
|
<li class="bandCell"><a class="bandT" href="?view=mine" data-nav="mine">Your projects</a><span class="bandC">the ones you own</span></li>
|
|
877
878
|
<li class="bandCell"><a class="bandT" href="?view=new" data-nav="new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
878
|
-
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li><!--apex-only:end-->
|
|
879
|
+
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Community home</a><span class="bandC">the builders of the week, month and year</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/leaderboard">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li><!--apex-only:end-->
|
|
879
880
|
</ul>
|
|
880
881
|
<!-- About and Docs are site pages, not project views: they live in the
|
|
881
882
|
footer row with Privacy / Terms / Contact (owner, 2026-08-28; task 1003332). -->
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
<!--apex-only:start--><div class="menuBand" id="menuCommunity" data-menu="community" hidden>
|
|
70
70
|
<div class="bandIn">
|
|
71
71
|
<ul class="bandRow">
|
|
72
|
-
<li class="bandCell"><a class="bandT" href="/community">
|
|
72
|
+
<li class="bandCell"><a class="bandT" href="/community">Community home</a><span class="bandC">the builders of the week, month and year</span></li>
|
|
73
|
+
<li class="bandCell"><a class="bandT" href="/community/leaderboard">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
|
|
73
74
|
<li class="bandCell"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li>
|
|
74
75
|
<li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
|
|
75
76
|
<li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
|
|
@@ -84,7 +85,7 @@
|
|
|
84
85
|
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
85
86
|
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
|
|
86
87
|
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
87
|
-
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li><!--apex-only:end-->
|
|
88
|
+
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Community home</a><span class="bandC">the builders of the week, month and year</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/leaderboard">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li><!--apex-only:end-->
|
|
88
89
|
</ul>
|
|
89
90
|
</div>
|
|
90
91
|
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "GET /community/crowns in late October 2026 (task 1004902), what the hub shows until the first month is crowned: September began before snapshot history did, so it is not crowned and the answer names 1 November, the day October is.",
|
|
3
|
+
"week": { "start": "2026-10-19", "end": "2026-10-26", "winner": { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 1240, "tasks_shipped": 4 } },
|
|
4
|
+
"month": { "start": "2026-09-01", "end": "2026-10-01", "first_crown": "2026-11-01", "winner": null },
|
|
5
|
+
"year": { "start": "2026-01-01", "since": "2026-09-27", "last": null, "winner": { "handle": "priya-nair", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "credits": 7400, "tasks_shipped": 27 } }
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "GET /community/crowns in early January 2027 (task 1004902): 2026 has been crowned for good and shows as last year under the new year so far; history began before 2027, so the year counts from 1 January.",
|
|
3
|
+
"week": { "start": "2027-01-04", "end": "2027-01-11", "winner": { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 980, "tasks_shipped": 3 } },
|
|
4
|
+
"month": { "start": "2026-12-01", "end": "2027-01-01", "first_crown": null, "winner": { "handle": "tomas-ek", "github_login": "tomasek", "display_name": "Tomas Ek", "avatar_url": "/__kit/avatar.svg", "credits": 4400, "tasks_shipped": 15 } },
|
|
5
|
+
"year": { "start": "2027-01-01", "since": "2027-01-01", "winner": { "handle": "lin-wei", "github_login": "linwei", "display_name": "Lin Wei", "avatar_url": null, "credits": 310, "tasks_shipped": 2 }, "last": { "start": "2026-01-01", "winner": { "handle": "maximiliana-van-der-berghe-lindqvist", "github_login": "mvdbl", "display_name": "Maximiliana van der Berghe-Lindqvist", "avatar_url": "/__kit/avatar.svg", "credits": 11250, "tasks_shipped": 38 } } }
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "GET /community/crowns with no one to show (task 1004902): nobody qualified, or each winner hides their numbers — the two look the same (spec D8).",
|
|
3
|
+
"week": { "start": "2026-10-26", "end": "2026-11-02", "winner": null },
|
|
4
|
+
"month": { "start": "2026-10-01", "end": "2026-11-01", "first_crown": null, "winner": null },
|
|
5
|
+
"year": { "start": "2026-01-01", "since": "2026-09-27", "last": null, "winner": null }
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "GET /community/crowns for the kit stub (task 1004902): the week, the month and the year as community-crowns.js answers in early November 2026 — October crowned, the year so far counted since snapshot history began on 27 Sep. Names are invented; the year's long one tests a card's wrap.",
|
|
3
|
+
"week": { "start": "2026-10-26", "end": "2026-11-02", "winner": { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 1240, "tasks_shipped": 4 } },
|
|
4
|
+
"month": { "start": "2026-10-01", "end": "2026-11-01", "first_crown": null, "winner": { "handle": "priya-nair", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "credits": 6120, "tasks_shipped": 23 } },
|
|
5
|
+
"year": { "start": "2026-01-01", "since": "2026-09-27", "last": null, "winner": { "handle": "maximiliana-van-der-berghe-lindqvist", "github_login": "mvdbl", "display_name": "Maximiliana van der Berghe-Lindqvist", "avatar_url": "/__kit/avatar.svg", "credits": 9870, "tasks_shipped": 31 } }
|
|
6
|
+
}
|
|
@@ -736,6 +736,19 @@ function communityWeek(res, origin) {
|
|
|
736
736
|
if (stubStatus(res, f)) return;
|
|
737
737
|
return json(res, 200, { week: f.week, winner: f.winner ? withAvatar(f.winner, origin) : null, past: (f.past || []).map((r) => withAvatar(r, origin)) });
|
|
738
738
|
}
|
|
739
|
+
// The crowns (task 1004902): the fixture is already the route's answer — the week, month
|
|
740
|
+
// and year, each with its winner or null (community-crowns.js).
|
|
741
|
+
function communityCrowns(res, origin) {
|
|
742
|
+
const f = heldFixture('community-crowns');
|
|
743
|
+
if (stubStatus(res, f)) return;
|
|
744
|
+
const shown = (w) => (w ? withAvatar(w, origin) : null);
|
|
745
|
+
const year = f.year || {};
|
|
746
|
+
return json(res, 200, {
|
|
747
|
+
week: { ...f.week, winner: shown(f.week && f.week.winner) },
|
|
748
|
+
month: { ...f.month, winner: shown(f.month && f.month.winner) },
|
|
749
|
+
year: { ...year, winner: shown(year.winner), last: year.last ? { ...year.last, winner: shown(year.last.winner) } : null },
|
|
750
|
+
});
|
|
751
|
+
}
|
|
739
752
|
function scoutingRead(res, origin) {
|
|
740
753
|
const f = heldFixture('scouting');
|
|
741
754
|
if (stubStatus(res, f)) return;
|
|
@@ -774,6 +787,7 @@ const server = http.createServer((req, res) => {
|
|
|
774
787
|
if (rest === '/community/leaderboard' && req.method === 'GET') return communityBoard(res, u, origin);
|
|
775
788
|
if (rest === '/community/search' && req.method === 'GET') return communitySearch(res, u, origin);
|
|
776
789
|
if (rest === '/community/builder-of-the-week' && req.method === 'GET') return communityWeek(res, origin);
|
|
790
|
+
if (rest === '/community/crowns' && req.method === 'GET') return communityCrowns(res, origin);
|
|
777
791
|
if (rest === '/scouting' && req.method === 'GET') return AUTH ? scoutingRead(res, origin) : json(res, 401, UNAUTH);
|
|
778
792
|
if (rest === '/connections' && req.method === 'GET') return AUTH ? connRead(res, 'list', origin) : json(res, 401, UNAUTH);
|
|
779
793
|
if (rest === '/connections/requests' && req.method === 'GET') return AUTH ? connRead(res, 'requests', origin) : json(res, 401, UNAUTH);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.19",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.19",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.19",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -10491,5 +10491,17 @@
|
|
|
10491
10491
|
"id": "1004899",
|
|
10492
10492
|
"text": "Page tweak rounds that hold nothing to do now close themselves after an hour, so the Studio no longer shows empty rounds as needing a look forever."
|
|
10493
10493
|
}
|
|
10494
|
+
],
|
|
10495
|
+
"1.23.18": [
|
|
10496
|
+
{
|
|
10497
|
+
"id": "1004901",
|
|
10498
|
+
"text": "Every stage of a page tweak now has a named person and a deadline. Drafts left alone get a reminder after a week and close after two, so no page sits in the Studio without someone being told."
|
|
10499
|
+
}
|
|
10500
|
+
],
|
|
10501
|
+
"1.23.19": [
|
|
10502
|
+
{
|
|
10503
|
+
"id": "1004902",
|
|
10504
|
+
"text": "The community page now opens on a Community home: the builder of the week, of the month and of the year side by side, how they are chosen, and a button to each community page. The leaderboard has its own page now, and Communit"
|
|
10505
|
+
}
|
|
10494
10506
|
]
|
|
10495
10507
|
}
|
|
@@ -75,6 +75,10 @@ const INTEGRATION = new Set([
|
|
|
75
75
|
// close the week, the shipped-work floor and tie order, and the ON CONFLICT pin
|
|
76
76
|
// under two racing writers. The DB-free sibling is builder_of_the_week.
|
|
77
77
|
'builder_of_the_week_db',
|
|
78
|
+
// task 1004902: the builders of the month and the year against real SQL — the month
|
|
79
|
+
// keeps the week's baseline rule, the year counts from a row's first snapshot inside
|
|
80
|
+
// it, the pin under a hide and two racing writers. The DB-free sibling is community_crowns.
|
|
81
|
+
'community_crowns_db',
|
|
78
82
|
// bfg, bfg_write, memory_isolation, memory_pull and setup_smoke LEFT this set under task
|
|
79
83
|
// 1004865: none of them ever needed a database. Each failed on a stale test harness (a fake
|
|
80
84
|
// server without res.fail, a stub on the wrong module, an old error shape, a renamed session
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.23.
|
|
84
|
+
const CORE_VERSION = '1.23.19'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -338,11 +338,12 @@ function buildPlatformApp() {
|
|
|
338
338
|
res.type('html').send(html);
|
|
339
339
|
});
|
|
340
340
|
// THE COMMUNITY PAGES (task 1003363, ADR 0217 D1, ADR 0337 D1/D3): who is building
|
|
341
|
-
// across every project — the
|
|
341
|
+
// across every project — the community home with the builders of the week, month and
|
|
342
|
+
// year (task 1004902), the leaderboard, the builder of the week (task 1002981), the
|
|
342
343
|
// builder search (the same for every viewer) and the recruiter list GET /scouting's own
|
|
343
|
-
// door admits. One file serves all
|
|
344
|
+
// door admits. One file serves all five; its script shows the one the path names.
|
|
344
345
|
// Hub-only for the /account reason: every read it makes is a platform-identity route.
|
|
345
|
-
app.get(['/community', '/community/week', '/community/find', '/community/recruiters'], (req, res, next) => {
|
|
346
|
+
app.get(['/community', '/community/leaderboard', '/community/week', '/community/find', '/community/recruiters'], (req, res, next) => {
|
|
346
347
|
if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
|
|
347
348
|
const html = landingPage('community.html');
|
|
348
349
|
if (!html) return next();
|
|
@@ -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);
|