@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.
- package/.bongos-core.json +62 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +20 -0
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0182-project-visibility-two-axes.md +2 -0
- package/docs/api/openapi.json +351 -4
- package/docs/api-reference.md +12 -2
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +12 -0
- package/modules/government/catalog.js +6 -0
- package/modules/government/migrations/government_009_mingle_manage.sql +55 -0
- package/modules/lifecycle/migrations/lifecycle_007_mingle.sql +134 -0
- package/modules/lifecycle/mingle.js +337 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/mingle.js +116 -0
- package/modules/platform-identity/platform-identity.js +24 -20
- package/modules/platform-identity/projects-feed.js +142 -0
- package/modules/platform-identity/routes/projects.js +27 -8
- package/modules/platform-identity/tests/platform-identity.mjs +9 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/government_seed.mjs +35 -0
- package/tests/mingle.mjs +370 -0
- package/tests/projects_hub.mjs +5 -1
- package/tests/visible_projects_feed.mjs +320 -0
package/tests/mingle.mjs
ADDED
|
@@ -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
|
+
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -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
|
-
|
|
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');
|