@bongos/core 1.21.57 → 1.21.59
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 +91 -51
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +13 -2
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +2 -0
- package/docs/adr/0365-a-guild-engages-a-project-through-each-members-own-door.md +58 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +233 -6
- package/docs/api-reference.md +8 -5
- package/docs/copy-inventory.md +384 -376
- package/docs/copy-registry.json +514 -432
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +930 -919
- package/modules/hall-ui/public/goals-page.js +20 -1
- package/modules/hall-ui/public/task-lib.js +13 -0
- package/modules/hall-ui/public/work.js +22 -1
- package/modules/platform-identity/application-echo.js +11 -5
- package/modules/platform-identity/guild-engagements.js +255 -0
- package/modules/platform-identity/guilds.js +3 -1
- package/modules/platform-identity/migrations/platform_identity_030_guild_engagements.sql +64 -0
- package/modules/platform-identity/routes/guilds.js +35 -1
- package/modules/platform-identity/routes/my-projects.js +27 -3
- package/modules/platform-identity/routes/projects.js +44 -0
- package/modules/platform-identity/routes/sso.js +21 -3
- package/modules/public-landing/public/assets/cosmos.css +7 -0
- package/modules/public-landing/public/guild.html +64 -2
- package/modules/public-landing/public/guild.probes.json +13 -3
- package/modules/public-landing/public/guild.states.json +10 -5
- package/modules/public-landing/public/projects.html +252 -16
- package/modules/public-landing/public/projects.probes.json +26 -1
- package/modules/public-landing/public/projects.states.json +4 -0
- package/modules/ui-design/kit/fixtures/me__guild-engagements.json +7 -0
- package/modules/ui-design/kit/serve.js +54 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/applicant_profile_boundary.mjs +104 -1
- package/tests/application_echo.mjs +91 -5
- package/tests/focus_version_filter_default.mjs +47 -0
- package/tests/guild_engagement.mjs +881 -0
- package/tests/guild_engagement_db.mjs +240 -0
- package/tests/hall_mine_lens.mjs +1 -0
- package/tests/hub_engagement_cards.mjs +307 -0
- package/tests/hub_guild_page.mjs +94 -7
- package/tests/privacy_matrix.mjs +99 -9
- package/tests/project_invite_ui.mjs +187 -3
|
@@ -248,9 +248,18 @@ function activityScope(sql, params) {
|
|
|
248
248
|
const CLIENT = { client_id: 'client-alpha', secret_hash: null, name: 'Alpha', redirect_uris: [], origin: 'https://alpha.example', owner_github_id: '1', status: 'active' };
|
|
249
249
|
const OTHER_CLIENT = { client_id: 'client-beta', secret_hash: null, name: 'Beta', redirect_uris: [], origin: 'https://beta.example', owner_github_id: '2', status: 'active' };
|
|
250
250
|
// (github_id, client_id) pairs the HUB witnessed, and whether they are inside
|
|
251
|
-
// the read-time window. Only PUB and PRIVOUT applied to alpha.
|
|
251
|
+
// the read-time window. Only PUB and PRIVOUT applied to alpha. An echo may carry
|
|
252
|
+
// the guild the application was made with (task 1002303, ADR 0365 D5).
|
|
252
253
|
let ECHOES = [];
|
|
253
254
|
|
|
255
|
+
// The guilds an echo can name, and who is (still) in them.
|
|
256
|
+
const GUILDS = [
|
|
257
|
+
{ id: '81', slug: 'open-guild', name: 'Open Guild', visibility: 'public' },
|
|
258
|
+
{ id: '82', slug: 'quiet-guild', name: 'Quiet Guild', visibility: 'unlisted' },
|
|
259
|
+
];
|
|
260
|
+
let GUILD_MEMBERS = [];
|
|
261
|
+
let labelFails = false;
|
|
262
|
+
|
|
254
263
|
let calls = [];
|
|
255
264
|
const pool = {
|
|
256
265
|
query: async (text, params = []) => {
|
|
@@ -262,6 +271,28 @@ const pool = {
|
|
|
262
271
|
return { rows: hit ? [{ ...hit }] : [], rowCount: hit ? 1 : 0 };
|
|
263
272
|
}
|
|
264
273
|
|
|
274
|
+
// THE GUILD LABEL (guild-engagements.js echoGuildLabel). Its membership
|
|
275
|
+
// requirement is a JOIN, so the join is read rather than assumed: drop it
|
|
276
|
+
// and an applicant who LEFT the guild genuinely keeps the label here.
|
|
277
|
+
if (/^SELECT g\.slug, g\.name, g\.visibility FROM platform_identity_application_echoes ae /.test(sql)) {
|
|
278
|
+
if (labelFails) throw new Error('the guild tables are on fire');
|
|
279
|
+
const m = /^SELECT g\.slug, g\.name, g\.visibility FROM platform_identity_application_echoes ae (.+?) WHERE (.+?) LIMIT 1$/.exec(sql);
|
|
280
|
+
if (!m) throw new Error(`fake pool: the label read moved — ${sql}`);
|
|
281
|
+
if (!/^JOIN platform_identity_guilds g ON g\.id = ae\.guild_id\b/.test(m[1])) throw new Error(`fake pool: the label join moved — ${m[1]}`);
|
|
282
|
+
const rest = m[1].replace(/^JOIN platform_identity_guilds g ON g\.id = ae\.guild_id/, '').trim();
|
|
283
|
+
const mustBeMember = rest === 'JOIN platform_identity_guild_members m ON m.guild_id = ae.guild_id AND m.github_id = ae.github_id';
|
|
284
|
+
if (rest && !mustBeMember) throw new Error(`fake pool: unmodelled label join — ${rest}`);
|
|
285
|
+
assert.equal(m[2], 'ae.github_id = $1::bigint AND ae.client_id = $2 AND ae.applied_at > now() - make_interval(days => $3::int)',
|
|
286
|
+
'the label reads ONE echo — this account, this client, inside the 30-day window');
|
|
287
|
+
assert.equal(params[2], 30);
|
|
288
|
+
const rows = ECHOES
|
|
289
|
+
.filter((e) => String(e.github_id) === String(params[0]) && e.client_id === params[1] && e.in_window && e.guild_id)
|
|
290
|
+
.filter((e) => !mustBeMember || GUILD_MEMBERS.some((gm) => gm.guild_id === e.guild_id && gm.github_id === String(e.github_id)))
|
|
291
|
+
.map((e) => GUILDS.find((g) => g.id === e.guild_id))
|
|
292
|
+
.map((g) => ({ slug: g.slug, name: g.name, visibility: g.visibility, id: g.id }));
|
|
293
|
+
return { rows: rows.slice(0, 1), rowCount: Math.min(rows.length, 1) };
|
|
294
|
+
}
|
|
295
|
+
|
|
265
296
|
// The GATE's own read. The window term is modelled explicitly rather than
|
|
266
297
|
// waved through: an echo outside it must stop working, and a fake that
|
|
267
298
|
// ignored `applied_at` would make that untestable.
|
|
@@ -584,6 +615,78 @@ test('a real applicant resolves, uncacheable, through the gate', async () => {
|
|
|
584
615
|
'a cached applicant view is a snapshot, which is the one thing "live at review time" forbids');
|
|
585
616
|
});
|
|
586
617
|
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// The guild label (task 1002303, ADR 0365 D5): { profile, guild }
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
const labelReads = () => calls.filter((c) => /FROM platform_identity_application_echoes ae /.test(c.sql));
|
|
623
|
+
|
|
624
|
+
test('the guild label: { slug, name } beside the profile while the applicant is still in the guild', async () => {
|
|
625
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true, guild_id: '81' }];
|
|
626
|
+
GUILD_MEMBERS = [{ guild_id: '81', github_id: PUB }];
|
|
627
|
+
labelFails = false;
|
|
628
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
629
|
+
assert.equal(r.status, 200);
|
|
630
|
+
assert.deepEqual(Object.keys(r.json).sort(), ['guild', 'profile'], 'the label sits BESIDE the profile, never inside it');
|
|
631
|
+
assert.equal(r.json.profile.view, VIEW_PUBLIC);
|
|
632
|
+
assert.deepEqual(r.json.guild, { slug: 'open-guild', name: 'Open Guild' }, 'a slug and a name — no guild id, no visibility');
|
|
633
|
+
assert.equal(labelReads()[0].params[1], CLIENT.client_id, 'asked about the AUTHENTICATED client');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test('the guild label: an UNLISTED guild is named with slug null — it has no public page to open', async () => {
|
|
637
|
+
ECHOES = [{ github_id: PRIVOUT, client_id: CLIENT.client_id, in_window: true, guild_id: '82' }];
|
|
638
|
+
GUILD_MEMBERS = [{ guild_id: '82', github_id: PRIVOUT }];
|
|
639
|
+
labelFails = false;
|
|
640
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PRIVOUT });
|
|
641
|
+
assert.equal(r.json.profile.view, VIEW_SLIVER);
|
|
642
|
+
assert.deepEqual(r.json.guild, { slug: null, name: 'Quiet Guild' });
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test('the guild label: null once the applicant LEFT the guild, and null for a solo application', async () => {
|
|
646
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true, guild_id: '81' }];
|
|
647
|
+
GUILD_MEMBERS = [];
|
|
648
|
+
labelFails = false;
|
|
649
|
+
const left = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
650
|
+
assert.equal(left.json.profile.view, VIEW_PUBLIC);
|
|
651
|
+
assert.equal(left.json.guild, null, 'a guild they are no longer in does not vouch for them');
|
|
652
|
+
|
|
653
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true, guild_id: null }];
|
|
654
|
+
GUILD_MEMBERS = [{ guild_id: '81', github_id: PUB }];
|
|
655
|
+
const solo = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
656
|
+
assert.equal(solo.json.guild, null);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('the guild label: null whenever the profile is null — and then not even read', async () => {
|
|
660
|
+
ECHOES = [{ github_id: PROV, client_id: CLIENT.client_id, in_window: true, guild_id: '81' }];
|
|
661
|
+
GUILD_MEMBERS = [{ guild_id: '81', github_id: PROV }];
|
|
662
|
+
labelFails = false;
|
|
663
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PROV });
|
|
664
|
+
assert.equal(r.status, 200);
|
|
665
|
+
assert.equal(r.json.profile, null);
|
|
666
|
+
assert.equal(r.json.guild, null, 'an applicant who discloses nothing discloses no guild either');
|
|
667
|
+
assert.deepEqual(labelReads(), [], 'the label is read only beside a profile');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test('the guild label is BEST-EFFORT: a failed read answers guild null and never costs the profile', async () => {
|
|
671
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true, guild_id: '81' }];
|
|
672
|
+
GUILD_MEMBERS = [{ guild_id: '81', github_id: PUB }];
|
|
673
|
+
labelFails = true;
|
|
674
|
+
try {
|
|
675
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
676
|
+
assert.equal(r.status, 200);
|
|
677
|
+
assert.equal(r.json.profile.view, VIEW_PUBLIC);
|
|
678
|
+
assert.equal(r.json.guild, null);
|
|
679
|
+
} finally { labelFails = false; }
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('the guild label waits behind the echo gate: no application, no label read', async () => {
|
|
683
|
+
ECHOES = [{ github_id: PUB, client_id: OTHER_CLIENT.client_id, in_window: true, guild_id: '81' }];
|
|
684
|
+
GUILD_MEMBERS = [{ guild_id: '81', github_id: PUB }];
|
|
685
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
686
|
+
assert.equal(r.status, 404);
|
|
687
|
+
assert.deepEqual(labelReads(), []);
|
|
688
|
+
});
|
|
689
|
+
|
|
587
690
|
test('bad credentials are refused before anything is read', async () => {
|
|
588
691
|
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true }];
|
|
589
692
|
const r = await post('/api/bongos/sso/applicant-profile', { client_id: CLIENT.client_id, client_secret: 'wrong', github_id: PUB });
|
|
@@ -75,7 +75,17 @@ test('the write is an upsert on the pair, refreshing the date (D1.4)', async ()
|
|
|
75
75
|
const [{ sql, params }] = pool.calls;
|
|
76
76
|
assert.match(sql, /ON CONFLICT \(github_id, client_id\) DO UPDATE SET applied_at = now\(\)/,
|
|
77
77
|
're-applying is a success, so a second ask refreshes rather than erroring or piling up a row');
|
|
78
|
-
assert.deepEqual(params, [String(ME), 'proj-a']);
|
|
78
|
+
assert.deepEqual(params, [String(ME), 'proj-a', null], 'a solo application binds NO guild');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('the write carries the guild the application was made with, and every upsert rewrites it (task 1002303, ADR 0365 D1)', async () => {
|
|
82
|
+
const pool = recorder();
|
|
83
|
+
await recordApplication({ githubId: ME, clientId: 'proj-a', guildId: 71 }, { pool });
|
|
84
|
+
const [{ sql, params }] = pool.calls;
|
|
85
|
+
assert.match(sql, /INSERT INTO platform_identity_application_echoes \(github_id, client_id, applied_at, guild_id\) VALUES \(\$1::bigint, \$2, now\(\), \$3::bigint\)/);
|
|
86
|
+
assert.match(sql, /DO UPDATE SET applied_at = now\(\), guild_id = EXCLUDED\.guild_id$/,
|
|
87
|
+
'the most recent application decides the label: a later solo one writes NULL over it');
|
|
88
|
+
assert.deepEqual(params, [String(ME), 'proj-a', '71']);
|
|
79
89
|
});
|
|
80
90
|
|
|
81
91
|
test('neither function carries a status, because the table holds none', async () => {
|
|
@@ -106,6 +116,10 @@ let echoFails = false;
|
|
|
106
116
|
let statements = [];
|
|
107
117
|
let account = null;
|
|
108
118
|
let accountFails = false;
|
|
119
|
+
// (session, guild slug, client) triples that resolve to an engagement for the
|
|
120
|
+
// join's `guild` field (task 1002303).
|
|
121
|
+
const ENGAGED = [{ github_id: ME, slug: 'guild-a', client_id: 'proj-open', guild_id: '71' }];
|
|
122
|
+
let engagementFails = false;
|
|
109
123
|
|
|
110
124
|
function seed() {
|
|
111
125
|
const now = Date.now();
|
|
@@ -128,6 +142,7 @@ function seed() {
|
|
|
128
142
|
{ client_id: 'proj-invited', membership_kind: 'pending', joined_at: now - DAY, last_active: null, project_name: 'Invited', project_origin: 'https://invited.example', owner_github_id: null },
|
|
129
143
|
];
|
|
130
144
|
echoFails = false;
|
|
145
|
+
engagementFails = false;
|
|
131
146
|
statements = [];
|
|
132
147
|
// An account that has cleared the consent floor: active, and the platform
|
|
133
148
|
// terms accepted (account-visibility.js isAccountActive).
|
|
@@ -238,13 +253,28 @@ const pool = {
|
|
|
238
253
|
|
|
239
254
|
if (/^INSERT INTO platform_identity_application_echoes/.test(sql)) {
|
|
240
255
|
if (echoFails) throw new Error('echo table is on fire');
|
|
241
|
-
const [githubId, clientId] = params;
|
|
256
|
+
const [githubId, clientId, guildId = null] = params;
|
|
257
|
+
// The conflict branch is READ from the statement, so dropping the guild_id
|
|
258
|
+
// rewrite from it genuinely leaves a stale label behind (task 1002303).
|
|
259
|
+
const rewritesGuild = /DO UPDATE SET .*\bguild_id = EXCLUDED\.guild_id\b/.test(sql);
|
|
242
260
|
const existing = echoes.find((e) => String(e.github_id) === String(githubId) && e.client_id === clientId);
|
|
243
|
-
if (existing)
|
|
244
|
-
|
|
261
|
+
if (existing) {
|
|
262
|
+
existing.applied_at = Date.now();
|
|
263
|
+
if (rewritesGuild) existing.guild_id = guildId;
|
|
264
|
+
} else echoes.push({ github_id: Number(githubId), client_id: clientId, applied_at: Date.now(), guild_id: guildId });
|
|
245
265
|
return { rows: [] };
|
|
246
266
|
}
|
|
247
267
|
|
|
268
|
+
// The join's guild check (guild-engagements.js engagementFor) — the rule
|
|
269
|
+
// itself is interpreted in tests/guild_engagement.mjs; here only WHICH guild
|
|
270
|
+
// resolves for whom matters, keyed on the bound session, slug and client.
|
|
271
|
+
if (/^SELECT g\.id, g\.slug, g\.name FROM platform_identity_guild_members m /.test(sql)) {
|
|
272
|
+
if (engagementFails) throw new Error('engagement table is on fire');
|
|
273
|
+
assert.match(sql, /lower\(g\.slug\) = \$2 AND e\.client_id = \$3 LIMIT 1$/);
|
|
274
|
+
const hit = ENGAGED.find((x) => String(params[0]) === String(x.github_id) && params[1] === x.slug && params[2] === x.client_id);
|
|
275
|
+
return { rows: hit ? [{ id: hit.guild_id, slug: hit.slug, name: hit.slug }] : [] };
|
|
276
|
+
}
|
|
277
|
+
|
|
248
278
|
if (/FROM platform_identity_application_echoes e/.test(sql)) {
|
|
249
279
|
if (echoFails) throw new Error('echo table is on fire');
|
|
250
280
|
m = /WHERE (.+?) ORDER BY/.exec(sql);
|
|
@@ -342,7 +372,7 @@ function call(method, pathname, body) {
|
|
|
342
372
|
req.end(payload);
|
|
343
373
|
});
|
|
344
374
|
}
|
|
345
|
-
const join = (note) => call('POST', '/api/bongos/my-projects/join', { origin: 'https://target.example', note });
|
|
375
|
+
const join = (note, guild) => call('POST', '/api/bongos/my-projects/join', { origin: 'https://target.example', note, ...(guild === undefined ? {} : { guild }) });
|
|
346
376
|
|
|
347
377
|
test('the interpreter refuses a predicate it cannot read — parameterised or not', () => {
|
|
348
378
|
assert.throws(() => predicateFor('github_id = $1::bigint AND something = $2', [1, 2]), /unread predicate on \$2/);
|
|
@@ -414,6 +444,62 @@ test('a failed echo write never turns a filed application into a reported failur
|
|
|
414
444
|
assert.equal(r.json.status, 'requested');
|
|
415
445
|
});
|
|
416
446
|
|
|
447
|
+
// ── applying WITH a guild (task 1002303, ADR 0365 D2) ─────────────────────────
|
|
448
|
+
// The guild is resolved for the SESSION before anything is relayed; one that
|
|
449
|
+
// no longer resolves relays nothing and echoes nothing, and one that does is the
|
|
450
|
+
// label the echo carries until the next application rewrites it.
|
|
451
|
+
|
|
452
|
+
test('a `guild` that no longer resolves is a 409 guild_engagement_gone, with nothing relayed and no echo', async () => {
|
|
453
|
+
for (const guild of ['other-guild', 'Not A Slug!', '']) {
|
|
454
|
+
seed();
|
|
455
|
+
echoes = echoes.filter((e) => e.client_id !== 'proj-open');
|
|
456
|
+
relay = { status: 201, body: { ok: true } };
|
|
457
|
+
const r = await join('please', guild);
|
|
458
|
+
assert.equal(r.status, 409, `guild ${JSON.stringify(guild)}`);
|
|
459
|
+
assert.equal(r.json.error.code, 'guild_engagement_gone');
|
|
460
|
+
assert.equal(relayed, null, 'the project never heard of it — the applicant chose to apply WITH the guild');
|
|
461
|
+
assert.equal(echo('proj-open'), undefined);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('a guild that resolves is checked for the SESSION, relayed as an ordinary application, and lands on the echo', async () => {
|
|
466
|
+
seed();
|
|
467
|
+
echoes = echoes.filter((e) => e.client_id !== 'proj-open');
|
|
468
|
+
relay = { status: 201, body: { ok: true } };
|
|
469
|
+
const r = await join('please', 'Guild-A');
|
|
470
|
+
assert.equal(r.status, 200);
|
|
471
|
+
assert.equal(r.json.status, 'requested');
|
|
472
|
+
assert.ok(relayed, 'relayed');
|
|
473
|
+
assert.deepEqual(Object.keys(JSON.parse(relayed.init.body)).sort(), ['display_name', 'github_login', 'note'],
|
|
474
|
+
'no new field reaches the project: the label travels on the applicant-profile read (ADR 0365 D5)');
|
|
475
|
+
const check = statements.find((x) => /^SELECT g\.id, g\.slug, g\.name FROM platform_identity_guild_members/.test(x.sql));
|
|
476
|
+
assert.deepEqual(check.params.slice(0, 3), [String(ME), 'guild-a', 'proj-open'], 'the session, the canonical slug, the resolved client');
|
|
477
|
+
assert.equal(echo('proj-open').guild_id, '71');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('a later solo application clears the guild label — the most recent application decides', async () => {
|
|
481
|
+
seed();
|
|
482
|
+
relay = { status: 201, body: { ok: true } };
|
|
483
|
+
await join('with them', 'guild-a');
|
|
484
|
+
assert.equal(echo('proj-open').guild_id, '71');
|
|
485
|
+
await join('on my own');
|
|
486
|
+
assert.equal(echo('proj-open').guild_id, null);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test('a refused application leaves no echo even with a guild, and a broken guild check relays nothing', async () => {
|
|
490
|
+
seed();
|
|
491
|
+
echoes = echoes.filter((e) => e.client_id !== 'proj-open');
|
|
492
|
+
relay = { status: 403, body: { error: { code: 'invite_only' } } };
|
|
493
|
+
assert.equal((await join(undefined, 'guild-a')).json.status, 'invite_only');
|
|
494
|
+
assert.equal(echo('proj-open'), undefined);
|
|
495
|
+
|
|
496
|
+
seed();
|
|
497
|
+
engagementFails = true;
|
|
498
|
+
const r = await join(undefined, 'guild-a');
|
|
499
|
+
assert.equal(r.status, 500, 'a label the hub cannot verify is never attached by guesswork');
|
|
500
|
+
assert.equal(relayed, null);
|
|
501
|
+
});
|
|
502
|
+
|
|
417
503
|
// ── the vouch the relay carries (R09, task 1002285; ADR 0201 D2) ─────────────
|
|
418
504
|
// The ECHO above is what authorizes the project's later profile read
|
|
419
505
|
// (applicationBacksProfileRead); the VOUCH is what tells the project whose
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// tests/focus_version_filter_default.mjs — the Goals page and Tasks board open their
|
|
2
|
+
// Version filter on the project's focus version (task 1004552). The decision is the
|
|
3
|
+
// pure task-lib helper; the two pages are pinned to calling it after their options fill.
|
|
4
|
+
//
|
|
5
|
+
// Run: node --test tests/focus_version_filter_default.mjs
|
|
6
|
+
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { focusVersionDefault } = require('../modules/hall-ui/public/task-lib.js');
|
|
14
|
+
const src = (rel) => readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
|
|
15
|
+
const opts = ['', 'BONGOS-V1', 'BONGOS-V2'];
|
|
16
|
+
|
|
17
|
+
test('a starred version is the default the filter opens on', () => {
|
|
18
|
+
assert.equal(focusVersionDefault({ focusId: 'BONGOS-V2', optionValues: opts, current: '' }), 'BONGOS-V2');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('no focus leaves the filter on any — nothing is guessed', () => {
|
|
22
|
+
for (const focusId of [null, undefined, '']) {
|
|
23
|
+
assert.equal(focusVersionDefault({ focusId, optionValues: opts, current: '' }), null);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('a URL filter wins over the focus default', () => {
|
|
28
|
+
assert.equal(focusVersionDefault({ focusId: 'BONGOS-V2', optionValues: opts, current: '', urlValue: 'BONGOS-V1' }), null);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('a choice the builder already made wins over the focus default', () => {
|
|
32
|
+
assert.equal(focusVersionDefault({ focusId: 'BONGOS-V2', optionValues: opts, current: 'BONGOS-V1' }), null);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a focus the select has no option for is left alone, not selected as a blank', () => {
|
|
36
|
+
assert.equal(focusVersionDefault({ focusId: 'BONGOS-V9', optionValues: opts, current: '' }), null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('both pages read the focus and apply it after their version options fill', () => {
|
|
40
|
+
const work = src('modules/hall-ui/public/work.js');
|
|
41
|
+
assert.match(work, /\/versions\/focus/);
|
|
42
|
+
assert.match(work, /populateVersionFilter\(taskState\.versions\);\s*restoreUrlFilter\('f-version'\);\s*if \(applyFocusVersion\(focusResp\)\)/);
|
|
43
|
+
const goals = src('modules/hall-ui/public/goals-page.js');
|
|
44
|
+
assert.match(goals, /\/versions\/focus/);
|
|
45
|
+
assert.match(goals, /populateVersionFilter\(\);\s*applyFocusVersion\(focusResp\);/);
|
|
46
|
+
for (const page of [work, goals]) assert.match(page, /OTBTaskLib\.focusVersionDefault\(/);
|
|
47
|
+
});
|