@bongos/core 1.19.470 → 1.19.472

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,230 @@
1
+ // tests/stale_timer.mjs — one project, one stale-claim timer
2
+ // (task 1003476, goal 1000072, criterion legibility-sort-and-staleness).
3
+ //
4
+ // THE BUG THIS PINS. "How long may a claim go silent" had three answers living in
5
+ // three files, and nothing made them meet: the sweeper that actually releases claims
6
+ // read GDS_STALE_CLAIM_HOURS (default 6h), the Archon roster that REPORTS staleness
7
+ // hardcoded 24h, and the two session-pulse SQL helpers defaulted to 30 minutes. The
8
+ // visible failure was the eighteen-hour lie: for most of a day after a claim had been
9
+ // auto-released, tagged [auto-released:stale] and handed back to the queue, the roster
10
+ // an Archon opens to decide whether to intervene still showed it healthy.
11
+ //
12
+ // So these assertions are mostly about SAMENESS — that no surface carries its own
13
+ // number — plus the one place the two kinds of caller must legitimately differ: a
14
+ // misconfigured timer makes the READ-ONLY roster degrade and say so, and makes the
15
+ // DESTRUCTIVE sweeper refuse to run at all.
16
+ //
17
+ // Run: node tests/stale_timer.mjs
18
+ import assert from 'node:assert/strict';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { createRequire } from 'node:module';
23
+ import { makeRunner } from './helpers.mjs';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
28
+ const st = require(path.join(ROOT, 'src/stale-timer.js'));
29
+
30
+ const PULSE_CLI = read('scripts/gds/pulse.js');
31
+ const PULSE_LIB = read('modules/sessions/pulse.js');
32
+ const DB_SHIP = read('modules/lifecycle/db-ship.js');
33
+ const ROSTER_ROUTE = read('src/bongos/routes/builders.js');
34
+ const FITNESS = read('scripts/gds/fitness.js');
35
+ const DOORWAY = read('src/module-api.js');
36
+
37
+ const { test, summary } = makeRunner();
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // 1. The resolver itself — executed, not described.
41
+ // ---------------------------------------------------------------------------
42
+
43
+ await test('no env, no override → the sweep default, labelled', () => {
44
+ const r = st.resolveStaleClaimHours({ env: {} });
45
+ assert.equal(r.hours, st.DEFAULT_STALE_CLAIM_HOURS);
46
+ assert.equal(r.hours, 6, 'the default is the SWEEP\'s historical 6h — the surface that acts');
47
+ assert.equal(r.source, 'default');
48
+ assert.equal(r.invalid, null);
49
+ });
50
+
51
+ await test('the live prod env var still resolves — legacy GDS_ prefix honoured', () => {
52
+ // Nothing in the running deployment may have to change for this to keep working:
53
+ // instance-config's legacy prefix chain is what carries GDS_STALE_CLAIM_HOURS.
54
+ const r = st.resolveStaleClaimHours({ env: { GDS_STALE_CLAIM_HOURS: '12' } });
55
+ assert.equal(r.hours, 12);
56
+ assert.equal(r.source, 'env:GDS_STALE_CLAIM_HOURS');
57
+ assert.equal(r.invalid, null);
58
+ });
59
+
60
+ await test('the branded prefix wins over the legacy one', () => {
61
+ const r = st.resolveStaleClaimHours({
62
+ env: { CLOUDBONGOS_STALE_CLAIM_HOURS: '3', GDS_STALE_CLAIM_HOURS: '12' },
63
+ });
64
+ assert.equal(r.hours, 3);
65
+ assert.equal(r.source, 'env:CLOUDBONGOS_STALE_CLAIM_HOURS');
66
+ });
67
+
68
+ await test('an override is a LENS — it never becomes the project answer', () => {
69
+ const env = { GDS_STALE_CLAIM_HOURS: '12' };
70
+ assert.equal(st.resolveStaleClaimHours({ override: '48', env }).hours, 48);
71
+ assert.equal(st.resolveStaleClaimHours({ override: '48', env }).source, 'override');
72
+ // asking again without the lens gives the project's number back, unchanged
73
+ assert.equal(st.resolveStaleClaimHours({ env }).hours, 12);
74
+ });
75
+
76
+ await test('an empty override falls through rather than resolving to 0', () => {
77
+ for (const override of ['', null, undefined]) {
78
+ const r = st.resolveStaleClaimHours({ override, env: { GDS_STALE_CLAIM_HOURS: '12' } });
79
+ assert.equal(r.hours, 12, `override=${JSON.stringify(override)} must not shadow the env`);
80
+ }
81
+ });
82
+
83
+ await test('an unusable value REPORTS instead of throwing, and never returns a bad number', () => {
84
+ for (const [raw, reason] of [['nope', 'not a number'], ['0', 'must be positive'], ['-4', 'must be positive'], ['9999', 'out of range (1..720)']]) {
85
+ const r = st.resolveStaleClaimHours({ env: { GDS_STALE_CLAIM_HOURS: raw } });
86
+ assert.equal(r.hours, st.DEFAULT_STALE_CLAIM_HOURS, `${raw} must fall back`);
87
+ assert.equal(r.source, 'default');
88
+ assert.ok(r.invalid, `${raw} must be reported as invalid`);
89
+ assert.equal(r.invalid.where, 'env');
90
+ assert.equal(r.invalid.varName, 'GDS_STALE_CLAIM_HOURS');
91
+ assert.equal(r.invalid.raw, raw);
92
+ assert.equal(r.invalid.reason, reason);
93
+ }
94
+ });
95
+
96
+ await test('the bound is the one GET /builders/roster already published (1..720)', () => {
97
+ assert.equal(st.MIN_STALE_CLAIM_HOURS, 1);
98
+ assert.equal(st.MAX_STALE_CLAIM_HOURS, 720);
99
+ assert.equal(st.resolveStaleClaimHours({ env: { GDS_STALE_CLAIM_HOURS: '720' } }).hours, 720);
100
+ assert.ok(st.resolveStaleClaimHours({ env: { GDS_STALE_CLAIM_HOURS: '721' } }).invalid);
101
+ });
102
+
103
+ await test('describeStaleTimer names the number AND its provenance', () => {
104
+ assert.equal(st.describeStaleTimer(st.resolveStaleClaimHours({ env: {} })), '6h (default)');
105
+ assert.equal(
106
+ st.describeStaleTimer(st.resolveStaleClaimHours({ env: { GDS_STALE_CLAIM_HOURS: '12' } })),
107
+ '12h (env:GDS_STALE_CLAIM_HOURS)'
108
+ );
109
+ const bad = st.describeStaleTimer(st.resolveStaleClaimHours({ env: { GDS_STALE_CLAIM_HOURS: 'nope' } }));
110
+ assert.match(bad, /^6h \(default\)/);
111
+ assert.match(bad, /ignoring GDS_STALE_CLAIM_HOURS="nope" \(not a number\)/);
112
+ });
113
+
114
+ await test('hours→minutes happens in ONE place and never rounds to zero', () => {
115
+ assert.equal(st.staleClaimMinutes(6), 360);
116
+ assert.equal(st.staleClaimMinutes(1), 60);
117
+ assert.equal(st.staleClaimMinutes(0.001), 1, 'a sub-minute timer must still be a minute, not 0');
118
+ assert.throws(() => st.staleClaimMinutes(0), /positive/);
119
+ assert.throws(() => st.staleClaimMinutes('x'), /positive/);
120
+ });
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // 2. Sameness — no surface carries its own number any more.
124
+ // ---------------------------------------------------------------------------
125
+
126
+ await test('the three old hardcoded defaults are gone from their files', () => {
127
+ assert.ok(!/thresholdMin = 30\b/.test(PULSE_LIB),
128
+ 'modules/sessions/pulse.js must not default to a third number (30 min)');
129
+ assert.ok(!/staleHours = 24\b/.test(DB_SHIP),
130
+ 'getBuildersRoster must not default to its own 24h');
131
+ assert.ok(!/let staleHours = 24\b/.test(ROSTER_ROUTE),
132
+ 'GET /builders/roster must not default to its own 24h');
133
+ });
134
+
135
+ await test('every staleness surface resolves through the one timer', () => {
136
+ assert.match(PULSE_CLI, /require\('\.\.\/\.\.\/src\/stale-timer'\)/,
137
+ 'the sweeper CLI must read the shared timer');
138
+ assert.match(PULSE_LIB, /resolveStaleClaimHours/,
139
+ 'the session-pulse SQL helpers must default to the shared timer');
140
+ assert.match(DB_SHIP, /api\.resolveStaleClaimHours\(\)/,
141
+ 'getBuildersRoster must default to the shared timer');
142
+ assert.match(ROSTER_ROUTE, /require\('\.\.\/\.\.\/stale-timer'\)/,
143
+ 'the roster route must read the shared timer');
144
+ });
145
+
146
+ await test('the sweeper no longer reads the env var itself', () => {
147
+ // The precedence has ONE home. A second reader is how 6-vs-24 happened.
148
+ assert.ok(!/process\.env\.GDS_STALE_CLAIM_HOURS/.test(PULSE_CLI),
149
+ 'scripts/gds/pulse.js must not re-read GDS_STALE_CLAIM_HOURS directly');
150
+ });
151
+
152
+ await test('an explicit argument still overrides at every layer', () => {
153
+ // The lens must survive: an Archon asking "what would 48h show me?" and an
154
+ // operator sweeping at a one-off threshold both depend on it.
155
+ assert.match(PULSE_LIB, /thresholdMin \?\? projectThresholdMin\(\)/);
156
+ assert.match(DB_SHIP, /staleHours \?\? api\.resolveStaleClaimHours\(\)/);
157
+ assert.match(ROSTER_ROUTE, /stale_hours != null/);
158
+ });
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // 3. The asymmetric fail posture: reporting degrades, enforcement refuses.
162
+ // ---------------------------------------------------------------------------
163
+
164
+ await test('the SWEEP refuses to run on a timer it cannot read', () => {
165
+ // It releases other people's claims and stamps their session logs. A reload does
166
+ // not undo that, so guessing is not an option available to it.
167
+ assert.match(PULSE_CLI, /refusing to sweep on a timer this project cannot read/);
168
+ const fn = PULSE_CLI.slice(PULSE_CLI.indexOf('function parseThresholdHours'));
169
+ const body = fn.slice(0, fn.indexOf('\n}\n') + 3);
170
+ assert.match(body, /if \(resolved\.invalid\)/, 'the refusal must key off the resolver verdict');
171
+ assert.match(body, /throw new Error/);
172
+ });
173
+
174
+ // The helper both Archon staleness reads go through.
175
+ const HELPER = (() => {
176
+ const from = ROSTER_ROUTE.indexOf('function staleHoursFor(req, res)');
177
+ assert.ok(from > 0, 'the shared helper must exist');
178
+ return ROSTER_ROUTE.slice(from, ROSTER_ROUTE.indexOf('\n}\n', from) + 3);
179
+ })();
180
+
181
+ await test('the ROSTER degrades to the default and names the bad value', () => {
182
+ // 500-ing the roster an Archon opened to find the problem would hide the problem.
183
+ assert.match(HELPER, /default \(invalid \$\{timer\.invalid\.varName\}/);
184
+ assert.ok(!/throw/.test(HELPER), 'the roster helper must not throw on a bad timer');
185
+ assert.match(HELPER, /res\.fail\('bad_stale_hours', 400/,
186
+ 'only an explicitly BAD query param is a 400 — never a bad project timer');
187
+ });
188
+
189
+ await test('BOTH Archon staleness reads report the number and its provenance', () => {
190
+ // Two routes, one helper. Before this they were two byte-identical copies of a
191
+ // hardcoded 24, which is exactly how a second copy drifts into a second policy.
192
+ const calls = [...ROSTER_ROUTE.matchAll(/= staleHoursFor\(req, res\)/g)];
193
+ assert.equal(calls.length, 2, 'the roster and its expand view both go through the helper');
194
+ assert.match(ROSTER_ROUTE, /res\.json\(\{ builders, stale_hours: stale\.hours, stale_hours_source: stale\.source \}\)/);
195
+ assert.match(ROSTER_ROUTE, /res\.json\(\{ builder_id: targetId, claims, stale_hours: stale\.hours, stale_hours_source: stale\.source \}\)/);
196
+ assert.match(HELPER, /source: 'query'/, 'a lens must be labelled as a lens');
197
+ });
198
+
199
+ await test('the expand view cannot disagree with the roster it expands from', () => {
200
+ assert.ok(!/staleHours = 24\b/.test(ROSTER_ROUTE), 'no route may keep its own 24');
201
+ assert.ok(!/staleHours = 24\b/.test(DB_SHIP), 'no db helper may keep its own 24');
202
+ assert.match(DB_SHIP, /staleHours \?\? api\.resolveStaleClaimHours\(\)\.hours/);
203
+ const both = [...DB_SHIP.matchAll(/staleHours \?\? api\.resolveStaleClaimHours\(\)\.hours/g)];
204
+ assert.equal(both.length, 2, 'getBuildersRoster AND getActiveClaimsForBuilderWithStale');
205
+ });
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // 4. The boundary the new kernel file has to keep.
209
+ // ---------------------------------------------------------------------------
210
+
211
+ await test('src/stale-timer.js is on the KERNEL_FILES roster', () => {
212
+ // The doorway (itself a KERNEL_FILE) re-exports it, so leaving it off would read as
213
+ // a kernel→non-kernel leak and red the build on Check 12.
214
+ const roster = FITNESS.slice(FITNESS.indexOf('const KERNEL_FILES = ['));
215
+ assert.match(roster.slice(0, roster.indexOf('];')), /'src\/stale-timer\.js'/);
216
+ });
217
+
218
+ await test('the timer imports only the kernel', () => {
219
+ const src = read('src/stale-timer.js');
220
+ const specs = [...src.matchAll(/require\('([^']+)'\)/g)].map((m) => m[1]);
221
+ assert.deepEqual(specs, ['./instance-config'],
222
+ 'a kernel file may depend only on the kernel — keep this list boring');
223
+ });
224
+
225
+ await test('the doorway hands modules the timer, not the raw env', () => {
226
+ assert.match(DOORWAY, /resolveStaleClaimHours: staleTimer\.resolveStaleClaimHours/);
227
+ assert.match(DOORWAY, /staleClaimMinutes: staleTimer\.staleClaimMinutes/);
228
+ });
229
+
230
+ summary();