@bongos/core 1.19.388 → 1.19.390

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/tests/grader.mjs CHANGED
@@ -842,20 +842,20 @@ test('selectGraderModel: builder Haiku → grader Opus', () => {
842
842
 
843
843
  // --- task 1002660: per-worker model tiering (cross-model-guarded) ---
844
844
 
845
- test('workerModelFor: Quality tiers down to haiku; other workers keep the panel model', () => {
845
+ test('workerModelFor: Quality runs on its sonnet tier (task 1003318 — haiku\'s window overflowed); other workers keep the panel model', () => {
846
846
  assert.equal(
847
- grader.workerModelFor('quality', 'claude-sonnet-5', 'claude-fable-5'),
848
- 'claude-haiku-4-5-20251001'
847
+ grader.workerModelFor('quality', 'claude-opus-5', 'claude-fable-5'),
848
+ 'claude-sonnet-5'
849
849
  );
850
850
  for (const kind of ['narc', 'hacker', 'efficiency']) {
851
- assert.equal(grader.workerModelFor(kind, 'claude-sonnet-5', 'claude-fable-5'), 'claude-sonnet-5');
851
+ assert.equal(grader.workerModelFor(kind, 'claude-opus-5', 'claude-fable-5'), 'claude-opus-5');
852
852
  }
853
853
  });
854
854
 
855
855
  test('workerModelFor: a tier override landing on the BUILDER family is refused (cross-model guarantee)', () => {
856
- // A haiku builder must not be quality-judged by haiku — fall back to the panel model.
856
+ // A sonnet builder must not be quality-judged by sonnet — fall back to the panel model.
857
857
  assert.equal(
858
- grader.workerModelFor('quality', 'claude-opus-5', 'claude-haiku-4-5-20251001'),
858
+ grader.workerModelFor('quality', 'claude-opus-5', 'claude-sonnet-5'),
859
859
  'claude-opus-5'
860
860
  );
861
861
  });
@@ -1795,6 +1795,7 @@ test('v4 #465/#467: TRANSIENT_WORKER_ERRORS covers infra noise but NOT timeout',
1795
1795
  assert.ok(t.has(code), `${code} should be retryable`);
1796
1796
  }
1797
1797
  assert.ok(!t.has('SUBAGENT_TIMEOUT'), 'timeout is a real budget signal, never retried');
1798
+ assert.ok(!t.has('PROMPT_TOO_LONG'), 'a window overflow is deterministic, never retried (task 1003318)');
1798
1799
  });
1799
1800
 
1800
1801
  test('v4 #467: audit-exports classifyExports — consumed / test-only / DEAD', () => {
@@ -0,0 +1,324 @@
1
+ // tests/grader_quality_window_overflow.mjs
2
+ //
3
+ // task 1003318: the grader's Quality worker ran on haiku 4.5, whose 200k context
4
+ // window overflows on any diff over ~150 KB. The CLI exits before the worker
5
+ // starts (`Prompt is too long: the request is ~251248 tokens (limit 200000)`),
6
+ // the exit was retried once as if transient, the dead axis was forwarded to the
7
+ // server as code_quality: null → Number(null) === 0 → a fabricated
8
+ // "FAIL — code_quality below 6" at (9+0)/2 = 4.5. Deterministic, not flaky:
9
+ // tasks 1003297 and 1003298 each burned three ~$5 panel runs on it.
10
+ //
11
+ // Three things must hold now:
12
+ // 1. the Quality tier is a model whose window covers the diffs the panel sees
13
+ // (claude-sonnet-5), still behind the ADR 0024 cross-model guard;
14
+ // 2. a window overflow is recorded as its own non-transient worker error
15
+ // (PROMPT_TOO_LONG) — no retry, stderr tail kept — and the panel treats it
16
+ // as missing signal (CONCERNS), never a 0/10 axis;
17
+ // 3. the server, seeing an explicit code_quality: null corroborated by an
18
+ // errored no-finding Quality worker, records the lens as UNAVAILABLE
19
+ // (json-null axis, score on the axis that ran, cq_lens_unavailable) and
20
+ // parks the task for manual-confirm — fail-closed, never an auto-pass, and
21
+ // never the fabricated 4.5. An uncorroborated null stays schema_error.
22
+ //
23
+ // Run: node tests/grader_quality_window_overflow.mjs
24
+
25
+ import { strict as assert } from 'node:assert';
26
+ import { createRequire } from 'node:module';
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const rubric = require('../modules/grading/grader-rubric.json');
30
+ const grader = require('../modules/grading/grader.js');
31
+ const workersMod = require('../modules/grading/grader-workers/index.js');
32
+ const ship = require('../scripts/gds/ship.js');
33
+
34
+ const { aggregatePanel, validateAndScore, workerModelFor } = grader;
35
+ const { runWorker, TRANSIENT_WORKER_ERRORS } = workersMod;
36
+ const { formatGradeOutcome } = ship;
37
+
38
+ let passed = 0;
39
+ let failed = 0;
40
+ function test(name, fn) {
41
+ try { fn(); passed++; console.log(` ok ${name}`); }
42
+ catch (err) { failed++; console.error(` FAIL ${name}`); console.error(` ${err.message}`); }
43
+ }
44
+ async function atest(name, fn) {
45
+ try { await fn(); passed++; console.log(` ok ${name}`); }
46
+ catch (err) { failed++; console.error(` FAIL ${name}`); console.error(` ${err.message}`); }
47
+ }
48
+
49
+ const OVERFLOW_LINE = 'Prompt is too long: the request is ~251248 tokens (limit 200000)';
50
+
51
+ // The exact error runSubagent throws on a nonzero exit (grader-subagent.js).
52
+ const nonzeroExit = ({ exitCode = 1, stderr = '', stdout = '' } = {}) =>
53
+ Object.assign(new Error(`subagent exited with code ${exitCode}`), {
54
+ code: 'SUBAGENT_NONZERO_EXIT', exit_code: exitCode, run_ms: 120, stdout, stderr,
55
+ });
56
+
57
+ // A Quality-shaped worker stub: no spawning, the real error short-circuit shape.
58
+ const STUB_QUALITY = {
59
+ KIND: 'quality', NAME: 'Quality', SPECIALTY: 'stub',
60
+ buildPrompt: () => 'prompt',
61
+ extractJson: (s) => JSON.parse(s),
62
+ computeVerdict: (parsed, meta) => (meta.subprocess_error
63
+ ? { verdict: 'fail', scores: {}, issues: [], notes_md: '', confidence: 0, error: meta.subprocess_error }
64
+ : { verdict: 'pass', scores: { code_quality: 9 }, issues: [], notes_md: '', confidence: 0.9, error: null }),
65
+ };
66
+
67
+ const healthy = (kind, scores) => ({ kind, verdict: 'pass', scores, issues: [], confidence: 0.85, error: null });
68
+
69
+ // ---------- 1. the tier ----------
70
+
71
+ test('rubric: the Quality worker tier is claude-sonnet-5, not the 200k-window haiku', () => {
72
+ assert.equal(rubric.v4.worker_model_tiers.quality, 'claude-sonnet-5');
73
+ assert.doesNotMatch(String(rubric.v4.worker_model_tiers.quality), /haiku/);
74
+ assert.match(rubric.v4.worker_model_tiers_note, /1003318/, 'the note records why the tier moved');
75
+ });
76
+
77
+ test('workerModelFor: the sonnet tier is used for a fable builder, refused for a sonnet builder (ADR 0024 guard)', () => {
78
+ assert.equal(workerModelFor('quality', 'claude-sonnet-5', 'claude-fable-5'), 'claude-sonnet-5');
79
+ assert.equal(workerModelFor('quality', 'claude-opus-5', 'claude-sonnet-5'), 'claude-opus-5',
80
+ 'a sonnet builder must not be graded by its own family — falls back to the panel model');
81
+ });
82
+
83
+ // ---------- 2. the worker ----------
84
+
85
+ await atest('runWorker: a "Prompt is too long" exit is recorded as PROMPT_TOO_LONG and NOT retried', async () => {
86
+ let calls = 0;
87
+ const r = await runWorker(STUB_QUALITY, {
88
+ task: {}, diff: '', changedFiles: [], model: 'm',
89
+ runSubagentFn: async () => { calls++; throw nonzeroExit({ exitCode: 1, stderr: OVERFLOW_LINE }); },
90
+ });
91
+ assert.equal(calls, 1, 'a window overflow is deterministic — the retry only burns another round');
92
+ assert.equal(r.retried, false);
93
+ assert.equal(r.error, 'PROMPT_TOO_LONG');
94
+ assert.equal(TRANSIENT_WORKER_ERRORS.has('PROMPT_TOO_LONG'), false);
95
+ assert.deepEqual(r.scores, {}, 'no axis is fabricated for a worker that never ran');
96
+ assert.equal(r.exit_code, 1);
97
+ assert.match(r.stderr_tail, /Prompt is too long/, 'the CLI line survives so the grade row names its cause');
98
+ });
99
+
100
+ await atest('runWorker: the overflow line on STDOUT classifies the same way', async () => {
101
+ const r = await runWorker(STUB_QUALITY, {
102
+ task: {}, diff: '', changedFiles: [], model: 'm',
103
+ runSubagentFn: async () => { throw nonzeroExit({ exitCode: 1, stdout: OVERFLOW_LINE }); },
104
+ });
105
+ assert.equal(r.error, 'PROMPT_TOO_LONG');
106
+ assert.equal(r.retried, false);
107
+ });
108
+
109
+ await atest('runWorker: an unrelated nonzero exit keeps SUBAGENT_NONZERO_EXIT and its one retry (#465 unchanged)', async () => {
110
+ let calls = 0;
111
+ const r = await runWorker(STUB_QUALITY, {
112
+ task: {}, diff: '', changedFiles: [], model: 'm',
113
+ runSubagentFn: async () => { calls++; throw nonzeroExit({ exitCode: 1, stderr: 'Invalid API key · Run /login' }); },
114
+ });
115
+ assert.equal(calls, 2);
116
+ assert.equal(r.retried, true);
117
+ assert.equal(r.error, 'SUBAGENT_NONZERO_EXIT');
118
+ });
119
+
120
+ const DEAD_QUALITY = {
121
+ kind: 'quality', verdict: 'fail', error: 'PROMPT_TOO_LONG', scores: {}, issues: [],
122
+ confidence: 0, exit_code: 1, stderr_tail: OVERFLOW_LINE,
123
+ };
124
+ const PANEL_WITH_DEAD_QUALITY = [
125
+ healthy('narc', { functional_fidelity: 9 }),
126
+ DEAD_QUALITY,
127
+ healthy('hacker', { security_score: 9 }),
128
+ healthy('efficiency', { efficiency_score: 8 }),
129
+ ];
130
+
131
+ test('aggregatePanel: an overflowed Quality is missing signal → CONCERNS with a null code_quality axis, not a 0', () => {
132
+ const r = aggregatePanel(PANEL_WITH_DEAD_QUALITY);
133
+ assert.equal(r.signals.panel_outcome, 'concerns');
134
+ assert.equal(r.passed, false);
135
+ assert.equal(r.rubric_scores.code_quality, null);
136
+ assert.equal(r.signals.error_count, 1);
137
+ assert.equal(r.signals.fail_count, 0);
138
+ assert.match(r.notes_md, /quality\s+ERROR.*PROMPT_TOO_LONG/);
139
+ });
140
+
141
+ // ---------- 3. the server ----------
142
+
143
+ // What ship-grade-steps.buildGradePayload forwards for the panel above.
144
+ const OVERFLOW_PAYLOAD = {
145
+ functional_fidelity: 9,
146
+ code_quality: null,
147
+ security_score: 9,
148
+ efficiency_score: 8,
149
+ notes_md: 'quality overflowed; the rest passed',
150
+ confidence: 0.3,
151
+ issues: [],
152
+ };
153
+ const OVERFLOW_META = { workers: PANEL_WITH_DEAD_QUALITY, panel_outcome: 'concerns' };
154
+
155
+ test('validateAndScore: corroborated null cq → lens UNAVAILABLE, score on the axis that ran, never the fabricated 4.5', () => {
156
+ const r = validateAndScore(OVERFLOW_PAYLOAD, OVERFLOW_META);
157
+ assert.equal(r.passed, false, 'a gating lens that did not run can never auto-pass');
158
+ assert.equal(r.signals.cq_lens_unavailable, true);
159
+ assert.notEqual(r.signals.schema_error, true);
160
+ assert.equal(r.rubric_scores.code_quality, null, 'json-null axis — the DB buckets it as unavailable, not a 0 fail');
161
+ assert.equal(r.rubric_scores.code_quality_effective, null);
162
+ assert.equal(r.rubric_scores.functional_fidelity, 9);
163
+ assert.equal(r.score, 9, 'the axis that ran — not (9+0)/2');
164
+ assert.match(r.notes_md, /lens unavailable/);
165
+ assert.match(r.notes_md, /manual-confirm/);
166
+ assert.doesNotMatch(r.notes_md, /code_quality below/);
167
+ assert.doesNotMatch(r.notes_md, /cq=0\//);
168
+ });
169
+
170
+ test('validateAndScore: null cq with a HEALTHY Quality in signals → fail-closed schema_error (injection shape)', () => {
171
+ const r = validateAndScore(OVERFLOW_PAYLOAD, { workers: [
172
+ healthy('narc', { functional_fidelity: 9 }),
173
+ { ...healthy('quality', { code_quality: null }) },
174
+ healthy('hacker', { security_score: 9 }),
175
+ ] });
176
+ assert.equal(r.passed, false);
177
+ assert.equal(r.signals.schema_error, true);
178
+ assert.notEqual(r.signals.cq_lens_unavailable, true);
179
+ assert.match(r.notes_md, /without a corroborated Quality outage/);
180
+ });
181
+
182
+ test('validateAndScore: null cq with an errored Quality that still raised a major issue → fail-closed (a finding wins)', () => {
183
+ const r = validateAndScore(OVERFLOW_PAYLOAD, { workers: [
184
+ healthy('narc', { functional_fidelity: 9 }),
185
+ { ...DEAD_QUALITY, issues: [{ severity: 'major', file: 'x.js', summary: 'real finding' }] },
186
+ healthy('hacker', { security_score: 9 }),
187
+ ] });
188
+ assert.equal(r.passed, false);
189
+ assert.equal(r.signals.schema_error, true);
190
+ assert.notEqual(r.signals.cq_lens_unavailable, true);
191
+ });
192
+
193
+ test('validateAndScore: null cq with NO workers signal at all → fail-closed schema_error', () => {
194
+ const r = validateAndScore(OVERFLOW_PAYLOAD, {});
195
+ assert.equal(r.passed, false);
196
+ assert.equal(r.signals.schema_error, true);
197
+ });
198
+
199
+ test('validateAndScore: a JUDGED cq of 0 beside an errored Quality is still a real FAIL (only corroborated null is a lens outage)', () => {
200
+ const r = validateAndScore({ ...OVERFLOW_PAYLOAD, code_quality: 0, confidence: 0.9 }, OVERFLOW_META);
201
+ assert.equal(r.passed, false);
202
+ assert.notEqual(r.signals.cq_lens_unavailable, true);
203
+ assert.equal(r.rubric_scores.code_quality, 0);
204
+ assert.match(r.notes_md, /code_quality below/);
205
+ });
206
+
207
+ test('validateAndScore: both lenses out (null ff + null cq, both corroborated) → unavailable, score 0, no pass', () => {
208
+ const r = validateAndScore({ ...OVERFLOW_PAYLOAD, functional_fidelity: null }, { workers: [
209
+ { kind: 'narc', verdict: 'fail', error: 'SUBAGENT_TIMEOUT', scores: {}, issues: [] },
210
+ DEAD_QUALITY,
211
+ healthy('hacker', { security_score: 9 }),
212
+ ] });
213
+ assert.equal(r.passed, false);
214
+ assert.equal(r.signals.ff_lens_unavailable, true);
215
+ assert.equal(r.signals.cq_lens_unavailable, true);
216
+ assert.equal(r.score, 0);
217
+ assert.equal(r.rubric_scores.functional_fidelity, null);
218
+ assert.equal(r.rubric_scores.code_quality, null);
219
+ });
220
+
221
+ test('validateAndScore: the healthy path is byte-for-byte unchanged (both axes judged → PASS at (9+8)/2)', () => {
222
+ const r = validateAndScore({ functional_fidelity: 9, code_quality: 8, confidence: 0.9, issues: [], notes_md: '' }, {
223
+ workers: [healthy('narc', { functional_fidelity: 9 }), healthy('quality', { code_quality: 8 }), healthy('hacker', { security_score: 9 })],
224
+ });
225
+ assert.equal(r.passed, true);
226
+ assert.equal(r.score, 8.5);
227
+ assert.notEqual(r.signals.cq_lens_unavailable, true);
228
+ });
229
+
230
+ // ---------- the builder-facing card ----------
231
+
232
+ test('formatGradeOutcome: a Quality-lens outage reads as UNAVAILABLE + manual-confirm, never "FAIL 4.5 — fix the issues"', () => {
233
+ const g = validateAndScore(OVERFLOW_PAYLOAD, OVERFLOW_META);
234
+ const out = formatGradeOutcome(g, 1003318);
235
+ assert.match(out, /QUALITY LENS UNAVAILABLE/i);
236
+ assert.match(out, /not a quality rejection/i);
237
+ assert.match(out, /Prompt is too long/, 'the CLI line explains the outage');
238
+ assert.match(out, /--regrade/);
239
+ assert.match(out, /\/confirm/, 'manual-confirm is the release valve');
240
+ assert.doesNotMatch(out, /Fix the issues above/);
241
+ assert.doesNotMatch(out, /blended 4\.5/);
242
+ assert.doesNotMatch(out, /code_quality:\s+0\/10/);
243
+ });
244
+
245
+ // ---------- the review's adversarial shapes (a dark lens never softens the gate) ----------
246
+
247
+ await atest('runWorker: a SUBAGENT_TIMEOUT whose partial stdout quotes the overflow sentence keeps its timeout tag', async () => {
248
+ const r = await runWorker(STUB_QUALITY, {
249
+ task: {}, diff: '', changedFiles: [], model: 'm',
250
+ runSubagentFn: async () => {
251
+ throw Object.assign(new Error('timeout'), { code: 'SUBAGENT_TIMEOUT', run_ms: 360000, stdout: `quoting: ${OVERFLOW_LINE}`, stderr: '' });
252
+ },
253
+ });
254
+ assert.equal(r.error, 'SUBAGENT_TIMEOUT', 'the #467 budget signal is never relabelled');
255
+ });
256
+
257
+ await atest('runWorker: stdout is consulted only when stderr is silent (the model may quote a diff that mentions the phrase)', async () => {
258
+ const r = await runWorker(STUB_QUALITY, {
259
+ task: {}, diff: '', changedFiles: [], model: 'm',
260
+ runSubagentFn: async () => { throw nonzeroExit({ exitCode: 1, stderr: 'Invalid API key · Run /login', stdout: `diff says: ${OVERFLOW_LINE}` }); },
261
+ });
262
+ assert.equal(r.error, 'SUBAGENT_NONZERO_EXIT');
263
+ assert.equal(r.retried, true);
264
+ });
265
+
266
+ const BLOCKER = { severity: 'blocker', file: 'src/gds/routes/x.js', summary: 'ungated write route' };
267
+ const HACKER_FAIL_META = { workers: [
268
+ healthy('narc', { functional_fidelity: 9 }),
269
+ DEAD_QUALITY,
270
+ { kind: 'hacker', verdict: 'fail', scores: { security_score: 2 }, issues: [BLOCKER], confidence: 0.9, error: null },
271
+ ], panel_outcome: 'fail' };
272
+
273
+ test('validateAndScore: a Hacker BLOCKER beside a dead Quality is a FAIL with a missing axis, never an outage', () => {
274
+ const r = validateAndScore({ ...OVERFLOW_PAYLOAD, security_score: 2, confidence: 0, issues: [BLOCKER] }, HACKER_FAIL_META);
275
+ assert.equal(r.passed, false);
276
+ assert.equal(r.signals.cq_lens_unavailable, true, 'the axis really is unavailable');
277
+ assert.equal(r.rubric_scores.code_quality, null);
278
+ assert.doesNotMatch(r.notes_md, /not a quality rejection/);
279
+ assert.doesNotMatch(r.notes_md, /manual-confirm available/);
280
+ assert.match(r.notes_md, /FAIL/);
281
+ });
282
+
283
+ test('formatGradeOutcome: with a blocker from another lens the card prints the findings, not the outage invitation', () => {
284
+ const g = validateAndScore({ ...OVERFLOW_PAYLOAD, security_score: 2, confidence: 0, issues: [BLOCKER] }, HACKER_FAIL_META);
285
+ const out = formatGradeOutcome(g, 1003318);
286
+ assert.doesNotMatch(out, /QUALITY LENS UNAVAILABLE/);
287
+ assert.match(out, /Issues \(1\)/);
288
+ assert.match(out, /ungated write route/);
289
+ assert.match(out, /Fix the issues above/);
290
+ });
291
+
292
+ test('formatGradeOutcome: an authorization refusal outranks the lens outage (task 1002614 stays closed)', () => {
293
+ const r = validateAndScore(OVERFLOW_PAYLOAD, OVERFLOW_META);
294
+ const g = { ...r, signals: { ...r.signals, server_guard: { tripped: [{ guard: 'permission_path' }], authorization_refusal: true } } };
295
+ const out = formatGradeOutcome(g, 1003318);
296
+ assert.match(out, /REFUSED \(authorization\)/);
297
+ assert.doesNotMatch(out, /QUALITY LENS UNAVAILABLE/);
298
+ assert.doesNotMatch(out, /--regrade re-runs the same server check[\s\S]*\/confirm/, 'no confirm curl after a refusal');
299
+ });
300
+
301
+ test('validateAndScore: a forged cq_lens_unavailable flag on a judged grade is cleared, and the card prints the ordinary FAIL', () => {
302
+ const r = validateAndScore({ functional_fidelity: 3, code_quality: 2, confidence: 0.9, issues: [], notes_md: '' }, {
303
+ cq_lens_unavailable: true, ff_lens_unavailable: true,
304
+ workers: [healthy('narc', { functional_fidelity: 3 }), healthy('quality', { code_quality: 2 }), healthy('hacker', { security_score: 9 })],
305
+ });
306
+ assert.equal(r.passed, false);
307
+ assert.equal('cq_lens_unavailable' in r.signals, false, 'client flag must not survive');
308
+ assert.equal('ff_lens_unavailable' in r.signals, false);
309
+ assert.equal(r.rubric_scores.code_quality, 2);
310
+ const out = formatGradeOutcome(r, 1003318);
311
+ assert.doesNotMatch(out, /QUALITY LENS UNAVAILABLE/);
312
+ assert.match(out, /Subagent grade: FAIL/);
313
+ });
314
+
315
+ test('validateAndScore: the outage line still names an absent / low confidence (the #489 signal is not hidden)', () => {
316
+ const absent = validateAndScore({ ...OVERFLOW_PAYLOAD, confidence: null }, OVERFLOW_META);
317
+ assert.equal(absent.passed, false);
318
+ assert.match(absent.notes_md, /UNAVAILABLE —[^\n]*confidence absent/);
319
+ const low = validateAndScore(OVERFLOW_PAYLOAD, OVERFLOW_META); // confidence 0.3 — the panel's fallthrough value
320
+ assert.match(low.notes_md, /UNAVAILABLE —[^\n]*low confidence 0\.3/);
321
+ });
322
+
323
+ console.log(`\n${passed} passed, ${failed} failed`);
324
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,163 @@
1
+ // tests/hall_preview_identity.mjs — hermetic unit test for the hall preview
2
+ // harness's live-identity layer (task 1003315: the preview greeted every
3
+ // builder as whoever's snapshot was checked into fixtures/me.json).
4
+ //
5
+ // NO disk, NO network, NO session: the session object and fetch are injected.
6
+ //
7
+ // Run: node tests/hall_preview_identity.mjs
8
+
9
+ import { strict as assert } from 'node:assert';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const lib = require('../scripts/hall-preview/live-identity.js');
14
+
15
+ let passed = 0, failed = 0;
16
+ function t(name, fn) {
17
+ try { fn(); passed++; console.log(` PASS ${name}`); }
18
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
19
+ }
20
+ async function ta(name, fn) {
21
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
22
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
23
+ }
24
+
25
+ const SESSION = { token: 'tok', apiBase: 'https://example.test' };
26
+ const ME = { builder: { id: '1000094', github_login: 'scootermack', rank: 'archon' } };
27
+
28
+ // A fetch stub: routes are keyed by the api path after /api/bongos/v1/. A number
29
+ // means "answer that status"; a missing key means 404.
30
+ function stubFetch(routes, { record } = {}) {
31
+ return async (url) => {
32
+ const p = String(url).split('/api/bongos/v1/')[1];
33
+ if (record) record.push(p);
34
+ const hit = Object.prototype.hasOwnProperty.call(routes, p) ? routes[p] : undefined;
35
+ if (hit === undefined) return { ok: false, status: 404, json: async () => ({}) };
36
+ if (typeof hit === 'number') return { ok: false, status: hit, json: async () => ({}) };
37
+ return { ok: true, status: 200, json: async () => hit };
38
+ };
39
+ }
40
+ const allOk = (extra = {}) => stubFetch(Object.assign({ me: ME }, extra));
41
+
42
+ // ---------- key derivation ----------
43
+ t('overrideKey: the path alone, except where ?section= picks the row shape', () => {
44
+ assert.equal(lib.overrideKey('me'), 'me');
45
+ assert.equal(lib.overrideKey('me/'), 'me', 'a trailing slash is the same key');
46
+ assert.equal(lib.overrideKey('builders/7/activity', { section: 'goals' }), 'builders/7/activity?section=goals');
47
+ assert.equal(lib.overrideKey('builders/7/activity', { limit: '20' }), 'builders/7/activity',
48
+ 'a query that is NOT ?section= does not fork the key');
49
+ });
50
+
51
+ t('expandPath: :id expands, and the key drops the non-discriminating query', () => {
52
+ assert.deepEqual(lib.expandPath('builders/:id/profile', '1000094'),
53
+ { key: 'builders/1000094/profile', url: 'builders/1000094/profile' });
54
+ assert.deepEqual(lib.expandPath('me/recent-ships?limit=30', '1'),
55
+ { key: 'me/recent-ships', url: 'me/recent-ships?limit=30' },
56
+ 'limit is a fetch detail, not part of the key the harness looks up');
57
+ assert.deepEqual(lib.expandPath('builders/:id/activity?section=goals', '9'),
58
+ { key: 'builders/9/activity?section=goals', url: 'builders/9/activity?section=goals' });
59
+ });
60
+
61
+ t('ME_SCOPED_PATHS: /me leads, and no box/terminal read is snapshotted', () => {
62
+ assert.equal(lib.ME_SCOPED_PATHS[0], 'me');
63
+ const box = lib.ME_SCOPED_PATHS.filter((p) => /(^|\/)box(\/|$)|terminal/.test(p));
64
+ assert.deepEqual(box, [], 'box endpoints carry connection details and are never fetched here');
65
+ });
66
+
67
+ // ---------- the fail-open paths (a preview must never die over this) ----------
68
+ await ta('no session: falls back, and never touches the network', async () => {
69
+ let called = 0;
70
+ const res = await lib.resolveLiveIdentity({ session: null, fetchImpl: async () => { called += 1; } });
71
+ assert.equal(res.ok, false);
72
+ assert.equal(called, 0);
73
+ assert.match(res.why, /builder-setup/);
74
+ });
75
+
76
+ await ta('a session with no apiBase: falls back rather than fetching a bare path', async () => {
77
+ const res = await lib.resolveLiveIdentity({
78
+ session: { token: 'tok' },
79
+ fetchImpl: async () => { throw new Error('should not fetch'); },
80
+ });
81
+ assert.equal(res.ok, false);
82
+ assert.match(res.why, /names no instance/);
83
+ });
84
+
85
+ await ta('401: falls back with the reauth line, NOT an exit (cli-lib apiCall would exit)', async () => {
86
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl: stubFetch({ me: 401 }) });
87
+ assert.equal(res.ok, false);
88
+ assert.match(res.why, /builder-reauth/);
89
+ });
90
+
91
+ await ta('offline: falls back with the unreachable instance named', async () => {
92
+ const res = await lib.resolveLiveIdentity({
93
+ session: SESSION,
94
+ fetchImpl: async () => { throw new Error('ECONNREFUSED'); },
95
+ });
96
+ assert.equal(res.ok, false);
97
+ assert.match(res.why, /example\.test/);
98
+ assert.match(res.why, /ECONNREFUSED/);
99
+ });
100
+
101
+ await ta('a /me with no builder: falls back (a half-identity is worse than the fixture)', async () => {
102
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl: stubFetch({ me: { builder: null } }) });
103
+ assert.equal(res.ok, false);
104
+ assert.match(res.why, /no builder/);
105
+ });
106
+
107
+ // ---------- the happy path ----------
108
+ await ta('signed in: /me is YOUR account, keyed for the harness lookup', async () => {
109
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl: allOk() });
110
+ assert.equal(res.ok, true);
111
+ assert.equal(res.builder.github_login, 'scootermack');
112
+ assert.equal(res.builder.rank, 'archon', 'the RANK is what unseals the Archon-gated pages in preview');
113
+ assert.deepEqual(res.overrides.get('me'), ME);
114
+ });
115
+
116
+ await ta('id-bearing reads are fetched for YOUR id, never the fixture builder id', async () => {
117
+ const record = [];
118
+ const fetchImpl = stubFetch({ me: ME, 'builders/1000094/profile': { bio: 'mine' } }, { record });
119
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl });
120
+ assert.deepEqual(res.overrides.get('builders/1000094/profile'), { bio: 'mine' });
121
+ assert.ok(record.includes('builders/1000094/profile'));
122
+ assert.ok(!record.some((p) => p.includes('/95/')), 'never the checked-in fixture builder id');
123
+ });
124
+
125
+ await ta('the two activity sections land under DIFFERENT keys (row shapes differ)', async () => {
126
+ const fetchImpl = stubFetch({
127
+ me: ME,
128
+ 'builders/1000094/activity?section=tasks': { rows: ['task'] },
129
+ 'builders/1000094/activity?section=goals': { rows: ['goal'] },
130
+ });
131
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl });
132
+ assert.deepEqual(res.overrides.get('builders/1000094/activity?section=tasks'), { rows: ['task'] });
133
+ assert.deepEqual(res.overrides.get('builders/1000094/activity?section=goals'), { rows: ['goal'] });
134
+ });
135
+
136
+ await ta('a read this rank cannot make keeps its fixture; the rest still resolves', async () => {
137
+ const res = await lib.resolveLiveIdentity({
138
+ session: SESSION,
139
+ fetchImpl: stubFetch({ me: ME, 'inbox/awaiting-nod?limit=20': 403 }),
140
+ });
141
+ assert.equal(res.ok, true, 'a partial identity still shows YOUR name, rank and credits');
142
+ assert.ok(res.skipped.includes('inbox/awaiting-nod'), 'skipped, so the fixture serves it');
143
+ assert.equal(res.overrides.has('inbox/awaiting-nod'), false);
144
+ });
145
+
146
+ await ta('every request carries the session bearer, is a GET, and is time-boxed', async () => {
147
+ const seen = [];
148
+ const fetchImpl = async (url, init) => {
149
+ seen.push(init);
150
+ const p = String(url).split('/api/bongos/v1/')[1];
151
+ return { ok: p === 'me', status: p === 'me' ? 200 : 404, json: async () => ME };
152
+ };
153
+ await lib.resolveLiveIdentity({ session: SESSION, fetchImpl });
154
+ assert.ok(seen.length > 1);
155
+ for (const init of seen) {
156
+ assert.equal(init.headers.authorization, 'Bearer tok');
157
+ assert.equal(init.method, undefined, 'no method set means GET; the harness never writes');
158
+ assert.ok(init.signal, 'every read is time-boxed so boot cannot hang');
159
+ }
160
+ });
161
+
162
+ console.log(`\nhall_preview_identity: ${passed} passed, ${failed} failed`);
163
+ process.exit(failed ? 1 : 0);