@bongos/core 1.19.513 → 1.19.515

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.
@@ -0,0 +1,370 @@
1
+ // tests/mingle.mjs — mingle is two switches, cross-craft, and a decline that
2
+ // costs nothing (task 1003177 / C07, goal 1000075; criterion
3
+ // collab-mingle-two-switches, ADR 0187 §5).
4
+ //
5
+ // The criterion names three testable things, and the owner attached the third as
6
+ // a condition of the feature rather than a follow-up:
7
+ // 1. a pair is only ever CROSS-CRAFT;
8
+ // 2. disabling EITHER switch prevents pairing;
9
+ // 3. a decline carries NO PENALTY — "and that has to be verifiable, not just
10
+ // stated, or 'without penalty' is a claim rather than a property".
11
+ //
12
+ // (3) is the reason this file drives the REAL functions against a RECORDING pool
13
+ // rather than asserting over prose. "No penalty" is a claim about what the code
14
+ // does NOT do, and the only honest way to test an absence is to capture every
15
+ // statement the code issues and assert on the whole set.
16
+ import { strict as assert } from 'node:assert';
17
+ import { test } from 'node:test';
18
+ import { createRequire } from 'node:module';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const mingle = require(path.join(ROOT, 'modules', 'lifecycle', 'mingle.js'));
26
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
27
+
28
+ const SRC = read('modules', 'lifecycle', 'mingle.js');
29
+ const ROUTES = read('modules', 'lifecycle', 'routes', 'mingle.js');
30
+ const MIGRATION = read('modules', 'lifecycle', 'migrations', 'lifecycle_007_mingle.sql');
31
+
32
+ // A pool that answers the module's real queries and RECORDS every one of them.
33
+ // The record is what makes the no-penalty test possible: it is an assertion about
34
+ // the complete set of statements a decline issues, not about the one we expected.
35
+ function recordingPool({ eligible = [], openPairs = [], pairRow = null } = {}) {
36
+ const queries = [];
37
+ return {
38
+ queries,
39
+ async query(sql, params) {
40
+ queries.push({ sql, params });
41
+ if (/FROM lifecycle_mingle_optin o/.test(sql)) {
42
+ return { rows: eligible.map((b) => ({ builder_id: b.builder_id, crafts: b.crafts })) };
43
+ }
44
+ if (/SELECT opted_in, updated_at FROM lifecycle_mingle_optin/.test(sql)) {
45
+ return { rows: [{ opted_in: true, updated_at: '2026-09-03T00:00:00Z' }] };
46
+ }
47
+ if (/INSERT INTO lifecycle_mingle_optin/.test(sql)) {
48
+ return { rows: [{ builder_id: params[0], opted_in: params[1], updated_at: 'now' }] };
49
+ }
50
+ if (/SELECT builder_lo::text AS lo/.test(sql)) {
51
+ return { rows: openPairs.map(([lo, hi]) => ({ lo, hi })) };
52
+ }
53
+ if (/INSERT INTO lifecycle_mingle_pairs/.test(sql)) {
54
+ // The round writes ONE bulk insert via unnest(), so the params are four
55
+ // parallel arrays rather than four scalars.
56
+ const [los, his, cl, ch] = params;
57
+ return {
58
+ rows: los.map((lo, i) => ({
59
+ id: i + 1, builder_lo: lo, builder_hi: his[i], craft_lo: cl[i], craft_hi: ch[i],
60
+ status: 'proposed', created_at: 'now',
61
+ })),
62
+ };
63
+ }
64
+ if (/UPDATE lifecycle_mingle_pairs/.test(sql)) {
65
+ return { rows: pairRow ? [pairRow] : [] };
66
+ }
67
+ if (/FROM lifecycle_mingle_pairs p/.test(sql)) return { rows: [] };
68
+ throw new Error(`unexpected query: ${sql.slice(0, 70)}`);
69
+ },
70
+ };
71
+ }
72
+
73
+ const on = () => async () => ({ value: 'true' });
74
+ const off = () => async () => null;
75
+
76
+ // ── 1. A PAIR IS ONLY EVER CROSS-CRAFT ──────────────────────────────────────
77
+
78
+ test('THE RULE: crafts must be DISJOINT, not merely different', () => {
79
+ assert.equal(mingle.isCrossCraft(['builder'], ['artist']), true);
80
+ assert.equal(mingle.isCrossCraft(['builder'], ['builder']), false);
81
+ // The case a "different primary craft" rule would get wrong: someone who is
82
+ // both shares a craft with a pure engineer, so pairing them puts two of the
83
+ // same in a room and calls it cross-craft.
84
+ assert.equal(mingle.isCrossCraft(['artist', 'builder'], ['builder']), false);
85
+ assert.equal(mingle.isCrossCraft(['artist', 'builder'], ['ideator']), true);
86
+ });
87
+
88
+ test('a builder with NO craft is never pairable', () => {
89
+ // Belonging to a craft is a claim about yourself the system must not assume —
90
+ // the same fail-closed answer help-requests gives.
91
+ assert.equal(mingle.isCrossCraft([], ['artist']), false);
92
+ assert.equal(mingle.isCrossCraft(['artist'], []), false);
93
+ const pairs = mingle.planPairs([
94
+ { builder_id: '1', crafts: [] },
95
+ { builder_id: '2', crafts: ['artist'] },
96
+ ]);
97
+ assert.deepEqual(pairs, []);
98
+ });
99
+
100
+ test('EVERY pair the planner emits is cross-craft', () => {
101
+ const eligible = [
102
+ { builder_id: '10', crafts: ['builder'] },
103
+ { builder_id: '20', crafts: ['builder'] },
104
+ { builder_id: '30', crafts: ['artist'] },
105
+ { builder_id: '40', crafts: ['ideator'] },
106
+ { builder_id: '50', crafts: ['artist', 'builder'] },
107
+ ];
108
+ const byId = new Map(eligible.map((b) => [b.builder_id, b.crafts]));
109
+ const pairs = mingle.planPairs(eligible);
110
+ assert.ok(pairs.length > 0, 'the planner found pairs at all');
111
+ for (const p of pairs) {
112
+ assert.equal(mingle.isCrossCraft(byId.get(p.builder_lo), byId.get(p.builder_hi)), true,
113
+ `${p.builder_lo}/${p.builder_hi} share a craft`);
114
+ assert.notEqual(p.craft_lo, p.craft_hi, 'the recorded crafts differ');
115
+ }
116
+ });
117
+
118
+ test('nobody is put in two pairs in one round', () => {
119
+ const pairs = mingle.planPairs([
120
+ { builder_id: '1', crafts: ['builder'] },
121
+ { builder_id: '2', crafts: ['artist'] },
122
+ { builder_id: '3', crafts: ['ideator'] },
123
+ { builder_id: '4', crafts: ['ui'] },
124
+ ]);
125
+ const seen = new Set();
126
+ for (const p of pairs) {
127
+ for (const id of [p.builder_lo, p.builder_hi]) {
128
+ assert.ok(!seen.has(id), `${id} appears twice`);
129
+ seen.add(id);
130
+ }
131
+ }
132
+ });
133
+
134
+ test('CROSS-CRAFT IS ALSO A DATABASE CONSTRAINT, not only a matcher rule', () => {
135
+ // Enforcing it solely in the matcher leaves any future writer — a backfill, a
136
+ // second caller, a fixture — free to violate the property the feature is named
137
+ // for.
138
+ assert.match(MIGRATION, /CHECK \(craft_lo <> craft_hi\)/);
139
+ assert.match(MIGRATION, /CHECK \(builder_lo <> builder_hi\)/);
140
+ });
141
+
142
+ test('the matching rule is RECORDED, and the route serves it verbatim', () => {
143
+ // "chosen deliberately and recorded" — a reader can fetch the rule rather than
144
+ // infer it from SQL.
145
+ const m = mingle.matchingModel();
146
+ assert.equal(m.rule, 'cross_craft');
147
+ assert.equal(m.overlap, 'none');
148
+ assert.ok(m.description && m.consent && m.decline);
149
+ assert.match(m.rejected, /same_craft/, 'the road not taken is recorded too');
150
+ assert.match(ROUTES, /matching: mingle\.matchingModel\(\)/);
151
+ });
152
+
153
+ // ── 2. DISABLING EITHER SWITCH PREVENTS PAIRING ─────────────────────────────
154
+
155
+ test('SWITCH 1 OFF: the project disabled means no pairing, and no write at all', async () => {
156
+ const pool = recordingPool({ eligible: [
157
+ { builder_id: '1', crafts: ['builder'] },
158
+ { builder_id: '2', crafts: ['artist'] },
159
+ ] });
160
+ const out = await mingle.runMingleRound({}, { pool, getSetting: off() });
161
+ assert.deepEqual(out.paired, []);
162
+ assert.equal(out.skipped, 'project_disabled');
163
+ // Not "paired then discarded" — it never got as far as reading who is eligible.
164
+ assert.equal(pool.queries.length, 0, 'a disabled project issues no query');
165
+ });
166
+
167
+ test('SWITCH 2 OFF: eligibility is opted-in only, enforced in SQL', async () => {
168
+ const pool = recordingPool({ eligible: [] });
169
+ await mingle.eligibleBuilders({ pool });
170
+ const sql = pool.queries[0].sql;
171
+ assert.match(sql, /o\.opted_in = true/, 'only people who opted in');
172
+ assert.match(sql, /b\.status = 'active'/, 'and only active builders');
173
+ });
174
+
175
+ test('BOTH ON is the only state that pairs', async () => {
176
+ const pool = recordingPool({ eligible: [
177
+ { builder_id: '1', crafts: ['builder'] },
178
+ { builder_id: '2', crafts: ['artist'] },
179
+ ] });
180
+ const out = await mingle.runMingleRound({}, { pool, getSetting: on() });
181
+ assert.equal(out.skipped, null);
182
+ assert.equal(out.paired.length, 1);
183
+ assert.equal(out.paired[0].builder_lo, '1');
184
+ });
185
+
186
+ test('a round is ONE insert, so a partial round cannot be written', () => {
187
+ const pool = recordingPool({ eligible: [
188
+ { builder_id: '1', crafts: ['builder'] },
189
+ { builder_id: '2', crafts: ['artist'] },
190
+ { builder_id: '3', crafts: ['ideator'] },
191
+ { builder_id: '4', crafts: ['ui'] },
192
+ ] });
193
+ return mingle.runMingleRound({}, { pool, getSetting: on() }).then((out) => {
194
+ assert.equal(out.paired.length, 2, 'two pairs from four builders');
195
+ const inserts = pool.queries.filter((q) => /INSERT INTO lifecycle_mingle_pairs/.test(q.sql));
196
+ assert.equal(inserts.length, 1, 'one statement for the whole round, not one per pair');
197
+ assert.match(inserts[0].sql, /unnest\(\$1::bigint\[\]/);
198
+ });
199
+ });
200
+
201
+ test('an empty plan writes nothing at all', async () => {
202
+ // Two builders of the SAME craft are not a pair, so there is nothing to insert
203
+ // — and no empty INSERT should be issued to discover that.
204
+ const pool = recordingPool({ eligible: [
205
+ { builder_id: '1', crafts: ['builder'] },
206
+ { builder_id: '2', crafts: ['builder'] },
207
+ ] });
208
+ const out = await mingle.runMingleRound({}, { pool, getSetting: on() });
209
+ assert.deepEqual(out.paired, []);
210
+ assert.equal(pool.queries.filter((q) => /INSERT/.test(q.sql)).length, 0);
211
+ });
212
+
213
+ test('FAIL CLOSED: the project switch opens only on an explicit yes', () => {
214
+ for (const yes of ['true', 'TRUE', ' on ', '1', 'yes']) {
215
+ assert.equal(mingle.parseEnabled(yes), true, `${yes} is a yes`);
216
+ }
217
+ // Absent, blank, garbage, and a value that was valid before someone tightened
218
+ // this are all NO. A consent gate that opens on an unreadable value is not a gate.
219
+ for (const no of [null, undefined, '', 'false', 'maybe', 'enabled', '2', 'off']) {
220
+ assert.equal(mingle.parseEnabled(no), false, `${String(no)} is not a yes`);
221
+ }
222
+ });
223
+
224
+ test('FAIL CLOSED: an unreadable project switch is not permission', async () => {
225
+ const enabled = await mingle.projectEnabled({ getSetting: async () => { throw new Error('db down'); } });
226
+ assert.equal(enabled, false);
227
+ });
228
+
229
+ test('the two switches are reported SEPARATELY, so a builder can see which is off', async () => {
230
+ // "You cannot mingle" with no reason is a dead end for the person who could fix
231
+ // half of it.
232
+ const pool = recordingPool();
233
+ const state = await mingle.mingleStateFor(7, { pool, getSetting: off() });
234
+ assert.equal(state.enabled, false);
235
+ assert.equal(state.project_enabled, false);
236
+ assert.equal(state.opted_in, true, 'my half is still reported as mine');
237
+ });
238
+
239
+ // ── 3. A DECLINE CARRIES NO PENALTY ─────────────────────────────────────────
240
+
241
+ // Anything that would make declining cost something. Matched against the SQL a
242
+ // decline actually issues AND against the module source.
243
+ const PENALTY_SURFACES = /credit|karma|rank|streak|achievement|reward|score|penalt|cooldown|strike/i;
244
+
245
+ test('THE NO-PENALTY PROPERTY: a decline writes ONE row in ONE table', async () => {
246
+ const pool = recordingPool({ pairRow: { id: 5, builder_lo: '1', builder_hi: '2', craft_lo: 'builder', craft_hi: 'artist', status: 'declined', decided_by: '1', decided_at: 'now' } });
247
+ const out = await mingle.decidePair(5, 1, 'declined', { pool });
248
+ assert.equal(out.pair.status, 'declined');
249
+
250
+ assert.equal(pool.queries.length, 1, 'exactly one statement');
251
+ const { sql } = pool.queries[0];
252
+ assert.match(sql, /UPDATE lifecycle_mingle_pairs/);
253
+ // The whole point: nothing else was touched. This is an assertion about the
254
+ // COMPLETE set of statements, which is the only honest way to test an absence.
255
+ assert.ok(!PENALTY_SURFACES.test(sql), `a decline touched a penalty surface: ${sql}`);
256
+ });
257
+
258
+ test('NO FUTURE MATCHING WEIGHT: the eligibility read cannot see pair history', async () => {
259
+ // This is the structural half, and it is what makes "no penalty" a property
260
+ // rather than a promise: the read that decides who may be paired has no way to
261
+ // learn that someone declined, so a decline cannot lower their odds.
262
+ const pool = recordingPool({ eligible: [] });
263
+ await mingle.eligibleBuilders({ pool });
264
+ const sql = pool.queries[0].sql;
265
+ assert.ok(!/lifecycle_mingle_pairs/.test(sql),
266
+ 'eligibleBuilders must never join the pairs table');
267
+ assert.ok(!PENALTY_SURFACES.test(sql), 'eligibility reads no penalty surface either');
268
+ });
269
+
270
+ test('a declined pair may be introduced again — a decline is not a permanent split', () => {
271
+ // The only thing pair history gates is a second OPEN proposal for the same two
272
+ // people (the unique index's job). A settled pair is absent from that set, so
273
+ // it can come back. A permanent exclusion would be a consequence, and declining
274
+ // is free.
275
+ assert.match(MIGRATION, /CREATE UNIQUE INDEX[\s\S]{0,200}lifecycle_mingle_pairs_open_uniq[\s\S]{0,120}WHERE status = 'proposed'/);
276
+ const eligible = [
277
+ { builder_id: '1', crafts: ['builder'] },
278
+ { builder_id: '2', crafts: ['artist'] },
279
+ ];
280
+ // With the pair already OPEN, it is skipped...
281
+ assert.deepEqual(mingle.planPairs(eligible, { excludePairs: new Set(['1:2']) }), []);
282
+ // ...and once settled it is not in the exclude set at all, so it returns.
283
+ assert.equal(mingle.planPairs(eligible, { excludePairs: new Set() }).length, 1);
284
+ });
285
+
286
+ test('the exclude set is built from OPEN pairs only, never from declines', async () => {
287
+ const pool = recordingPool({ openPairs: [['1', '2']] });
288
+ await mingle.openPairKeys({ pool });
289
+ assert.match(pool.queries[0].sql, /WHERE status = 'proposed'/);
290
+ assert.ok(!/declined/.test(pool.queries[0].sql), 'declines are not read back');
291
+ });
292
+
293
+ test('NOTHING in the module reaches a penalty surface', () => {
294
+ // The source-level backstop for the two behavioural tests above: a future
295
+ // "small" addition — bump a counter, dock some karma — fails here even if it
296
+ // never runs in a test.
297
+ //
298
+ // TWO EXEMPTIONS, AND THEY ARE THE SAME ONE. Comments discuss penalties, and so
299
+ // does the MATCHING literal — its whole job is to record, in words a surface can
300
+ // render, WHICH standing a decline does not touch. Naming a surface in order to
301
+ // disclaim it is the opposite of reaching for it, so both are stripped before
302
+ // the scan. This is the codebase's standing convention: a line that CITES a rule
303
+ // is the only thing exempt from it (scripts/gds/fitness.js says so of the
304
+ // category-advisory guard). Everything else — every identifier, every line of
305
+ // SQL — is scanned.
306
+ const code = SRC
307
+ .replace(/^\s*\/\/.*$/gm, '')
308
+ .replace(/const MATCHING = Object\.freeze\(\{[\s\S]*?\n\}\);/, '');
309
+ assert.ok(!/MATCHING = Object\.freeze/.test(code), 'the MATCHING literal was actually stripped');
310
+ assert.ok(!/credits|karma|streak|achievement/i.test(code),
311
+ 'mingle.js must not touch a credit/karma/streak/achievement surface');
312
+ });
313
+
314
+ test('the schema carries no penalty column', () => {
315
+ // A table that recorded a decline anywhere but in its own status column would be
316
+ // the penalty the criterion forbids — the same posture lifecycle_005 takes.
317
+ const ddl = MIGRATION.replace(/^\s*--.*$/gm, '');
318
+ assert.ok(!/credit|karma|streak|cooldown|penalt|count/i.test(ddl),
319
+ 'no counter, cooldown or penalty column');
320
+ });
321
+
322
+ // ── the routes ──────────────────────────────────────────────────────────────
323
+
324
+ test('consent is keyed by the SESSION, never a body field', () => {
325
+ // Consent you can give on someone else's behalf is not consent.
326
+ assert.match(ROUTES, /mingle\.setOptIn\(req\.builder\.id/);
327
+ assert.ok(!/setOptIn\(\s*req\.body/.test(ROUTES));
328
+ });
329
+
330
+ test('the operator routes gate on the ATOM; my own switch gates on nothing', () => {
331
+ // BV1.R105: a modules/*/routes gate is a permission atom, never a rank —
332
+ // tests/government_require_permission fails the build on a new requireRank, and
333
+ // the two files still spelling it that way are documented holdouts.
334
+ const projectRoute = ROUTES.slice(ROUTES.indexOf("router.put('/mingle/project'"));
335
+ assert.match(projectRoute.slice(0, 220), /requirePermission\('mingle\.manage'\)/);
336
+ const roundRoute = ROUTES.slice(ROUTES.indexOf("router.post('/mingle/round'"));
337
+ assert.match(roundRoute.slice(0, 220), /requirePermission\('mingle\.manage'\)/);
338
+
339
+ const optIn = ROUTES.slice(ROUTES.indexOf("router.put('/mingle/opt-in'"), ROUTES.indexOf("router.put('/mingle/project'"));
340
+ assert.ok(!/requirePermission|requireRank/.test(optIn),
341
+ 'opting YOURSELF in is not a privileged act');
342
+ });
343
+
344
+ test('the atom is declared in the catalog, and a permission no route consumes does not exist', () => {
345
+ const catalog = read('modules', 'government', 'catalog.js');
346
+ assert.match(catalog, /key: 'mingle\.manage'/, 'the atom is declared');
347
+ assert.match(catalog, /key: 'mingle\.manage'[^\n]*floor: 'metic'/, 'at the discord.channels.manage floor');
348
+ assert.match(catalog, /key: 'mingle\.manage'[^\n]*guards: '[^']*\/mingle\/project/, 'and it names the routes it guards');
349
+ });
350
+
351
+ test('you can only answer an introduction you are in, and the check is in SQL', () => {
352
+ const fn = SRC.slice(SRC.indexOf('async function decidePair'));
353
+ const body = fn.slice(0, fn.indexOf('\n}'));
354
+ assert.match(body, /builder_lo = \$2 OR builder_hi = \$2/,
355
+ 'membership is enforced by the UPDATE, not by a check a later caller could skip');
356
+ assert.match(body, /status = 'proposed'/, 'and a settled pair cannot be re-decided');
357
+ });
358
+
359
+ test('the route is registered in the module manifest', () => {
360
+ const manifest = JSON.parse(read('modules', 'lifecycle', 'module.json'));
361
+ assert.ok(manifest.contributes.routes.includes('mingle'));
362
+ });
363
+
364
+ test('the craft vocabulary is CONSUMED from help-requests, never redefined', () => {
365
+ // Two spellings of the same craft ('builder' the craft is 'engineer' the
366
+ // discipline) would silently match nobody.
367
+ assert.match(SRC, /require\('\.\/help-requests'\)/);
368
+ assert.ok(!/CRAFT_TO_DISCIPLINE\s*=|DISCIPLINE_TO_CRAFT\s*=/.test(SRC),
369
+ 'mingle must not declare its own craft map');
370
+ });
@@ -1858,7 +1858,11 @@ test('the kit proves the picker: a state renders it on the owner\'s feed-carried
1858
1858
 
1859
1859
  test('the feed carries the owner\'s choice through a seam: platform-identity resolves provisioning.projectPlanets optionally and spreads every platform-drawn key on every project (tasks 1003347, 1003352)', () => {
1860
1860
  const route = fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'routes', 'projects.js'), 'utf8');
1861
- assert.match(route, /api\.resolveOptional\('provisioning\.projectPlanets', async \(\) => \(\{\}\)\)\(projects\.map\(\(p\) => hostOfOrigin\(p\.origin\)\)\)/, 'optional — an instance without provisioning has no choices to carry');
1861
+ // R06 (task 1002325) narrowed the hosts asked about from `projects` to `identified`:
1862
+ // a stealth project is on the map ANONYMOUSLY (ADR 0182 D2a), so its host is kept out
1863
+ // of this enrichment entirely rather than resolved and then redacted. The seam itself
1864
+ // is unchanged — still optional, still host-matched, still spread on every project.
1865
+ assert.match(route, /api\.resolveOptional\('provisioning\.projectPlanets', async \(\) => \(\{\}\)\)\(identified\.map\(\(p\) => hostOfOrigin\(p\.origin\)\)\)/, 'optional — an instance without provisioning has no choices to carry');
1862
1866
  assert.match(route, /return \{ planet_template: c\.planet_template \|\| null, planet_accent: c\.planet_accent \|\| null,\r?\n\s*card_backdrop: c\.card_backdrop \|\| null \};/, 'the wire always carries every key — null = nothing chosen');
1863
1867
  assert.match(route, /\.\.\.planetOf\(p\),/, 'spread beside the stats');
1864
1868
  assert.doesNotMatch(route, /require\(['"][^'"]*provisioning/, 'never an import across modules');