@bongos/core 1.23.19 → 1.23.21
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 +82 -42
- 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 +107 -101
- package/docs/copy-registry.json +189 -125
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +3 -1
- package/docs/page-readings.json +73 -58
- package/modules/copy-desk/chrome-pages.js +178 -0
- package/modules/copy-desk/page-data.js +34 -2
- package/modules/copy-desk/tests/copy_chrome_pages.mjs +127 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +1 -1
- package/modules/platform-identity/builder-of-the-week.js +18 -9
- package/modules/platform-identity/community-leaderboard.js +3 -0
- package/modules/platform-identity/community-movers.js +152 -0
- package/modules/platform-identity/routes/community-leaderboard.js +22 -2
- package/modules/public-landing/public/community-movers-none.states.json +15 -0
- package/modules/public-landing/public/community.html +62 -4
- package/modules/public-landing/public/community.states.json +1 -1
- package/modules/ui-design/kit/fixtures/community-movers-none.json +6 -0
- package/modules/ui-design/kit/fixtures/community-movers.json +12 -0
- package/modules/ui-design/kit/serve.js +8 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/copy-apply.js +3 -1
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/scripts/bongos/tweak-renders.js +9 -2
- package/src/module-api.js +1 -1
- package/tests/community_movers.mjs +280 -0
- package/tests/community_movers_db.mjs +147 -0
- package/tests/copy_desk_page_docx.mjs +3 -3
- package/tests/copy_desk_page_editor.mjs +6 -6
- package/tests/copy_desk_page_next.mjs +3 -2
- package/tests/copy_desk_page_reads.mjs +2 -1
- package/tests/copy_desk_page_submit.mjs +30 -2
- package/tests/hub_community_page.mjs +78 -9
- package/tests/privacy_matrix.mjs +48 -9
- package/tests/profile_role_stats.mjs +2 -1
|
@@ -65,8 +65,8 @@ const errDetail = (b) => (b && b.error && b.error.details) || {};
|
|
|
65
65
|
// and an unplaced line.
|
|
66
66
|
const L = (key, section, text, placement, row) => ({ key, section, text, placement, ...(row || {}) });
|
|
67
67
|
const STUDIO_LINES = [
|
|
68
|
-
L('L0001', '
|
|
69
|
-
L('L0002', '
|
|
68
|
+
L('L0001', 'shell bar', 'Studio', 'shared', { file: 'modules/hall-ui/public/shell.js', line: 40, string_id: 'sh0001' }),
|
|
69
|
+
L('L0002', 'shell bar', 'Ctrl K', 'shared'),
|
|
70
70
|
L('L0003', 'greeting', 'Welcome back', 'placed', { file: 'modules/hall-ui/public/studio.html', line: 12, string_id: 'st0003' }),
|
|
71
71
|
L('L0004', 'greeting', '{…} pages tweaked', 'placed', { file: 'modules/hall-ui/public/studio.js', line: 88, string_id: 'st0004' }),
|
|
72
72
|
L('L0005', 'greeting', 'Your desk is quiet today', 'unplaced'),
|
|
@@ -360,6 +360,34 @@ try {
|
|
|
360
360
|
guarded._setList(null);
|
|
361
361
|
}
|
|
362
362
|
});
|
|
363
|
+
|
|
364
|
+
// Task 1004904: a shared-chrome page is a page like any other, whole path.
|
|
365
|
+
await test('a chrome page: claimed, drafted and submitted through the real routes, its lines placed from the first occurrence', async () => {
|
|
366
|
+
const TOP = '/copy-desk/pages/builders:top-bar';
|
|
367
|
+
const chromeLine = (key, text, row) => L(key, 'top bar', text, 'shared', row);
|
|
368
|
+
const r0 = readingsWith(
|
|
369
|
+
{ hash: 'studio-h1', lines: [chromeLine('L0001', 'Projects', { file: 'modules/hall-ui/public/shell.js', line: 41, string_id: 'sh0041' }), chromeLine('L0002', 'Sign out', { file: 'modules/hall-ui/public/shell.js', line: 42, string_id: 'sh0042' }), ...STUDIO_LINES.slice(2)] },
|
|
370
|
+
{ hash: 'index-h2', lines: INDEX_LINES },
|
|
371
|
+
);
|
|
372
|
+
writeReadings(r0);
|
|
373
|
+
const studio = await call('GET', '/copy-desk/pages/builders:studio', ALICE);
|
|
374
|
+
assert.equal(studio.status, 200);
|
|
375
|
+
const page = await call('GET', TOP, ALICE);
|
|
376
|
+
assert.equal(page.status, 200, JSON.stringify(page.body));
|
|
377
|
+
assert.equal((await call('POST', `${TOP}/claim`, ALICE)).status, 201);
|
|
378
|
+
const read = await call('GET', `${TOP}/draft`, ALICE);
|
|
379
|
+
assert.equal(read.status, 200, JSON.stringify(read.body));
|
|
380
|
+
const hash = read.body.reading.reading_hash || read.body.reading_hash;
|
|
381
|
+
assert.ok(hash, 'the chrome page has a reading hash of its own');
|
|
382
|
+
assert.deepEqual(read.body.reading.lines.map((l) => l.text), ['Projects', 'Sign out']);
|
|
383
|
+
const saved = await call('PUT', `${TOP}/draft`, ALICE, { reading_hash: hash, lines: [{ key: 'L0001', after: 'Your projects' }] });
|
|
384
|
+
assert.equal(saved.status, 200, JSON.stringify(saved.body));
|
|
385
|
+
const sub = await call('POST', `${TOP}/submit`, ALICE);
|
|
386
|
+
assert.equal(sub.status, 200, JSON.stringify(sub.body));
|
|
387
|
+
const round = db.tasks.find((t) => t.source_ref === 'page-tweak/builders:top-bar/r1');
|
|
388
|
+
const batch = pages.parseBatchBlock(round.description).batch;
|
|
389
|
+
assert.deepEqual(batch.lines.map((l) => [l.key, l.file, l.line, l.before, l.after]), [['L0001', 'modules/hall-ui/public/shell.js', 41, 'Projects', 'Your projects']]);
|
|
390
|
+
});
|
|
363
391
|
} finally {
|
|
364
392
|
server.close();
|
|
365
393
|
pageData._setRoot(null);
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
// its refusals; the recruiter list as a door signed out, a refusal on a 403, and rows
|
|
18
18
|
// only from a 200 — name, face and login, never the ranked half's numbers; the builder
|
|
19
19
|
// of the week as one line on the board and as its own page, drawn as the server sent it;
|
|
20
|
-
// the home's three crowns, its empty and failed states, and its doors
|
|
20
|
+
// the home's three crowns, its empty and failed states, and its doors; the row under
|
|
21
|
+
// them (task 1002983), the rising builder and the projects on the move, from its own read
|
|
21
22
|
// • the Community menu on every page that wears the hub's bar (apex-only where the page
|
|
22
23
|
// also serves a child), folded into the Projects band on phones
|
|
23
24
|
// • the routes: the five paths served on the hub's apex only
|
|
24
|
-
// • the kit: the states files, and the stub answering as the
|
|
25
|
+
// • the kit: the states files, and the stub answering as the six reads do
|
|
25
26
|
//
|
|
26
27
|
// DB-free. Run: node --test tests/hub_community_page.mjs
|
|
27
28
|
|
|
@@ -51,6 +52,7 @@ const SEARCH_JS = fs.readFileSync(path.join(PI, 'builder-search.js'), 'utf8');
|
|
|
51
52
|
const SUBPAGES = [['/community', 'Community home'], ['/community/leaderboard', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
52
53
|
const WEEK_JS = fs.readFileSync(path.join(PI, 'builder-of-the-week.js'), 'utf8');
|
|
53
54
|
const CROWNS_JS = fs.readFileSync(path.join(PI, 'community-crowns.js'), 'utf8');
|
|
55
|
+
const MOVERS_JS = fs.readFileSync(path.join(PI, 'community-movers.js'), 'utf8');
|
|
54
56
|
|
|
55
57
|
// ── 1. the markup ────────────────────────────────────────────────────────────
|
|
56
58
|
|
|
@@ -119,7 +121,8 @@ test('the home states the month’s and year’s rule, and it is the module’s;
|
|
|
119
121
|
assert.match(rule, /The week and the month are decided once, when they end; the year shows the leader so far, and is decided on 1 January\./);
|
|
120
122
|
assert.match(CROWNS_JS, /ON CONFLICT \(period, period_start\) DO NOTHING/);
|
|
121
123
|
assert.match(CROWNS_JS, /computeWinner\(start, now, pool, \{ fromFirstSnapshot: true \}\)/, 'the year so far is live');
|
|
122
|
-
for (const id of ['homeFailed', 'homeCrowns', 'crownYearLast']) assert.match(home, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
124
|
+
for (const id of ['homeFailed', 'homeCrowns', 'crownYearLast', 'homeMovers', 'moversRule', 'moveNone']) assert.match(home, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
125
|
+
assert.ok(home.indexOf('id="homeMovers"') > home.indexOf('id="homeRule"') && home.indexOf('id="homeMovers"') < home.indexOf('id="doorsH"'), 'the row under the crowns and their rule, above the doors (owner decision 5, task 1004902)');
|
|
123
126
|
const doors = [...home.split('<ul class="homeDoors" aria-labelledby="doorsH">')[1].split('</ul>')[0].matchAll(/<li( class="knowCell" hidden)?><a class="btn ghost" href="([^"]+)">([^<]+)<\/a><\/li>/g)];
|
|
124
127
|
assert.deepEqual(doors.map((m) => [m[2], m[3], !!m[1]]), [
|
|
125
128
|
['/community/leaderboard', 'Leaderboard', false], ['/community/week', 'Builder of the week', false], ['/community/find', 'Find a builder', false],
|
|
@@ -127,6 +130,19 @@ test('the home states the month’s and year’s rule, and it is the module’s;
|
|
|
127
130
|
], 'one door per page, the menu’s order; Builders you may know signed in only, like its menu item');
|
|
128
131
|
});
|
|
129
132
|
|
|
133
|
+
test('the movers’ rule is the module’s: the base cap, the shipped-work floor, the projects’ order and who is counted (task 1002983)', () => {
|
|
134
|
+
const rule = MAIN.split('<p class="commNote" id="moversRule" hidden>')[1].split('</p>')[0];
|
|
135
|
+
const cap = MOVERS_JS.match(/const RISING_BASE_MAX = (\d+);/)[1];
|
|
136
|
+
assert.match(rule, new RegExp(`the most \\{\\{currency\\}\\} in the last 30 days, among the builders who had earned under ${cap} \\{\\{currency\\}\\} before them and shipped at least one work in them\\.`));
|
|
137
|
+
assert.match(MOVERS_JS, /const WINDOW = '30d';/);
|
|
138
|
+
assert.match(MOVERS_JS, /s\.base < RISING_BASE_MAX && s\.tasks_shipped >= 1/, 'the cap and the floor the page states are the ones applied');
|
|
139
|
+
assert.match(rule, /Projects on the move are the public projects that shipped the most works in the last 30 days, then earned the most \{\{currency\}\}\./);
|
|
140
|
+
assert.match(MOVERS_JS, /\(b\.tasks_shipped - a\.tasks_shipped\) \|\| \(b\.credits - a\.credits\)/);
|
|
141
|
+
assert.match(MOVERS_JS, /onMapSql\('p'\)\} AND p\.visibility = 'public'/, '“public projects”: on the map with the Public door');
|
|
142
|
+
assert.match(rule, /Builders who hide their numbers or keep their account private are not counted in either\./);
|
|
143
|
+
assert.match(MOVERS_JS, /eligibleIdentities/);
|
|
144
|
+
});
|
|
145
|
+
|
|
130
146
|
test('the window dropdown and the query limits are the routes’ own (boundary-forced copies)', () => {
|
|
131
147
|
const days = BOARD_JS.match(/const WINDOW_DAYS = Object\.freeze\(\{([^}]*)\}\);/);
|
|
132
148
|
const windows = [...days[1].matchAll(/'?(\w+)'?:/g)].map((m) => m[1]);
|
|
@@ -144,12 +160,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
|
|
|
144
160
|
assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
|
|
145
161
|
});
|
|
146
162
|
|
|
147
|
-
test('the script builds no markup from strings, reads only the
|
|
163
|
+
test('the script builds no markup from strings, reads only the seven routes, and never re-ranks or reads the ranked tier', () => {
|
|
148
164
|
assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
|
|
149
|
-
// every path literal in the code: the API base, the
|
|
165
|
+
// every path literal in the code: the API base, the seven reads, the five sub-pages, the
|
|
150
166
|
// sign-in door and the hall's invite box the Recruit door opens (task 1002292)
|
|
151
167
|
const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
|
|
152
|
-
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/crowns', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
|
|
168
|
+
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/crowns', '/community/find', '/community/leaderboard', '/community/movers', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
|
|
153
169
|
assert.doesNotMatch(CODE, /method:/, 'it only reads');
|
|
154
170
|
assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
|
|
155
171
|
assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
|
|
@@ -201,8 +217,14 @@ const CROWNS = () => ({
|
|
|
201
217
|
month: { start: '2026-10-01', end: '2026-11-01', first_crown: null, winner: WINNER('bo', 6120, 1, { display_name: null, avatar_url: 'javascript:alert(1)' }) },
|
|
202
218
|
year: { start: '2026-01-01', since: '2026-09-27', last: null, winner: WINNER('cy', 9870, 31) },
|
|
203
219
|
});
|
|
220
|
+
const MOVERS = () => ({
|
|
221
|
+
window: { start: '2026-09-28T09:30:00.000Z', end: '2026-10-28T09:30:00.000Z' },
|
|
222
|
+
rising: WINNER('kofi', 640, 5, { display_name: 'Kofi <b>M</b>' }),
|
|
223
|
+
projects: [{ name: 'Tide <i>pool</i>', origin: 'https://tidepool.example', tasks_shipped: 23, credits: 1840 }, { name: 'Quill', origin: 'javascript:alert(1)', tasks_shipped: 1, credits: 45 }],
|
|
224
|
+
});
|
|
204
225
|
const READS = () => ({
|
|
205
226
|
'/community/crowns': { status: 200, body: CROWNS() },
|
|
227
|
+
'/community/movers': { status: 200, body: MOVERS() },
|
|
206
228
|
'/community/builder-of-the-week': { status: 200, body: WEEK() },
|
|
207
229
|
'/community/leaderboard?window=all': { status: 200, body: { window: 'all', count: 99, results: [ROW(26, 'ana', 1200), ROW(27, 'bo', 1500, { tasks_shipped: 1, karma: 0, avatar_url: 'javascript:alert(1)', display_name: null })] } },
|
|
208
230
|
'/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
|
|
@@ -271,7 +293,7 @@ test('each path shows its one section, marks its menu items current and names it
|
|
|
271
293
|
|
|
272
294
|
test('the home: one read, the three crowns with their periods, names as text, and nothing asked about projects', async () => {
|
|
273
295
|
const ui = await boot({ pathname: '/community' });
|
|
274
|
-
assert.deepEqual(ui.gets(), ['/api/bongos/community/crowns']);
|
|
296
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/crowns', '/api/bongos/community/movers']);
|
|
275
297
|
assert.equal(ui.$('homeLoading').hidden, true);
|
|
276
298
|
assert.equal(ui.$('homeFailed').hidden, true);
|
|
277
299
|
assert.equal(ui.$('homeCrowns').hidden, false);
|
|
@@ -286,7 +308,7 @@ test('the home: one read, the three crowns with their periods, names as text, an
|
|
|
286
308
|
assert.equal(find(ui.$('crownYear'), 'mineRow__host').textContent, '@cy');
|
|
287
309
|
assert.equal(ui.$('crownYearLast').hidden, true, 'no year crowned yet');
|
|
288
310
|
const signedIn = await boot({ pathname: '/community', me: { github_login: 'owner' } });
|
|
289
|
-
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/crowns'], 'signed in, the same
|
|
311
|
+
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/crowns', '/api/bongos/community/movers'], 'signed in, the same two reads: no Recruit door here');
|
|
290
312
|
assert.equal(signedIn.$('homeCrowns').textContent, ui.$('homeCrowns').textContent, 'the same crowns for every viewer');
|
|
291
313
|
});
|
|
292
314
|
|
|
@@ -332,6 +354,45 @@ test('the home: a failed read is an alert, never three empty crowns; a rate limi
|
|
|
332
354
|
assert.match((await boot({ pathname: '/community', reads })).$('homeFailed').textContent, /^Too many requests/);
|
|
333
355
|
});
|
|
334
356
|
|
|
357
|
+
test('the row under the crowns: the rising builder as a crown, the projects in the server’s order, names as text, a link only to a web address (task 1002983)', async () => {
|
|
358
|
+
const ui = await boot({ pathname: '/community' });
|
|
359
|
+
assert.equal(ui.$('homeMovers').hidden, false);
|
|
360
|
+
assert.equal(ui.$('moversRule').hidden, false);
|
|
361
|
+
assert.equal(find(ui.$('moveRising'), 'crownName').textContent, 'Kofi <b>M</b>', 'a name is text, never markup');
|
|
362
|
+
assert.equal(find(ui.$('moveRising'), 'crownNums').textContent, '640credits5works shipped');
|
|
363
|
+
const rows = kids(ui.$('moveProjects'));
|
|
364
|
+
assert.deepEqual(rows.map((r) => find(r, 'commPlace').textContent), ['1', '2'], 'positions in the order sent');
|
|
365
|
+
const first = find(rows[0], 'mineRow__name');
|
|
366
|
+
assert.equal(first.tag, 'a');
|
|
367
|
+
assert.equal(first.attrs.href, 'https://tidepool.example/');
|
|
368
|
+
assert.equal(first.textContent, 'Tide <i>pool</i>');
|
|
369
|
+
assert.equal(find(rows[0], 'mineRow__hint').textContent, (1840).toLocaleString() + ' credits earned');
|
|
370
|
+
assert.equal(find(rows[0], 'commCredits').textContent, '23works shipped');
|
|
371
|
+
assert.equal(find(rows[1], 'mineRow__name').tag, 'div', 'an address that is not http(s) is never a link');
|
|
372
|
+
assert.equal(find(rows[1], 'commCredits').textContent, '1work shipped', 'one work, singular');
|
|
373
|
+
assert.equal(ui.$('moveNone').hidden, true);
|
|
374
|
+
const signedIn = await boot({ pathname: '/community', me: { github_login: 'owner' } });
|
|
375
|
+
assert.equal(signedIn.$('homeMovers').textContent, ui.$('homeMovers').textContent, 'the same row for every viewer');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('the row under the crowns: nobody to show is one line in each card; a failed read drops only the row', async () => {
|
|
379
|
+
const reads = READS();
|
|
380
|
+
reads['/community/movers'] = { status: 200, body: { window: {}, rising: null, projects: [] } };
|
|
381
|
+
const quiet = await boot({ pathname: '/community', reads });
|
|
382
|
+
assert.equal(quiet.$('homeMovers').hidden, false);
|
|
383
|
+
assert.equal(find(quiet.$('moveRising'), 'commMsg').textContent, 'No rising builder in the last 30 days.');
|
|
384
|
+
assert.equal(kids(quiet.$('moveProjects')).length, 0);
|
|
385
|
+
assert.equal(quiet.$('moveNone').hidden, false);
|
|
386
|
+
for (const answer of [{ status: 500, body: {} }, { status: 429, body: {} }, { reject: true }]) {
|
|
387
|
+
reads['/community/movers'] = answer;
|
|
388
|
+
const failed = await boot({ pathname: '/community', reads });
|
|
389
|
+
assert.equal(failed.$('homeMovers').hidden, true);
|
|
390
|
+
assert.equal(failed.$('moversRule').hidden, true, 'no rule for cards that are not there');
|
|
391
|
+
assert.equal(failed.$('homeCrowns').hidden, false, 'the crowns stand on their own');
|
|
392
|
+
assert.equal(failed.$('homeFailed').hidden, true, 'and no alert: the row is the home’s extra');
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
335
396
|
test('the leaderboard, signed out: the all-time board in the server’s order with the server’s places, and nothing asked about the viewer', async () => {
|
|
336
397
|
const ui = await boot();
|
|
337
398
|
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week']);
|
|
@@ -747,6 +808,8 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
|
|
|
747
808
|
assert.equal(states.states['out-find'].url, '/community/find');
|
|
748
809
|
assert.equal(states.states['out-week'].url, '/community/week');
|
|
749
810
|
assert.ok(states.states['out-board'].expect.visible.includes('#boardCrown'), 'the board carries the crown line');
|
|
811
|
+
assert.ok(states.states.out.expect.visible.includes('#homeMovers'), 'the home carries the row under the crowns (task 1002983)');
|
|
812
|
+
assert.equal(json('community-movers-none.states.json').stub.fixtures['community-movers'], 'community-movers-none');
|
|
750
813
|
// the home's other answers (task 1004902): before the first month, nobody, and a new year
|
|
751
814
|
for (const [file, fixture] of [['community-crowns-first.states.json', 'community-crowns-first'], ['community-crowns-none.states.json', 'community-crowns-none'], ['community-crowns-new-year.states.json', 'community-crowns-new-year']]) {
|
|
752
815
|
assert.equal(json(file).stub.fixtures['community-crowns'], fixture);
|
|
@@ -790,7 +853,7 @@ async function call(origin, p) {
|
|
|
790
853
|
return { status: res.status, body: await res.json() };
|
|
791
854
|
}
|
|
792
855
|
|
|
793
|
-
test('the kit stub answers the crowns, the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
|
|
856
|
+
test('the kit stub answers the crowns, the movers, the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
|
|
794
857
|
const s = await stub();
|
|
795
858
|
try {
|
|
796
859
|
const crowns = await call(s.origin, '/community/crowns');
|
|
@@ -798,6 +861,12 @@ test('the kit stub answers the crowns, the board, the builder of the week, the s
|
|
|
798
861
|
const { week: w, month: m, year: y } = crowns.body;
|
|
799
862
|
assert.deepEqual([Object.keys(w).sort(), Object.keys(m).sort(), Object.keys(y).sort()], [['end', 'start', 'winner'], ['end', 'first_crown', 'start', 'winner'], ['last', 'since', 'start', 'winner']], 'community-crowns.js’s shape');
|
|
800
863
|
assert.ok([w.winner, m.winner, y.winner].every((r) => r && Object.keys(r).sort().join() === 'avatar_url,credits,display_name,github_login,handle,tasks_shipped'), 'the module’s projection, no github_id');
|
|
864
|
+
const movers = await call(s.origin, '/community/movers');
|
|
865
|
+
assert.deepEqual(Object.keys(movers.body), ['window', 'rising', 'projects'], 'community-movers.js’s shape');
|
|
866
|
+
assert.equal(Object.keys(movers.body.rising).sort().join(), 'avatar_url,credits,display_name,github_login,handle,tasks_shipped', 'the crown’s projection, no github_id');
|
|
867
|
+
assert.ok(movers.body.projects.length && movers.body.projects.every((p) => Object.keys(p).sort().join() === 'credits,name,origin,tasks_shipped'), 'a project’s name, address and two numbers, no client_id');
|
|
868
|
+
const ships = movers.body.projects.map((p) => p.tasks_shipped);
|
|
869
|
+
assert.deepEqual(ships, [...ships].sort((a, b) => b - a), 'most works first');
|
|
801
870
|
const week = await call(s.origin, '/community/builder-of-the-week');
|
|
802
871
|
assert.deepEqual(Object.keys(week.body).sort(), ['past', 'week', 'winner'], 'the route’s three keys');
|
|
803
872
|
assert.ok(week.body.winner && week.body.past.length, 'a crown and the weeks before it');
|
package/tests/privacy_matrix.mjs
CHANGED
|
@@ -158,7 +158,11 @@ function identitySentinels(acct) {
|
|
|
158
158
|
// the year so far and last year (task 1004902): a crown IS stats, so the board's
|
|
159
159
|
// rule. Each account in turn is made the winner of all three, so the reads'
|
|
160
160
|
// own predicates decide: 'listed' | 'absent'
|
|
161
|
-
//
|
|
161
|
+
// movers GET /community/movers Anon · Builder · Recruiter — the rising builder and the
|
|
162
|
+
// projects on the move (task 1002983): a card IS stats, so the board's rule, and
|
|
163
|
+
// a project's totals are the sum of what may be shown. Each account in turn is
|
|
164
|
+
// the one who moved, so the reads' own predicates decide: 'listed' | 'absent'
|
|
165
|
+
// directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
|
|
162
166
|
// sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
|
|
163
167
|
// self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
|
|
164
168
|
// guild GET /guilds/:slug Anon · Builder · Recruiter · Self — the guild page's PUBLIC
|
|
@@ -174,13 +178,13 @@ function identitySentinels(acct) {
|
|
|
174
178
|
// people the guild page already shows them — shown_publicly AND existence;
|
|
175
179
|
// hide_stats never hides membership, and private or provisional is never listed
|
|
176
180
|
const MATRIX = {
|
|
177
|
-
PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
|
|
178
|
-
PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
179
|
-
SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', crowns: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
180
|
-
PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
181
|
-
PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
182
|
-
PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
183
|
-
OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
181
|
+
PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
|
|
182
|
+
PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
183
|
+
SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
184
|
+
PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
185
|
+
PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
186
|
+
PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
|
|
187
|
+
OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
|
|
184
188
|
};
|
|
185
189
|
|
|
186
190
|
// ===========================================================================
|
|
@@ -303,6 +307,9 @@ function weekOf(key) {
|
|
|
303
307
|
// THE ACCOUNT that won the month, the year so far and last year (task 1004902); the
|
|
304
308
|
// crowns test makes each fixture account the winner in turn.
|
|
305
309
|
let CROWNED = 'PUB';
|
|
310
|
+
// THE ACCOUNT that moved in the 30 days, on MX_CLIENT's project (task 1002983); the movers
|
|
311
|
+
// test makes each fixture account the one in turn.
|
|
312
|
+
let MOVED = 'PUB';
|
|
306
313
|
|
|
307
314
|
const pool = {
|
|
308
315
|
query: async (text, params = []) => {
|
|
@@ -393,6 +400,20 @@ const pool = {
|
|
|
393
400
|
}));
|
|
394
401
|
return { rows, rowCount: rows.length };
|
|
395
402
|
}
|
|
403
|
+
// THE MOVERS (task 1002983): the 30 days' rows carry the floor on the act CTE, as the
|
|
404
|
+
// year's; MOVED had nothing before them. The project names are MX_CLIENT's.
|
|
405
|
+
if (/^WITH act AS/.test(sql) && /LEFT JOIN per_project pp/.test(sql)) {
|
|
406
|
+
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'movers');
|
|
407
|
+
const rows = ACCOUNTS.filter((a) => a.key === MOVED).filter(accountFilter(where, [])).map((a) => ({
|
|
408
|
+
client_id: MX_CLIENT.client_id, github_id: a.github_id, credits: String(ROLLUP[a.github_id].credits),
|
|
409
|
+
tasks_shipped: String(ROLLUP[a.github_id].works_shipped), karma: String(ROLLUP[a.github_id].karma), base_credits: '0',
|
|
410
|
+
}));
|
|
411
|
+
return { rows, rowCount: rows.length };
|
|
412
|
+
}
|
|
413
|
+
if (/FROM platform_identity_sso_clients c JOIN platform_identity_projects p ON p\.origin = c\.origin WHERE c\.client_id = ANY\(\$1::text\[\]\)/.test(sql)) {
|
|
414
|
+
const rows = (params[0] || []).includes(MX_CLIENT.client_id) ? [{ client_id: MX_CLIENT.client_id, name: MX_CLIENT.name, origin: MX_CLIENT.origin }] : [];
|
|
415
|
+
return { rows, rowCount: rows.length };
|
|
416
|
+
}
|
|
396
417
|
// ... the year so far: computeWinner's act CTE carries the floor, then the leader's re-read
|
|
397
418
|
if (/^WITH act AS/.test(sql) && /\) f ON true/.test(sql)) {
|
|
398
419
|
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'year so far');
|
|
@@ -868,6 +889,24 @@ test('the crowns — the month, the year so far and last year only for shown, pu
|
|
|
868
889
|
} finally { CROWNED = 'PUB'; }
|
|
869
890
|
});
|
|
870
891
|
|
|
892
|
+
test('the movers — the rising builder and a project’s totals only from shown, public, active accounts, the same for every viewer', async () => {
|
|
893
|
+
const { clearMoversMemo } = require('../modules/platform-identity/community-movers.js');
|
|
894
|
+
try {
|
|
895
|
+
for (const k of KEYS) {
|
|
896
|
+
MOVED = k;
|
|
897
|
+
clearMoversMemo(); // the 30 days are memoised: a new mover is a new computation
|
|
898
|
+
const seen = [];
|
|
899
|
+
for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/movers'));
|
|
900
|
+
for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got different movers with ${k} moving`);
|
|
901
|
+
const { rising, projects } = seen[0].json;
|
|
902
|
+
const listed = MATRIX[k].movers === 'listed';
|
|
903
|
+
assert.equal(!!rising && rising.handle === ACCT[k].handle, listed, `${k} as the rising builder: expected ${MATRIX[k].movers}`);
|
|
904
|
+
assert.deepEqual(projects.map((p) => p.tasks_shipped), listed ? [ROLLUP[ACCT[k].github_id].works_shipped] : [], `${k}'s works in a project's totals: expected ${MATRIX[k].movers}`);
|
|
905
|
+
assert.doesNotMatch(seen[0].text, /github_id|client_id/, 'github_id and client_id never cross');
|
|
906
|
+
}
|
|
907
|
+
} finally { MOVED = 'PUB'; }
|
|
908
|
+
});
|
|
909
|
+
|
|
871
910
|
test('the recruiting directory — the recruiter sees the full and name-only tiers; Anon and Builder are refused', async () => {
|
|
872
911
|
const r = await get(RECRUITER, '/api/bongos/scouting');
|
|
873
912
|
assert.equal(r.status, 200);
|
|
@@ -910,7 +949,7 @@ function disclosed(r) {
|
|
|
910
949
|
}
|
|
911
950
|
|
|
912
951
|
test('the walk covered every surface for every row (so the claims below are about something)', () => {
|
|
913
|
-
for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/community/builder-of-the-week', '/community/crowns', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
|
|
952
|
+
for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/community/builder-of-the-week', '/community/crowns', '/community/movers', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
|
|
914
953
|
assert.ok(SEEN.some((r) => r.path.includes(p) && r.status === 200), `no 200 from ${p} — the sweep below would be vacuous for it`);
|
|
915
954
|
}
|
|
916
955
|
assert.ok(SEEN.length >= KEYS.length * 6, `only ${SEEN.length} responses recorded`);
|
|
@@ -69,7 +69,8 @@ const LEDGER_ROWS = [
|
|
|
69
69
|
// ---- the page-tweak ledger (Ines and one other artist) ----------------------
|
|
70
70
|
|
|
71
71
|
const pages = require('../modules/copy-desk/pages.js');
|
|
72
|
-
|
|
72
|
+
// The studio's own inventory: the real pages plus each surface's chrome pages.
|
|
73
|
+
const inventory = require('../modules/copy-desk/page-data.js').loadInventory().data;
|
|
73
74
|
const onSurface = (s) => inventory.pages.filter((p) => p.surface === s).map((p) => p.id);
|
|
74
75
|
const LANDING = onSurface('landing');
|
|
75
76
|
const BUILDERS = onSurface('builders');
|