@bongos/core 1.19.170 → 1.19.172
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 +39 -24
- 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 +5 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +79 -3
- package/docs/api-reference.md +3 -2
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/profile.html +13 -1
- package/modules/hall-ui/public/profile.js +98 -5
- package/modules/platform-identity/migrations/platform_identity_008_handle.sql +70 -0
- package/modules/platform-identity/platform-identity.js +140 -5
- package/modules/platform-identity/routes/profile.js +47 -1
- package/modules/platform-identity/tests/platform-identity.mjs +139 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/server.js +1 -0
- package/src/module-api.js +5 -1
- package/tests/handle_machinery.mjs +115 -0
- package/tests/profile_ui_cross_project.mjs +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// tests/profile_ui_cross_project.mjs — the profile page's cross-project section
|
|
2
|
+
// (task 1002280, R04 · goal 1000045, ADR 0171).
|
|
3
|
+
//
|
|
4
|
+
// EXECUTES the real modules/hall-ui/public/profile.js in a minimal DOM stub
|
|
5
|
+
// (the hall_signout.mjs pattern) against canned API responses, and asserts the
|
|
6
|
+
// consent-sensitive render contract:
|
|
7
|
+
//
|
|
8
|
+
// - no `cross_project` key in the response (vanilla instance) → the section
|
|
9
|
+
// stays hidden entirely;
|
|
10
|
+
// - `cross_project: null` (hidden / provisional / none — indistinguishable by
|
|
11
|
+
// design) → the "keeps their track record private" line renders and NOTHING
|
|
12
|
+
// derived from the rollup does (no tiles, no digits — a count plus a hidden
|
|
13
|
+
// total is a leak by subtraction);
|
|
14
|
+
// - the self hide-toggle is client-side only (ADR 0171 D4): the Settings link
|
|
15
|
+
// renders iff the /me id matches the profile id, on both the ?id= and the
|
|
16
|
+
// own-profile load paths;
|
|
17
|
+
// - the full object renders totals tiles, named project rows (https origins
|
|
18
|
+
// linked, anything else plain text), `rank_held` as plain text — never the
|
|
19
|
+
// local rank-badge component (spec Decision 1) — and the counted-never-named
|
|
20
|
+
// "+N more" line.
|
|
21
|
+
//
|
|
22
|
+
// Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { test } from 'node:test';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import vm from 'node:vm';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.js'), 'utf8');
|
|
32
|
+
const HTML = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.html'), 'utf8');
|
|
33
|
+
|
|
34
|
+
// ---- fixtures -------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const BUILDER = {
|
|
37
|
+
id: 7, github_login: 'pheidias', display_name: 'Pheidias', avatar_url: null,
|
|
38
|
+
rank: 'metic', total_credits: 120, karma: 3, created_at: '2026-01-01T00:00:00.000Z',
|
|
39
|
+
preferred_disciplines: ['engineer'],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CP_FULL = {
|
|
43
|
+
totals: { projects: 3, credits: 150, works_shipped: 7, karma: 4 },
|
|
44
|
+
disciplines: ['engineer', 'ui'],
|
|
45
|
+
last_active: '2026-08-01T00:00:00.000Z',
|
|
46
|
+
projects: [
|
|
47
|
+
{ name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic' },
|
|
48
|
+
{ name: 'Odd Origin', origin: 'http://insecure.example', rank_held: null },
|
|
49
|
+
],
|
|
50
|
+
more_projects: 1,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const baseProfile = () => ({ builder: { ...BUILDER }, shipped_count: 1, recent_ships: [], achievements: [] });
|
|
54
|
+
|
|
55
|
+
// ---- harness --------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
// Boot profile.js with canned /me + profile responses; returns the memoised
|
|
58
|
+
// element map after the async load() settles.
|
|
59
|
+
async function boot({ search = '', me, profile } = {}) {
|
|
60
|
+
const els = new Map();
|
|
61
|
+
const makeEl = () => ({
|
|
62
|
+
hidden: false, textContent: '', _html: '',
|
|
63
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
64
|
+
});
|
|
65
|
+
const $ = (sel) => { if (!els.has(sel)) els.set(sel, makeEl()); return els.get(sel); };
|
|
66
|
+
// Mirror the markup: the section ships with the `hidden` attribute.
|
|
67
|
+
$('#xp-scroll').hidden = true;
|
|
68
|
+
|
|
69
|
+
const escapeHtml = (s) => String(s)
|
|
70
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
71
|
+
const api = {
|
|
72
|
+
async request(_method, url) {
|
|
73
|
+
if (url === '/api/bongos/me') return me ?? { status: 401, ok: false, data: null };
|
|
74
|
+
if (/\/builders\/[^/]+\/profile$/.test(url)) return profile ?? { status: 404, ok: false, data: null };
|
|
75
|
+
return { status: 404, ok: false, data: null };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const windowObj = {
|
|
79
|
+
__BRANDING__: {},
|
|
80
|
+
BongosClient: { createClient: () => api },
|
|
81
|
+
OTB: {
|
|
82
|
+
$, escapeHtml,
|
|
83
|
+
fmtNum: (n) => String(Number(n) || 0),
|
|
84
|
+
fmtDate: (d) => String(d).slice(0, 10),
|
|
85
|
+
rankLabel: (r) => r,
|
|
86
|
+
toast() {},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
const sandbox = {
|
|
90
|
+
window: windowObj, location: { search }, URLSearchParams, console,
|
|
91
|
+
};
|
|
92
|
+
sandbox.globalThis = sandbox;
|
|
93
|
+
vm.createContext(sandbox);
|
|
94
|
+
vm.runInContext(SRC, sandbox, { filename: 'profile.js' });
|
|
95
|
+
// load() is fire-and-forget; let its await chain settle.
|
|
96
|
+
for (let i = 0; i < 4; i++) await new Promise((r) => setImmediate(r));
|
|
97
|
+
return { els, $ };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const meOk = (id) => ({ status: 200, ok: true, data: { builder: { id } } });
|
|
101
|
+
const profileOk = (data) => ({ status: 200, ok: true, data });
|
|
102
|
+
|
|
103
|
+
// ---- the markup ships the section hidden ----------------------------------
|
|
104
|
+
|
|
105
|
+
test('profile.html carries the hidden cross-project section + reserved slots', () => {
|
|
106
|
+
assert.match(HTML, /<section[^>]*id="xp-scroll"[^>]*hidden>/, 'the section must ship hidden — it reveals only when the response carries a cross_project key');
|
|
107
|
+
assert.match(HTML, /Reserved slots[\s\S]*connections, guilds, and actions/, 'the spec Decision 5 slot reservation must stay in the layout');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---- vanilla instance: key absent → section absent -------------------------
|
|
111
|
+
|
|
112
|
+
test('no cross_project key (vanilla instance) → the section stays hidden', async () => {
|
|
113
|
+
const { $ } = await boot({ search: '?id=7', me: meOk(99), profile: profileOk(baseProfile()) });
|
|
114
|
+
assert.equal($('#xp-scroll').hidden, true);
|
|
115
|
+
assert.equal($('#xp-body').innerHTML, '');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---- cross_project: null → private line, nothing derived -------------------
|
|
119
|
+
|
|
120
|
+
test('cross_project null, stranger view → private line only, no numbers, no self link', async () => {
|
|
121
|
+
const data = { ...baseProfile(), cross_project: null };
|
|
122
|
+
const { $ } = await boot({ search: '?id=7', me: meOk(99), profile: profileOk(data) });
|
|
123
|
+
const xp = $('#xp-scroll');
|
|
124
|
+
const html = $('#xp-body').innerHTML;
|
|
125
|
+
assert.equal(xp.hidden, false, 'on the hub the section reveals even when the record is private');
|
|
126
|
+
assert.match(html, /keeps their track record private/);
|
|
127
|
+
assert.ok(!html.includes('stat-tile'), 'no tiles in the private state');
|
|
128
|
+
assert.ok(!/\d/.test(html), 'no digits at all — no partial numbers (leak by subtraction)');
|
|
129
|
+
assert.ok(!html.includes('/settings#cross-project'), 'no self link for a stranger');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('cross_project null, self view via ?id= → private line + the Settings link', async () => {
|
|
133
|
+
const data = { ...baseProfile(), cross_project: null };
|
|
134
|
+
// /me id as a STRING (pg bigint serialization) against the numeric profile id
|
|
135
|
+
// — self-detection must be serialization-proof.
|
|
136
|
+
const { $ } = await boot({ search: '?id=7', me: meOk('7'), profile: profileOk(data) });
|
|
137
|
+
const html = $('#xp-body').innerHTML;
|
|
138
|
+
assert.match(html, /keeps their track record private/);
|
|
139
|
+
assert.ok(html.includes('/settings#cross-project'), 'the self hide-toggle links to Settings → Cross-project record');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('self view with no ?id= (own-profile path) also gets the Settings link', async () => {
|
|
143
|
+
const data = { ...baseProfile(), cross_project: null };
|
|
144
|
+
const { $ } = await boot({ search: '', me: meOk(7), profile: profileOk(data) });
|
|
145
|
+
assert.ok($('#xp-body').innerHTML.includes('/settings#cross-project'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---- the full record -------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
test('full cross_project → tiles, named rows, plain-text standing, +N more', async () => {
|
|
151
|
+
const data = { ...baseProfile(), cross_project: CP_FULL };
|
|
152
|
+
const { $ } = await boot({ search: '?id=7', me: meOk(99), profile: profileOk(data) });
|
|
153
|
+
const xp = $('#xp-scroll');
|
|
154
|
+
const html = $('#xp-body').innerHTML;
|
|
155
|
+
assert.equal(xp.hidden, false);
|
|
156
|
+
assert.equal((html.match(/stat-tile__num/g) || []).length, 4, 'projects / works shipped / credits / karma tiles');
|
|
157
|
+
assert.match(html, /Public Proj/);
|
|
158
|
+
assert.ok(html.includes('href="https://pub.example"'), 'a well-formed https origin links out');
|
|
159
|
+
assert.match(html, /Odd Origin/);
|
|
160
|
+
assert.ok(!html.includes('href="http://insecure.example"'), 'a non-https origin renders as plain text');
|
|
161
|
+
assert.match(html, /Metic/, 'rank_held renders capitalized as that project\'s own standing');
|
|
162
|
+
assert.ok(!html.includes('rank-badge'), 'never the local rank-badge component for another project\'s rank (spec Decision 1)');
|
|
163
|
+
assert.match(html, /\+ 1 more project/, 'non-public projects are counted, never named');
|
|
164
|
+
assert.match(html, /Disciplines: engineer, ui/);
|
|
165
|
+
assert.match(html, /Last active 2026-08-01/);
|
|
166
|
+
assert.ok(!html.includes('/settings#cross-project'), 'no self link for a stranger');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('zero-activity record → the empty line, no project table', async () => {
|
|
170
|
+
const cp = { ...CP_FULL, projects: [], more_projects: 0 };
|
|
171
|
+
const { $ } = await boot({ search: '?id=7', me: meOk(99), profile: profileOk({ ...baseProfile(), cross_project: cp }) });
|
|
172
|
+
const html = $('#xp-body').innerHTML;
|
|
173
|
+
assert.match(html, /No cross-project activity reported yet\./);
|
|
174
|
+
assert.ok(!html.includes('<table'), 'no empty ledger');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---- signed-out empty state ------------------------------------------------
|
|
178
|
+
|
|
179
|
+
test('signed-out visitor with no ?id= → empty state keeps the section hidden', async () => {
|
|
180
|
+
const { $ } = await boot({ search: '', me: { status: 401, ok: false, data: null } });
|
|
181
|
+
assert.equal($('#xp-scroll').hidden, true);
|
|
182
|
+
});
|