@bongos/core 1.19.914 → 1.19.916

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,271 @@
1
+ // tests/ideas_config.mjs — the two per-instance ideator dials (BV2.R05).
2
+ //
3
+ // What this file is actually guarding is not "the config parses". It is the
4
+ // thing the task description originally got wrong: that the ignite floor is a
5
+ // SCHEDULING preference and the trusted tier is an AUTHORITY boundary, and that
6
+ // turning the first dial must not move the second. Most of the assertions below
7
+ // exist to fail loudly if someone later "simplifies" this by making METIC_PLUS
8
+ // itself configurable.
9
+
10
+ import assert from 'node:assert/strict';
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { createRequire } from 'node:module';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ // fileURLToPath, never URL.pathname — a space becomes %20 and a Windows drive
19
+ // gains a leading slash, so require()/fs throw MODULE_NOT_FOUND at load
20
+ // (task 1002889; both tests/test_path_guard.mjs and fitness.js enforce it).
21
+ const ROOT = fileURLToPath(new URL('..', import.meta.url));
22
+
23
+ const ideasConfig = require(path.join(ROOT, 'modules/ideas/config.js'));
24
+ const routing = require(path.join(ROOT, 'modules/ideas/routing.js'));
25
+ const catalog = require(path.join(ROOT, 'modules/government/catalog.js'));
26
+ const instanceConfig = require(path.join(ROOT, 'src/instance-config.js'));
27
+ const api = require(path.join(ROOT, 'src/module-api.js'));
28
+
29
+ // Env knobs are read through api.resolveEnv, which PREFIXES the suffix with the
30
+ // instance's own env prefix (OTB_, CLOUDBONGOS_, ...). Resolving it here rather
31
+ // than hard-coding one keeps this test honest on a rebranded instance — and
32
+ // hard-coding the bare suffix is exactly how the first cut of this file passed a
33
+ // name nothing reads.
34
+ const ENV_PREFIX = instanceConfig.envPrefix();
35
+ const envName = (suffix) => `${ENV_PREFIX}_${suffix}`;
36
+
37
+ let passed = 0;
38
+ let failed = 0;
39
+ async function test(name, fn) {
40
+ try { await fn(); passed++; console.log(` ok ${name}`); }
41
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
42
+ }
43
+
44
+ // A scratch pair of config paths, so nothing here touches the real files.
45
+ function withPacks(neutral, instance, fn) {
46
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ideas-config-'));
47
+ const neutralPath = path.join(dir, 'ideas.neutral.json');
48
+ const instancePath = path.join(dir, 'ideas.json');
49
+ if (neutral !== null) fs.writeFileSync(neutralPath, typeof neutral === 'string' ? neutral : JSON.stringify(neutral));
50
+ if (instance !== null) fs.writeFileSync(instancePath, typeof instance === 'string' ? instance : JSON.stringify(instance));
51
+ try { return fn({ neutralPath, instancePath }); }
52
+ finally { fs.rmSync(dir, { recursive: true, force: true }); }
53
+ }
54
+ const load = (neutral, instance, env = {}) =>
55
+ withPacks(neutral, instance, (roots) => ideasConfig.loadIdeasConfig({ env, roots }));
56
+
57
+ // --- the defaults change nothing -------------------------------------------
58
+
59
+ await test('the shipped neutral starter resolves to today\'s behaviour', () => {
60
+ const shipped = JSON.parse(fs.readFileSync(path.join(ROOT, 'config/ideas.neutral.json'), 'utf8'));
61
+ assert.equal(shipped.igniteFloor, 'metic', 'the neutral ignite floor must be metic — anything else widens on upgrade');
62
+ assert.equal(shipped.descentDecayRate, 0.5, 'decision 16 ruled halving');
63
+ });
64
+
65
+ await test('the default floor yields EXACTLY the METIC_PLUS set', () => {
66
+ // This is the assertion that makes `igniteFloor: 'metic'` a true no-op. If the
67
+ // ladder slice is ever off by one, these two sets diverge and every filing
68
+ // changes landing status silently.
69
+ const trusted = ideasConfig.trustedRanksForFloor('metic');
70
+ assert.deepEqual([...trusted].sort(), [...routing.METIC_PLUS].sort());
71
+ });
72
+
73
+ await test('a floor admits everything at or above it, and nothing below', () => {
74
+ assert.deepEqual([...ideasConfig.trustedRanksForFloor('xenos')].sort(), ['archon', 'metic', 'thetes', 'xenos']);
75
+ assert.deepEqual([...ideasConfig.trustedRanksForFloor('archon')].sort(), ['archon']);
76
+ assert.deepEqual([...ideasConfig.trustedRanksForFloor('thetes')].sort(), ['archon', 'metic', 'thetes']);
77
+ });
78
+
79
+ // --- the drift pin ----------------------------------------------------------
80
+
81
+ await test('the rank ladder is DERIVED from the doorway, not restated', () => {
82
+ // tests/rank_tier_single_source.mjs forbids a ladder literal outside its
83
+ // allowlist; this is the behavioural half of that rule. Deriving means a rank
84
+ // added to the kernel widens this floor's vocabulary with no edit here.
85
+ assert.deepEqual([...ideasConfig.RANK_ORDER], [...api.LIVE_RANK_LADDER]);
86
+ // The government catalog is a third statement of the same ladder (allowlisted
87
+ // there). If the two sources ever disagree, the ignite floor and the rank
88
+ // gates would be reading different vocabularies, so pin them together.
89
+ assert.deepEqual([...api.LIVE_RANK_LADDER], [...catalog.RANK_ORDER],
90
+ 'the kernel ladder and modules/government/catalog.js have drifted');
91
+ });
92
+
93
+ // --- precedence -------------------------------------------------------------
94
+
95
+ await test('an instance pack overrides the neutral starter', () => {
96
+ const c = load({ igniteFloor: 'metic', descentDecayRate: 0.5 }, { igniteFloor: 'xenos', descentDecayRate: 0.25 });
97
+ assert.equal(c.igniteFloor, 'xenos');
98
+ assert.equal(c.descentDecayRate, 0.25);
99
+ });
100
+
101
+ await test('an instance pack may set one dial without restating the other', () => {
102
+ const c = load({ igniteFloor: 'metic', descentDecayRate: 0.5 }, { igniteFloor: 'thetes' });
103
+ assert.equal(c.igniteFloor, 'thetes');
104
+ assert.equal(c.descentDecayRate, 0.5, 'the unnamed dial must fall through to neutral, not to zero');
105
+ });
106
+
107
+ await test('env overrides both dials, above the instance pack', () => {
108
+ const c = load({ igniteFloor: 'metic' }, { igniteFloor: 'thetes' },
109
+ { [envName('IDEAS_IGNITE_FLOOR')]: 'archon', [envName('IDEAS_DESCENT_DECAY_RATE')]: '0.75' });
110
+ assert.equal(c.igniteFloor, 'archon');
111
+ assert.equal(c.descentDecayRate, 0.75);
112
+ });
113
+
114
+ await test('an UNPREFIXED env name is inert, not a second doorway', () => {
115
+ // resolveEnv only reads <PREFIX>_<SUFFIX>. A bare name must change nothing —
116
+ // otherwise any process with a generic IDEAS_IGNITE_FLOOR in its environment
117
+ // would silently move another instance's floor.
118
+ const c = load({ igniteFloor: 'metic' }, null,
119
+ { IDEAS_IGNITE_FLOOR: 'xenos', IDEAS_DESCENT_DECAY_RATE: '1' });
120
+ assert.equal(c.igniteFloor, 'metic');
121
+ assert.equal(c.descentDecayRate, 0.5);
122
+ });
123
+
124
+ await test('an invalid env value falls through to the pack, not to nothing', () => {
125
+ const c = load({ igniteFloor: 'metic', descentDecayRate: 0.5 }, { igniteFloor: 'thetes' },
126
+ { [envName('IDEAS_IGNITE_FLOOR')]: 'everyone', [envName('IDEAS_DESCENT_DECAY_RATE')]: 'soon' });
127
+ assert.equal(c.igniteFloor, 'thetes');
128
+ assert.equal(c.descentDecayRate, 0.5);
129
+ });
130
+
131
+ await test('no config at all still resolves to the in-code defaults', () => {
132
+ // The core whose config/ directory did not ship. It must boot to today's
133
+ // behaviour, not to an empty floor or a zero rate.
134
+ const c = load(null, null);
135
+ assert.equal(c.igniteFloor, 'metic');
136
+ assert.equal(c.descentDecayRate, 0.5);
137
+ });
138
+
139
+ // --- fail to vanilla, never to dead ----------------------------------------
140
+
141
+ await test('a malformed instance pack falls back rather than bricking boot', () => {
142
+ const c = load({ igniteFloor: 'thetes', descentDecayRate: 0.25 }, '{ this is not json');
143
+ assert.equal(c.igniteFloor, 'thetes');
144
+ assert.equal(c.descentDecayRate, 0.25);
145
+ });
146
+
147
+ await test('an unknown rank name is refused, not coerced', () => {
148
+ // The dangerous direction. A typo must not silently become the widest floor.
149
+ for (const bad of ['everyone', 'METIC ', '', 'admin', 'Xeno', null, 7, {}]) {
150
+ const c = load({ igniteFloor: 'metic' }, { igniteFloor: bad });
151
+ assert.equal(c.igniteFloor, 'metic', `floor "${String(bad)}" should have been refused`);
152
+ }
153
+ });
154
+
155
+ await test('a rank name is case-normalised but NOT trimmed', () => {
156
+ assert.equal(load({ igniteFloor: 'metic' }, { igniteFloor: 'ARCHON' }).igniteFloor, 'archon');
157
+ assert.equal(load({ igniteFloor: 'metic' }, { igniteFloor: ' archon' }).igniteFloor, 'metic',
158
+ 'a padded rank is a hostile shape, not a rank');
159
+ });
160
+
161
+ await test('a decay rate outside [0,1] is refused rather than clamped', () => {
162
+ for (const bad of [1.5, -0.1, NaN, Infinity, -Infinity, '0.5', null, {}]) {
163
+ const c = load({ descentDecayRate: 0.5 }, { descentDecayRate: bad });
164
+ assert.equal(c.descentDecayRate, 0.5, `rate ${String(bad)} should have been refused`);
165
+ }
166
+ });
167
+
168
+ await test('both ends of the decay band are legitimate choices', () => {
169
+ assert.equal(load({ descentDecayRate: 0.5 }, { descentDecayRate: 0 }).descentDecayRate, 0);
170
+ assert.equal(load({ descentDecayRate: 0.5 }, { descentDecayRate: 1 }).descentDecayRate, 1);
171
+ });
172
+
173
+ await test('an unrecognised floor yields the DEFAULT set, never an empty one', () => {
174
+ // Failing closed here would stop every ignition on a config typo. The floor is
175
+ // a scheduling preference; a stopped queue is a worse failure than a default.
176
+ assert.deepEqual([...ideasConfig.trustedRanksForFloor('nonsense')].sort(), [...routing.METIC_PLUS].sort());
177
+ assert.deepEqual([...ideasConfig.trustedRanksForFloor(null)].sort(), [...routing.METIC_PLUS].sort());
178
+ });
179
+
180
+ // --- the dial actually moves the landing ------------------------------------
181
+
182
+ await test('lowering the floor makes a sub-Metic bug land claimable', () => {
183
+ const trusted = ideasConfig.trustedRanksForFloor('xenos');
184
+ const d = routing.decideLanding({ goalId: 5, kind: 'bug', filerRank: 'xenos', trustedRanks: trusted });
185
+ assert.equal(d.status, 'ready');
186
+ assert.equal(d.reason, 'bug_trusted_filer');
187
+ });
188
+
189
+ await test('lowering the floor also moves the HOMELESS bug branch', () => {
190
+ // Two call sites read the floor. A fix that threaded only one of them would
191
+ // leave the dial half-connected and this is what catches it.
192
+ const trusted = ideasConfig.trustedRanksForFloor('xenos');
193
+ const d = routing.decideLanding({ goalId: null, kind: 'bug', filerRank: 'xenos', vector: 'api', trustedRanks: trusted });
194
+ assert.equal(d.status, 'ready');
195
+ assert.equal(d.reason, 'bug_homeless_trusted_filer');
196
+ });
197
+
198
+ await test('raising the floor makes a Metic bug await a nod', () => {
199
+ const trusted = ideasConfig.trustedRanksForFloor('archon');
200
+ const d = routing.decideLanding({ goalId: 5, kind: 'bug', filerRank: 'metic', trustedRanks: trusted });
201
+ assert.equal(d.status, 'backlog');
202
+ assert.equal(d.reason, 'bug_awaiting_nod');
203
+ });
204
+
205
+ await test('omitting trustedRanks reproduces the constant exactly', () => {
206
+ // Every pre-R05 caller and test omits it. If this ever diverges, the whole
207
+ // "defaults preserve today's behaviour" claim is false.
208
+ for (const rank of ['xenos', 'thetes', 'metic', 'archon', null, 'nonsense']) {
209
+ const before = routing.decideLanding({ goalId: 5, kind: 'bug', filerRank: rank });
210
+ const after = routing.decideLanding({
211
+ goalId: 5, kind: 'bug', filerRank: rank,
212
+ trustedRanks: ideasConfig.trustedRanksForFloor('metic'),
213
+ });
214
+ assert.deepEqual(after, before, `rank ${String(rank)} diverged`);
215
+ }
216
+ });
217
+
218
+ await test('a non-Set trustedRanks is ignored, not trusted', () => {
219
+ // A caller handing an array, a string or an object must not accidentally widen
220
+ // anything — the guard is `instanceof Set`, and this pins it.
221
+ for (const bad of [['xenos'], 'xenos', { has: () => true }, true]) {
222
+ const d = routing.decideLanding({ goalId: 5, kind: 'bug', filerRank: 'xenos', trustedRanks: bad });
223
+ assert.equal(d.status, 'backlog', `trustedRanks ${JSON.stringify(bad)} was honoured and should not have been`);
224
+ }
225
+ });
226
+
227
+ // --- THE WALL: the floor is not the trusted tier ----------------------------
228
+
229
+ await test('lowering the ignite floor does NOT let a sub-Metic propose a criterion', () => {
230
+ // The security claim the amended description turns on. mayProposeCriterion
231
+ // reads the fixed constant and takes no floor, so there is no argument this
232
+ // test could pass that would widen it.
233
+ assert.equal(ideasConfig.trustedRanksForFloor('xenos').has('xenos'), true, 'precondition: floor is wide open');
234
+ for (const rank of ['xenos', 'thetes']) {
235
+ assert.equal(routing.mayProposeCriterion(rank), false, `${rank} may not propose a criterion at any floor`);
236
+ }
237
+ assert.equal(routing.mayProposeCriterion('metic'), true);
238
+ assert.equal(routing.mayProposeCriterion('archon'), true);
239
+ });
240
+
241
+ await test('lowering the ignite floor does NOT breach the protected-scope wall', () => {
242
+ const trusted = ideasConfig.trustedRanksForFloor('xenos');
243
+ const landing = routing.decideLanding({ goalId: 5, kind: 'bug', filerRank: 'xenos', trustedRanks: trusted });
244
+ assert.equal(landing.status, 'ready', 'precondition: the dial is turned all the way down');
245
+
246
+ const verdict = routing.authorizeRoutedLanding({
247
+ landing, filerRank: 'xenos', scopeProtected: true,
248
+ });
249
+ assert.equal(verdict.ok, false, 'a wide-open ignite floor must not open the protected core');
250
+ assert.equal(verdict.code, 'protected_scope');
251
+ });
252
+
253
+ await test('authorizeRoutedLanding takes no floor argument at all', () => {
254
+ // Structural, not behavioural: if someone later adds one, this reds and makes
255
+ // them read the header rather than discovering the widening in production.
256
+ const src = fs.readFileSync(path.join(ROOT, 'modules/ideas/routing.js'), 'utf8');
257
+ const fn = src.slice(src.indexOf('function authorizeRoutedLanding'));
258
+ const sig = fn.slice(0, fn.indexOf(')') + 1);
259
+ assert.ok(!/trustedRanks|igniteFloor/.test(sig), `the scope wall grew a floor argument: ${sig}`);
260
+ });
261
+
262
+ await test('METIC_PLUS is still a hard-coded constant', () => {
263
+ // filing_abuse_matrix.mjs holds the same line. Keeping a copy here means the
264
+ // R05 test file itself refuses the shortcut it exists to prevent.
265
+ const src = fs.readFileSync(path.join(ROOT, 'modules/ideas/routing.js'), 'utf8');
266
+ assert.match(src, /METIC_PLUS = new Set\(\['metic', 'archon'\]\)/,
267
+ 'METIC_PLUS must stay a constant — it is the trusted tier, not the ignite floor');
268
+ });
269
+
270
+ console.log(`\nideas_config: ${passed} passed, ${failed} failed`);
271
+ process.exit(failed ? 1 : 0);