@bongos/core 1.19.469 → 1.19.471

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,181 @@
1
+ // tests/hall_catalog_world.mjs — the catalog pages in the chrome world (task
2
+ // 1003472, goal 1000081): modules and the repo atlas, REDESIGNED rather than
3
+ // re-skinned. Static pins over the shipped source: the module catalog is ONE
4
+ // panel of the oversight family's rows (the card grid, the boxed search and
5
+ // the .mod-chip alias are gone), the description is clamped with a details
6
+ // disclosure under it, the right column is the family's grant toggle or a
7
+ // state pill and never an accent fill, the submit door sits on the row, and
8
+ // the figure band carries the numbers the page head used to say as prose; the
9
+ // atlas keeps its nine semantic colours and puts every piece of chrome around
10
+ // the scene in the shell's dialect — the panel is a .scroll, the view switch
11
+ // the tab strip, the detail card a hairline panel with no shadow — and gains
12
+ // an island index a keyboard or a phone can open.
13
+ import test from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
21
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
22
+ // Negative pins read the code, not its comments (a comment that says what left
23
+ // would otherwise fail the pin that says it left).
24
+ const decomment = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/^\s*\/\/.*$/gm, '');
25
+ const STYLE = read('style.css');
26
+ const OVERSIGHT = read('oversight.css');
27
+ const tokensDefined = (css, where) => {
28
+ for (const t of new Set([...css.matchAll(/var\((--[a-z0-9-]+)\)/g)].map((m) => m[1]))) {
29
+ const declared = (src) => new RegExp(`(^\\s*|[{;]\\s*)${t}:`, 'm').test(src);
30
+ assert.ok(declared(STYLE) || declared(OVERSIGHT) || declared(css),
31
+ `${where} spends ${t}, which neither style.css, oversight.css nor the sheet itself defines`);
32
+ }
33
+ };
34
+ const noAlias = (rawCss, where) => {
35
+ const css = decomment(rawCss);
36
+ const alias = css.match(/--(surface|surface-2|border|border-strong|accent-weak|accent-text)\b/g) || [];
37
+ assert.deepEqual(alias, [], `${where} spends a compat alias: ${alias.join(', ')}`);
38
+ };
39
+
40
+ // ---- modules ----------------------------------------------------------------
41
+
42
+ test('modules: ONE panel of family rows — the filter bar, the figure band and two groups on one .scroll', () => {
43
+ const html = read('modules.html');
44
+ assert.match(html, /<link rel="stylesheet" href="\/builders\/oversight\.css">\s*<link rel="stylesheet" href="\/builders\/modules\.css">/, 'the family sheet loads before the page sheet');
45
+ assert.equal((html.match(/class="scroll[ "]/g) || []).length, 1, 'one panel, not three');
46
+ assert.match(html, /<div id="mod-filters"><\/div>/, 'the kit filter bar has a mount');
47
+ assert.match(html, /class="ov-figs" id="mod-figs"/, 'the figure band is the family\'s');
48
+ assert.match(html, /<ul class="ov-rows" id="core-grid">/, 'the core catalog is a run of rows');
49
+ assert.match(html, /<ul class="ov-rows" id="own-grid">/, 'the project\'s own modules are a run of rows');
50
+ assert.equal((html.match(/class="ov-group__head"/g) || []).length, 2, 'two group heads');
51
+ assert.doesNotMatch(decomment(html), /mod-grid|mod-card|mod-browse|submit-scroll|class="settings-status"/, 'the card grid, the boxed search and the third panel are gone');
52
+ assert.doesNotMatch(html, /\d+ module\(s\)/, 'the page head no longer says the numbers as prose');
53
+ });
54
+
55
+ test('modules: the row is the oversight row — title with the mono key, clamped description, facts, one control on the right', () => {
56
+ const js = read('modules.js');
57
+ assert.match(js, /<li class="ov-row mod-row" data-key=/, 'each module is an .ov-row');
58
+ assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
59
+ assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
60
+ assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
61
+ assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}<\/div>/, 'provenance, credit and upkeep are the facts line');
62
+ assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
63
+ const css = read('modules.css');
64
+ assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
65
+ assert.match(css, /\.mod-row:has\(\.mod-more\[open\]\) \.mod-row__desc/, 'and lifts the clamp while the details are open');
66
+ assert.doesNotMatch(decomment(js), /class="mod-chip|innerHTML = .*style=|<span style=/, 'no .mod-chip, no inline style');
67
+ });
68
+
69
+ test('modules: the switch is the family grant toggle or a state pill — never an accent fill, never a dead control', () => {
70
+ const js = read('modules.js');
71
+ assert.match(js, /class="ov-toggle mod-toggle" aria-pressed=/, 'the toggle is .ov-toggle with aria-pressed');
72
+ assert.match(js, /: `<span class="fact-pill \$\{on \? 'fact-pill--ok' : ''\}"/, 'a viewer below the floor sees the state as a pill');
73
+ assert.match(js, /rankLabel\('metic'\)/, 'the floor is written from the pack');
74
+ assert.doesNotMatch(js, /Metic\+/, 'and never as a hardcoded rank');
75
+ const css = decomment(read('modules.css'));
76
+ assert.doesNotMatch(css, /background:\s*var\(--accent\)/, 'no accent fill on a state');
77
+ assert.doesNotMatch(css, /button\.mod-toggle|\[data-on=/, 'the old accent-filled toggle is gone');
78
+ assert.match(js, /class="btn-ghost btn--sm mod-submit" data-submit-key=/, 'the submit door is a ghost pill on the row');
79
+ });
80
+
81
+ test('modules: upkeep colours the exception — undeclared is the warn pill, maintained a plain fact', () => {
82
+ const js = read('modules.js');
83
+ assert.match(js, /fact-pill fact-pill--warn"[^>]*>upkeep undeclared</, 'undeclared upkeep is the warn pill');
84
+ assert.match(js, /<span class="ov-fact">upkeep <b>\$\{escapeHtml\(status\)\}<\/b><\/span>/, 'a maintained module reads as a plain fact');
85
+ assert.match(js, /fact-pill fact-pill--info">core-loaded</, 'core-loaded is the info pill');
86
+ assert.match(js, /<span class="fact-chip">\$\{escapeHtml\(m\.license\)\}<\/span>/, 'the licence is a chip');
87
+ });
88
+
89
+ test('modules: the kit filter bar carries the search and a state lens, and the empty state is the kit\'s', () => {
90
+ const js = read('modules.js');
91
+ assert.match(js, /kit\.makeFilterBar\(\{/, 'the bar is the kit\'s');
92
+ assert.match(js, /search: \{ id: 'mod-search'/, 'the search keeps its id (the states file fills it)');
93
+ assert.match(js, /\{ id: 'mod-state', label: 'State'/, 'a state lens');
94
+ assert.match(js, /kit\.emptyStateHtml\(/, 'empties are the kit\'s');
95
+ assert.doesNotMatch(decomment(js), /mod-empty/, '.mod-empty is gone');
96
+ const html = read('modules.html');
97
+ assert.match(html, /<script src="\/builders\/hall-kit\.js"><\/script>/, 'and the page loads the kit');
98
+ });
99
+
100
+ test('modules: the figure band carries modules, enabled, built here and undeclared upkeep', () => {
101
+ const js = read('modules.js');
102
+ for (const label of ['modules', 'enabled', 'built here', 'no upkeep']) {
103
+ assert.match(js, new RegExp(`fig\\('${label}'`), `figure: ${label}`);
104
+ }
105
+ });
106
+
107
+ test('modules.css spends only the fifteen, and every token it names exists', () => {
108
+ const css = read('modules.css');
109
+ noAlias(css, 'modules.css');
110
+ tokensDefined(css, 'modules.css');
111
+ assert.doesNotMatch(decomment(css), /#[0-9a-f]{3,8}\b/i, 'no literal colours');
112
+ });
113
+
114
+ test('the shell no longer aliases .mod-chip onto the pill', () => {
115
+ const alias = STYLE.slice(STYLE.indexOf('.fact-pill,\n'), STYLE.indexOf('{', STYLE.indexOf('.fact-pill,\n')));
116
+ assert.doesNotMatch(alias, /mod-chip/, 'modules takes .fact-pill by name');
117
+ });
118
+
119
+ // ---- atlas ------------------------------------------------------------------
120
+
121
+ test('atlas: the nine semantic colours stay fixed literals, and nothing else in the sheet is one', () => {
122
+ const css = read('atlas.css');
123
+ for (const t of ['--ab-sun', '--ab-sky', '--ab-coral', '--ab-paper', '--ab-good', '--ab-core', '--ab-db', '--ab-ghost', '--ab-danger']) {
124
+ assert.match(css, new RegExp(`${t}:#[0-9a-f]{6}`), `${t} is a fixed literal (ADR 0094)`);
125
+ }
126
+ const rest = decomment(css).replace(/--ab-(sun|sky|coral|paper|good|core|db|ghost|danger):#[0-9a-f]{6}/g, '');
127
+ assert.deepEqual(rest.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'no other literal colour');
128
+ noAlias(css, 'atlas.css');
129
+ tokensDefined(css, 'atlas.css');
130
+ assert.doesNotMatch(rest, /box-shadow/, 'no box-shadow: the world draws depth with hairlines');
131
+ });
132
+
133
+ test('atlas: the panel is the shell\'s .scroll and the view switch is the shell\'s tab strip', () => {
134
+ const html = read('atlas.html');
135
+ assert.match(html, /<section class="scroll atlas-page"/, 'the map sits on a .scroll');
136
+ assert.match(html, /<div class="kit-tabs ab-views" role="tablist"/, 'the view switch is .kit-tabs');
137
+ assert.equal((html.match(/class="setup__tab" role="tab" data-view=/g) || []).length, 2, 'two tabs');
138
+ assert.doesNotMatch(decomment(html), /ab-modes|ab-chip"|class="card"/, 'the segmented box, the live chip and the empty card are gone');
139
+ const css = decomment(read('atlas.css'));
140
+ assert.doesNotMatch(css, /\.ab-modes|\.ab-chip\b|radial-gradient|\.ab-empty \.card/, 'and their rules with them');
141
+ const js = read('atlas.js');
142
+ assert.match(js, /setAttribute\('aria-selected'/, 'the script switches the tabs by aria-selected');
143
+ assert.doesNotMatch(decomment(js), /classList\.toggle\('active'/, 'not by an .active class');
144
+ });
145
+
146
+ test('atlas: the detail card is a hairline panel whose facts are the shell\'s pills and chips, and a neighbour is a door', () => {
147
+ const css = read('atlas.css');
148
+ assert.match(css, /\.ab-popup\{[^}]*border:var\(--line\) solid var\(--rule\)/, 'the card closes on one rule hairline');
149
+ const js = read('atlas.js');
150
+ assert.match(js, /fact-pill fact-pill--' \+ \(PILL\[n\.badge\.k\]/, 'the badge is a status pill');
151
+ assert.match(js, /<span class="fact-chip">' \+ esc\(n\.metric\)/, 'the metric is a chip');
152
+ assert.match(js, /<button type="button" class="fact-chip ab-jump" data-node=/, 'a connected island is a button');
153
+ assert.doesNotMatch(decomment(js), /ab-chip2|ab-badge ab-b-|↔/, 'the glass chips, the private badges and the arrow glyph are gone');
154
+ assert.match(js, /ev\.key === 'Escape'/, 'Escape closes the card');
155
+ });
156
+
157
+ test('atlas: the island index renders every node of the view as a row a keyboard can open', () => {
158
+ const html = read('atlas.html');
159
+ assert.match(html, /<details class="kit-collapse ab-index" id="ab-index">/, 'the index is a collapsible under the map');
160
+ assert.match(html, /<ul class="ov-rows" id="ab-index-rows"><\/ul>/, 'its rows are the family\'s');
161
+ const js = read('atlas.js');
162
+ assert.match(js, /function drawIndex\(d\)/, 'the index is drawn per view');
163
+ assert.match(js, /<li class="ov-row ov-row--dense ab-ix" data-node=/, 'each island is a dense row');
164
+ assert.match(js, /class="ov-row__title ab-ix__t" aria-expanded="false" aria-controls=/, 'the title is a disclosure button');
165
+ assert.match(js, /body\.innerHTML = detailHTML\(nodeById\(id\)\)/, 'and opens the same detail the map\'s card shows');
166
+ assert.match(js, /matchMedia\('\(max-width: 640px\)'\)\.matches\) indexEl\.open = true/, 'open by default on a phone, where the index is the read');
167
+ assert.match(js, /drawIndex\(d\);/, 'drawn on every render');
168
+ });
169
+
170
+ test('atlas: the hub breathes at the world\'s ambient floor, not as a pulse', () => {
171
+ const css = read('atlas.css');
172
+ const m = css.match(/animation:abbreathe (\d+)s/);
173
+ assert.ok(m && Number(m[1]) >= 20, 'the loop is 20s or slower');
174
+ });
175
+
176
+ test('the states files exercise the redesign\'s new states', () => {
177
+ const mod = JSON.parse(read('modules.states.json'));
178
+ for (const s of ['browse', 'search-empty', 'lens', 'details', 'out']) assert.ok(mod.states[s], `modules: ${s}`);
179
+ const atlas = JSON.parse(read('atlas.states.json'));
180
+ for (const s of ['map', 'truth', 'detail', 'index', 'out']) assert.ok(atlas.states[s], `atlas: ${s}`);
181
+ });
@@ -33,6 +33,7 @@ const PUBLISHED_SURFACE = [
33
33
  'llmCache', // BV1.R73: shared LLM-cost cache (1.6.0; tracked domain leak)
34
34
  'llmPricing', // BV1.R77: shared token→USD pricing (1.7.0; tracked domain leak — economy carve)
35
35
  'branding', 'clientBranding', 'userAgent', 'resolveEnv', 'readConfigFileSync', 'configHome', 'configPath', // userAgent added 1.12.0 (BV1.R86; github-push UA)
36
+ 'resolveStaleClaimHours', 'staleClaimMinutes', // added by task 1003476 (ADR 0226; goal 1000072): the PROJECT's stale-claim timer — the sweep that releases a silent claim and the Archon roster that reports one read the same number through here, after eighteen hours of disagreeing (6 vs 24)
36
37
  'joinabilityMode', // added by task 1003044 (ADR 0194; the carrier after 1.19.374): the project's membership door, one reader for the sign-in gate AND the onboarding module's public POST
37
38
  'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
38
39
  'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
@@ -248,7 +248,46 @@ await t('GET /builders/roster as archon → 200', async () => {
248
248
  });
249
249
  assert.equal(r.status, 200);
250
250
  assert.ok(Array.isArray(r.json?.builders));
251
- assert.equal(r.json?.stale_hours, 24);
251
+ // The default is the PROJECT's stale-claim timer, not a number this route owns
252
+ // (task 1003476, goal 1000072). It used to assert a hardcoded 24 while the sweeper
253
+ // released at 6 — this assertion WAS the eighteen-hour lie, written down.
254
+ const timer = require('../src/stale-timer.js');
255
+ assert.equal(r.json?.stale_hours, timer.resolveStaleClaimHours().hours);
256
+ assert.equal(r.json?.stale_hours_source, 'default');
257
+ });
258
+
259
+ await t('GET /builders/roster → the timer follows the project env', async () => {
260
+ const prev = process.env.GDS_STALE_CLAIM_HOURS;
261
+ process.env.GDS_STALE_CLAIM_HOURS = '9';
262
+ try {
263
+ const r = await send({
264
+ port, method: 'GET', path: '/api/gds/builders/roster',
265
+ headers: { 'x-stub-builder-id': '1', 'x-stub-builder-rank': 'archon' },
266
+ });
267
+ assert.equal(r.json?.stale_hours, 9, 'resolved per request, not frozen at boot');
268
+ assert.equal(r.json?.stale_hours_source, 'env:GDS_STALE_CLAIM_HOURS');
269
+ } finally {
270
+ if (prev === undefined) delete process.env.GDS_STALE_CLAIM_HOURS;
271
+ else process.env.GDS_STALE_CLAIM_HOURS = prev;
272
+ }
273
+ });
274
+
275
+ await t('GET /builders/roster → a bad project timer degrades, it does not 500', async () => {
276
+ // The Archon opened this page to find the problem; 500-ing it hides the problem.
277
+ const prev = process.env.GDS_STALE_CLAIM_HOURS;
278
+ process.env.GDS_STALE_CLAIM_HOURS = 'six-ish';
279
+ try {
280
+ const r = await send({
281
+ port, method: 'GET', path: '/api/gds/builders/roster',
282
+ headers: { 'x-stub-builder-id': '1', 'x-stub-builder-rank': 'archon' },
283
+ });
284
+ assert.equal(r.status, 200);
285
+ assert.equal(r.json?.stale_hours, 6);
286
+ assert.match(String(r.json?.stale_hours_source), /^default \(invalid GDS_STALE_CLAIM_HOURS="six-ish"/);
287
+ } finally {
288
+ if (prev === undefined) delete process.env.GDS_STALE_CLAIM_HOURS;
289
+ else process.env.GDS_STALE_CLAIM_HOURS = prev;
290
+ }
252
291
  });
253
292
 
254
293
  await t('GET /builders/roster?stale_hours=48 → echoes 48', async () => {
@@ -258,6 +297,7 @@ await t('GET /builders/roster?stale_hours=48 → echoes 48', async () => {
258
297
  });
259
298
  assert.equal(r.status, 200);
260
299
  assert.equal(r.json?.stale_hours, 48);
300
+ assert.equal(r.json?.stale_hours_source, 'query', 'a lens is labelled as a lens');
261
301
  });
262
302
 
263
303
  await t('GET /builders/roster?stale_hours=0 → 400', async () => {
@@ -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();