@celilo/e2e 0.19.3 → 0.20.1

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.
Files changed (50) hide show
  1. package/README.md +30 -13
  2. package/bin/e2e-bake-management +196 -12
  3. package/bin/e2e-infra +0 -1
  4. package/bin/e2e-up +14 -3
  5. package/docker/Dockerfile.observer +12 -1
  6. package/docker/Dockerfile.target-machine +22 -1
  7. package/npm-registry-server/package.json +1 -1
  8. package/package.json +3 -3
  9. package/registry-server/package.json +1 -1
  10. package/scripts/pack-celilo-packages.ts +15 -0
  11. package/src/block-timing.test.ts +559 -0
  12. package/src/block-timing.ts +366 -0
  13. package/src/cli/build.test.ts +135 -4
  14. package/src/cli/build.ts +237 -94
  15. package/src/cli/command-registry.ts +21 -0
  16. package/src/cli/command-tree-parser.ts +11 -2
  17. package/src/cli/completion.ts +9 -0
  18. package/src/cli/host.ts +252 -0
  19. package/src/cli/index.ts +78 -51
  20. package/src/cli/module-discovery.ts +108 -13
  21. package/src/cli/scaffold.ts +18 -26
  22. package/src/container-manager.cleanup.test.ts +284 -0
  23. package/src/container-manager.runner.test.ts +351 -0
  24. package/src/container-manager.test.ts +84 -0
  25. package/src/container-manager.ts +721 -185
  26. package/src/docker-compose-generator.ts +135 -61
  27. package/src/doctor.test.ts +259 -4
  28. package/src/doctor.ts +276 -3
  29. package/src/exit-cleanup.test.ts +83 -1
  30. package/src/fleet-nameserver-gate.test.ts +45 -0
  31. package/src/host-vm.test.ts +156 -0
  32. package/src/host-vm.ts +230 -0
  33. package/src/index.ts +11 -0
  34. package/src/live-stack.test.ts +300 -0
  35. package/src/live-stack.ts +355 -0
  36. package/src/no-unjustified-sleep.test.ts +90 -0
  37. package/src/proxmox-provisioner.test.ts +18 -2
  38. package/src/proxmox-provisioner.ts +22 -0
  39. package/src/public-sim-routes.test.ts +9 -2
  40. package/src/repo-root.ts +33 -0
  41. package/src/run-args.test.ts +76 -0
  42. package/src/run-args.ts +89 -0
  43. package/src/runner.ts +213 -8
  44. package/src/shared-infra.ts +88 -32
  45. package/src/socks-proxy.ts +2 -0
  46. package/src/source-fingerprint.test.ts +213 -0
  47. package/src/source-fingerprint.ts +212 -0
  48. package/src/stage-simulator-inputs.ts +93 -0
  49. package/src/stages.ts +133 -0
  50. package/src/wait-for-run.ts +1 -0
@@ -0,0 +1,559 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import {
5
+ declaredBlockCaps,
6
+ declaredCapEntries,
7
+ mergeBlockTiming,
8
+ overBudgetBlocks,
9
+ parseBlockDurations,
10
+ parseJunitDurations,
11
+ serializeBlockTiming,
12
+ tightBlocks,
13
+ } from './block-timing';
14
+
15
+ const REPO_ROOT = join(import.meta.dir, '..', '..', '..');
16
+
17
+ /**
18
+ * Fixtures are copied verbatim out of real `e2e/results/*​/output.log` files —
19
+ * a hand-written mirror would give confident coverage of a format bun does not
20
+ * emit, which is the same bug one level up.
21
+ */
22
+
23
+ // modules/wireguard-manager/e2e, run 2026-09-05T04-35-43. Stage 1 ran 155s and
24
+ // blew an assertion; stages 2-8 are requireStage cascade-skips.
25
+ const CASCADE = `
26
+ (fail) wireguard-manager runs behind the idp on the private ingress > stage 1: firewall, registrar and public ingress [155030.65ms]
27
+ 8 | function requireStage(_label: string) {
28
+ (fail) wireguard-manager runs behind the idp on the private ingress > stage 2: authentik provides idp [39.19ms]
29
+ ^ error: Skipped: deploy stage failed — timeout after 120000ms
30
+ (fail) wireguard-manager runs behind the idp on the private ingress > stage 3: the control-plane tunnel [1.29ms]
31
+ ^ error: Skipped: deploy stage failed — timeout after 120000ms
32
+ `
33
+ .trim()
34
+ .split('\n');
35
+
36
+ // Modeled on the wave-0 census alerting log (e2e/results/2026-09-06T04-21-57,
37
+ // celilo#1291), where the layout was read verbatim: stage 1 failed for REAL at
38
+ // 1021397ms, and the text under its (fail) line is stage 2's skip message,
39
+ // because bun prints a block's own error ABOVE its (fail) line. The forward
40
+ // window the skip filter used before read that message and deleted stage 1's
41
+ // measurement — the exact block this file exists to keep.
42
+ const CENSUS_SHAPE = `
43
+ CeliloCommandError: module deploy caddy failed
44
+ stderr: "\\ntimeout after 180000ms",
45
+ exitCode: 124,
46
+ killed 1 dangling process
47
+ (fail) alerting reflects real module health > stage 1: a healthy deployed module raises no alerts [1021397.57ms]
48
+ 35 | describe('alerting reflects real module health', () => {
49
+ error: Skipped — prior stage failed (stage 1)
50
+ at requireStage (alerting.test.ts:40:31)
51
+ (fail) alerting reflects real module health > stage 2: a broken service produces an alert naming the failing check [6.91ms]
52
+ 35 | describe('alerting reflects real module health', () => {
53
+ error: Skipped — prior stage failed (stage 1)
54
+ at requireStage (alerting.test.ts:40:31)
55
+ (fail) alerting reflects real module health > stage 3: a degrading system accumulates alerts [3.47ms]
56
+ `
57
+ .trim()
58
+ .split('\n');
59
+
60
+ const CENSUS_JUNIT = `<testsuite name="alerting" tests="3">
61
+ <testcase name="stage 1: a healthy deployed module raises no alerts" classname="alerting reflects real module health" time="1021.398" />
62
+ <testcase name="stage 2: a broken service produces an alert naming the failing check" classname="alerting reflects real module health" time="0.007" />
63
+ </testsuite>`;
64
+
65
+ describe('parseBlockDurations', () => {
66
+ test('records the block that ran and drops the cascade-skips behind it', () => {
67
+ expect(parseBlockDurations(CASCADE)).toEqual({
68
+ 'stage 1: firewall, registrar and public ingress': 155031,
69
+ });
70
+ });
71
+
72
+ test('a sub-millisecond skip is never recorded as a fast healthy block', () => {
73
+ // The whole point: 39.19ms for "stage 2" is the absence of a measurement.
74
+ // Recorded, it reads as a block with 300s of headroom.
75
+ const blocks = parseBlockDurations(CASCADE);
76
+ expect(Object.keys(blocks)).not.toContain('stage 2: authentik provides idp');
77
+ });
78
+
79
+ test('a real failure that is not a cascade-skip IS recorded', () => {
80
+ const lines = [
81
+ '(fail) split-horizon views > deploy resolver (in dmz) + caddy [300001.00ms]',
82
+ ' ^ this test timed out after 300000ms.',
83
+ ];
84
+ expect(parseBlockDurations(lines)).toEqual({
85
+ 'deploy resolver (in dmz) + caddy': 300001,
86
+ });
87
+ });
88
+
89
+ test('bun printing its result list twice yields one entry', () => {
90
+ const once = '(pass) suite > stage 1 [1234.50ms]';
91
+ expect(parseBlockDurations([once, once])).toEqual({ 'stage 1': 1235 });
92
+ });
93
+
94
+ test('strips colour, since bun renders the list with ANSI', () => {
95
+ const lines = ['\x1b[32m(pass)\x1b[0m suite \x1b[2m>\x1b[0m stage 1 \x1b[2m[900.00ms]\x1b[0m'];
96
+ expect(parseBlockDurations(lines)).toEqual({ 'stage 1': 900 });
97
+ });
98
+
99
+ test('an unnested test keeps its own name', () => {
100
+ expect(parseBlockDurations(['(pass) deploys caddy directly [400.00ms]'])).toEqual({
101
+ 'deploys caddy directly': 400,
102
+ });
103
+ });
104
+
105
+ test('ignores lines that are not bun results', () => {
106
+ expect(parseBlockDurations(['Ran 3 tests across 1 file. [1892.67s]', ' 0 pass'])).toEqual({});
107
+ });
108
+
109
+ test('a real failure is not deleted by the sibling skip messages printed under its line', () => {
110
+ // The celilo#1291 regression: stage 1 ran 1021s and failed on its own
111
+ // error, and the marker filter read stage 2's "Skipped — prior stage
112
+ // failed" from beneath its (fail) line, classified it as a cascade-skip,
113
+ // and returned {} for the whole suite. The duration guard carries the
114
+ // record now: stage 1 failed and did real work, so it stays.
115
+ expect(parseBlockDurations(CENSUS_SHAPE, CENSUS_JUNIT)).toEqual({
116
+ 'stage 1: a healthy deployed module raises no alerts': 1021398,
117
+ });
118
+ });
119
+
120
+ test('census-shaped skip siblings are still dropped', () => {
121
+ // Stages 2 and 3 never ran: they failed, and their measured durations are
122
+ // a fraction of the did-no-work threshold.
123
+ const blocks = parseBlockDurations(CENSUS_SHAPE, CENSUS_JUNIT);
124
+ expect(Object.keys(blocks)).toEqual(['stage 1: a healthy deployed module raises no alerts']);
125
+ });
126
+ });
127
+
128
+ describe('mergeBlockTiming', () => {
129
+ test('replaces a suite wholesale so a split stops reporting the old block', () => {
130
+ const before = { alerting: { 'stage 1': 100, 'stage 2': 200 } };
131
+ expect(mergeBlockTiming(before, 'alerting', { 'stage 1a': 50, 'stage 1b': 60 })).toEqual({
132
+ alerting: { 'stage 1a': 50, 'stage 1b': 60 },
133
+ });
134
+ });
135
+
136
+ test('a run that parsed no blocks leaves the record alone', () => {
137
+ const before = { alerting: { 'stage 1': 100 } };
138
+ expect(mergeBlockTiming(before, 'alerting', {})).toBe(before);
139
+ });
140
+
141
+ test('other suites are untouched', () => {
142
+ const before = { alerting: { a: 1 }, smoke: { b: 2 } };
143
+ expect(mergeBlockTiming(before, 'alerting', { a: 9 })).toEqual({
144
+ alerting: { a: 9 },
145
+ smoke: { b: 2 },
146
+ });
147
+ });
148
+ });
149
+
150
+ test('serializeBlockTiming sorts suites and blocks so the file diffs by value', () => {
151
+ const out = serializeBlockTiming({ zed: { b: 2, a: 1 }, alpha: { c: 3 } });
152
+ expect(out).toBe(
153
+ '{\n "alpha": {\n "c": 3\n },\n "zed": {\n "a": 1,\n "b": 2\n }\n}\n',
154
+ );
155
+ });
156
+
157
+ describe('declaredBlockCaps', () => {
158
+ test('reads the third argument of each test block', () => {
159
+ const src = [
160
+ " test('stage 1: deploy', async () => {",
161
+ ' await thing();',
162
+ ' }, 300_000);',
163
+ '',
164
+ " test('stage 2: verify', async () => {",
165
+ ' }, 120_000);',
166
+ ].join('\n');
167
+ expect(declaredBlockCaps(src)).toEqual({
168
+ 'stage 1: deploy': 300_000,
169
+ 'stage 2: verify': 120_000,
170
+ });
171
+ });
172
+
173
+ test('a block with no third argument gets no cap, not a default', () => {
174
+ // It silently inherits the runner's `--timeout 3600000`. Reporting a
175
+ // fabricated 300s here would hide exactly that.
176
+ const src = [" test('uncapped', async () => {", ' });'].join('\n');
177
+ expect(declaredBlockCaps(src)).toEqual({});
178
+ });
179
+
180
+ test('a template-literal name is skipped rather than mis-keyed', () => {
181
+ const src = [' test(`stage 2: ${host} resolves`, async () => {', ' }, 60_000);'].join('\n');
182
+ // Interpolated names cannot be matched against a runtime block name, so the
183
+ // parser must not invent a key for them.
184
+ expect(Object.keys(declaredBlockCaps(src))).toHaveLength(0);
185
+ });
186
+
187
+ test('reads the wrapped format biome emits for stage()', () => {
188
+ const src = [
189
+ ' stage(',
190
+ " 'stage 1: stand up the topology',",
191
+ ' async () => {',
192
+ ' await thing();',
193
+ ' },',
194
+ ' 300_000,',
195
+ ' );',
196
+ ].join('\n');
197
+ expect(declaredBlockCaps(src)).toEqual({ 'stage 1: stand up the topology': 300_000 });
198
+ });
199
+
200
+ test('a wrapped block with no timeout inherits the hour, silently', () => {
201
+ const src = [
202
+ ' stage(',
203
+ " 'stage 2: verify',",
204
+ ' async () => {',
205
+ ' await thing();',
206
+ ' },',
207
+ ' );',
208
+ ].join('\n');
209
+ expect(declaredBlockCaps(src)).toEqual({});
210
+ });
211
+ });
212
+
213
+ describe('declaredCapEntries', () => {
214
+ test('reports the line of the closer that declared the cap', () => {
215
+ const src = [
216
+ ' stage(',
217
+ " 'stage 1: deploy',",
218
+ ' async () => {',
219
+ ' await thing();',
220
+ ' },',
221
+ ' 300_000,',
222
+ ' );',
223
+ ].join('\n');
224
+ expect(declaredCapEntries(src)).toEqual([{ name: 'stage 1: deploy', capMs: 300_000, line: 6 }]);
225
+ });
226
+
227
+ test('a nested object close followed by `});` does not steal the block name', () => {
228
+ // The literal shape that bit aspect-fanout-new-systems stage 1: a nested
229
+ // `},` line arms the timeout expectation, the `});` line consumed it, and
230
+ // the real block closer recorded as unattributed.
231
+ const src = [
232
+ ' stage(',
233
+ " 'stage 1: deploy',",
234
+ ' async () => {',
235
+ ' await net.respondWith({',
236
+ " interview: { 'a.b': 'c' },",
237
+ ' });',
238
+ ' });',
239
+ ' await thing();',
240
+ ' },',
241
+ ' 900_000,',
242
+ ' );',
243
+ ].join('\n');
244
+ expect(declaredBlockCaps(src)).toEqual({ 'stage 1: deploy': 900_000 });
245
+ });
246
+
247
+ test('a nested waitFor closer does not steal the block name or its cap', () => {
248
+ const src = [
249
+ ' stage(',
250
+ " 'stage 1: deploy',",
251
+ ' async () => {',
252
+ ' await net.waitFor(',
253
+ ' async () => {',
254
+ ' return (await probe()) === true;',
255
+ ' },',
256
+ ' 30_000,',
257
+ " 'probe to pass',",
258
+ ' );',
259
+ ' },',
260
+ ' 900_000,',
261
+ ' );',
262
+ ].join('\n');
263
+ expect(declaredBlockCaps(src)).toEqual({ 'stage 1: deploy': 900_000 });
264
+ });
265
+
266
+ test('a template-literal name yields an unattributed entry, not silence', () => {
267
+ const src = [' test(`stage ${n}: x`, async () => {', ' }, 60_000);'].join('\n');
268
+ expect(declaredCapEntries(src)).toEqual([{ name: null, capMs: 60_000, line: 2 }]);
269
+ });
270
+ });
271
+
272
+ describe('tightBlocks', () => {
273
+ const caps = { a: 300_000, b: 300_000, c: 120_000 };
274
+
275
+ test('flags a block at or above the fraction, ordered worst first', () => {
276
+ const got = tightBlocks({ a: 240_000, b: 291_000, c: 10_000 }, caps);
277
+ expect(got.map((t) => t.block)).toEqual(['b', 'a']);
278
+ });
279
+
280
+ test('a block with no declared cap is absent, not assumed healthy', () => {
281
+ expect(tightBlocks({ unknown: 3_000_000 }, caps)).toEqual([]);
282
+ });
283
+
284
+ test('a block with no measurement is absent', () => {
285
+ expect(tightBlocks({}, caps)).toEqual([]);
286
+ });
287
+
288
+ test('a block that blew its cap is still reported', () => {
289
+ expect(tightBlocks({ a: 300_001 }, caps)[0].fraction).toBeGreaterThan(1);
290
+ });
291
+ });
292
+
293
+ describe('overBudgetBlocks', () => {
294
+ // The celilo#1291 recurrence gate: a check over a finished run's junit.xml
295
+ // that FAILS when any block exceeds its declared cap. The census's alerting
296
+ // run produced exactly this junit against exactly these caps and every
297
+ // downstream consumer reported nothing, because the skip filter dropped the
298
+ // block before anything compared it to a cap. This function reads the junit
299
+ // directly, so the measurement cannot be filtered away first.
300
+ const source = [
301
+ " test('stage 1: a healthy deployed module raises no alerts', async () => {",
302
+ ' }, 300_000);',
303
+ " test('stage 2: a broken service produces an alert', async () => {",
304
+ ' }, 180_000);',
305
+ ].join('\n');
306
+
307
+ test('flags the census overrun: 1021398ms against a 300000ms declaration', () => {
308
+ const overruns = overBudgetBlocks(CENSUS_JUNIT, source);
309
+ expect(overruns).toEqual([
310
+ {
311
+ block: 'stage 1: a healthy deployed module raises no alerts',
312
+ ms: 1021398,
313
+ capMs: 300000,
314
+ fraction: 1021398 / 300000,
315
+ },
316
+ ]);
317
+ });
318
+
319
+ test('a run within its caps passes the gate', () => {
320
+ const within = CENSUS_JUNIT.replace('time="1021.398"', 'time="102.398"');
321
+ expect(overBudgetBlocks(within, source)).toEqual([]);
322
+ });
323
+
324
+ test('a block with no declared cap is not enforceable and stays out', () => {
325
+ const uncapped = CENSUS_JUNIT.replace(
326
+ 'stage 1: a healthy deployed module raises no alerts',
327
+ 'stage 1: an uncapped block',
328
+ );
329
+ expect(overBudgetBlocks(uncapped, source)).toEqual([]);
330
+ });
331
+
332
+ test('the census junit against the real alerting caps reports the real overrun', () => {
333
+ // Reach, not reasoning: the actual fixture from the wave-0 census run,
334
+ // against the caps the actual suite file declares.
335
+ const src = readFileSync(join(REPO_ROOT, 'e2e', 'tests', 'alerting.test.ts'), 'utf-8');
336
+ const overruns = overBudgetBlocks(CENSUS_JUNIT, src);
337
+ expect(overruns.length).toBe(1);
338
+ expect(overruns[0].fraction).toBeGreaterThan(3);
339
+ });
340
+ });
341
+
342
+ describe('the cap parser reaches the real suites', () => {
343
+ // Measuring reach, not reasoning about it. A style change that stops the
344
+ // parser matching would otherwise turn every suite into "no cap declared" —
345
+ // a confident, well-formed report about nothing (celilo#1268).
346
+ const files: string[] = [];
347
+ const roots = [join(REPO_ROOT, 'e2e', 'tests')];
348
+ for (const m of readdirSync(join(REPO_ROOT, 'modules'))) {
349
+ const d = join(REPO_ROOT, 'modules', m, 'e2e');
350
+ if (existsSync(d)) roots.push(d);
351
+ }
352
+ for (const r of roots) {
353
+ if (!existsSync(r)) continue;
354
+ for (const f of readdirSync(r)) if (f.endsWith('.test.ts')) files.push(join(r, f));
355
+ }
356
+
357
+ test('finds test files to read at all', () => {
358
+ expect(files.length).toBeGreaterThan(30);
359
+ });
360
+
361
+ test('most e2e blocks declare a cap the parser can read', () => {
362
+ let withCap = 0;
363
+ let blocks = 0;
364
+ for (const f of files) {
365
+ const src = readFileSync(f, 'utf-8');
366
+ blocks += (src.match(/^\s*(?:test|it|stage)(?:\.\w+)?\(/gm) ?? []).length;
367
+ withCap += Object.keys(declaredBlockCaps(src)).length;
368
+ }
369
+ // 227 of ~260 at the time this was written. The floor is deliberately well
370
+ // under that: it must fail on a parser that stops working, not on a suite
371
+ // being added.
372
+ expect(blocks).toBeGreaterThan(150);
373
+ expect(withCap / blocks).toBeGreaterThan(0.6);
374
+ });
375
+
376
+ test('reads split-horizon-views, the suite this gate was written for', () => {
377
+ const f = join(
378
+ REPO_ROOT,
379
+ 'modules',
380
+ 'knot-unbound-internal',
381
+ 'e2e',
382
+ 'split-horizon-views.test.ts',
383
+ );
384
+ const caps = declaredBlockCaps(readFileSync(f, 'utf-8'));
385
+ expect(Object.keys(caps).length).toBeGreaterThan(0);
386
+ for (const cap of Object.values(caps)) expect(cap).toBeLessThanOrEqual(300_000);
387
+ });
388
+ });
389
+
390
+ describe('no e2e suite declares the same block name twice', () => {
391
+ // The record is keyed on the block name, so two blocks sharing one inside a
392
+ // suite collapse into a single entry and the slower one hides the other. Hit
393
+ // for real while splitting celilo-apt-deploy: cutting stage 1 into six left a
394
+ // second "stage 3" and a second "stage 4" further down the file.
395
+ const files: string[] = [];
396
+ const roots = [join(REPO_ROOT, 'e2e', 'tests')];
397
+ for (const m of readdirSync(join(REPO_ROOT, 'modules'))) {
398
+ const d = join(REPO_ROOT, 'modules', m, 'e2e');
399
+ if (existsSync(d)) roots.push(d);
400
+ }
401
+ for (const r of roots) {
402
+ if (!existsSync(r)) continue;
403
+ for (const f of readdirSync(r)) if (f.endsWith('.test.ts')) files.push(join(r, f));
404
+ }
405
+
406
+ test('every declared block name is unique within its file', () => {
407
+ const dupes: string[] = [];
408
+ for (const f of files) {
409
+ const seen = new Set<string>();
410
+ for (const m of readFileSync(f, 'utf-8').matchAll(
411
+ /^\s*(?:test|it)(?:\.\w+)?\(\s*['"](.+?)['"]\s*,/gm,
412
+ )) {
413
+ const name = m[1];
414
+ if (seen.has(name)) dupes.push(`${f.slice(REPO_ROOT.length + 1)}: "${name}"`);
415
+ seen.add(name);
416
+ }
417
+ }
418
+ expect(dupes).toEqual([]);
419
+ });
420
+ });
421
+
422
+ // Emitted by `bun test --reporter=junit` for the cascade shape above: one real
423
+ // failure, one requireStage skip, one PASS. Copied from a real bun 1.3.3 run.
424
+ const CASCADE_JUNIT = `<?xml version="1.0" encoding="UTF-8"?>
425
+ <testsuites name="bun test" tests="3" assertions="2" failures="2" skipped="0" time="0.340297">
426
+ <testsuite name="cascade.test.ts" file="cascade.test.ts" tests="3" failures="2" time="0.324">
427
+ <testcase name="stage 1: the real work" classname="" time="0.123538" file="cascade.test.ts" line="4" assertions="1">
428
+ <failure type="AssertionError" />
429
+ </testcase>
430
+ <testcase name="stage 2: cascade" classname="" time="0.00026" file="cascade.test.ts" line="8" assertions="0">
431
+ <failure type="AssertionError" />
432
+ </testcase>
433
+ <testcase name="stage 3: passes" classname="" time="0.201089" file="cascade.test.ts" line="9" assertions="1" />
434
+ </testsuite>
435
+ </testsuites>`;
436
+
437
+ const CASCADE_STDOUT = [
438
+ '(fail) stage 1: the real work [123.54ms]',
439
+ ' ^ error: expect(received).toBe(expected)',
440
+ '(fail) stage 2: cascade [0.26ms]',
441
+ ' ^ error: Skipped: deploy stage failed — deploy: boom',
442
+ ]
443
+ .join('\n')
444
+ .split('\n');
445
+
446
+ describe('parseJunitDurations', () => {
447
+ test('reads seconds with microsecond precision into ms', () => {
448
+ expect(parseJunitDurations(CASCADE_JUNIT)).toEqual({
449
+ 'stage 1: the real work': 124,
450
+ 'stage 2: cascade': 0,
451
+ 'stage 3: passes': 201,
452
+ });
453
+ });
454
+
455
+ test('covers a block that PASSED — the whole reason this source exists', () => {
456
+ // bun's console prints nothing for a passing test. Measured across 108
457
+ // recorded output.log files: zero contain a "(pass)" line.
458
+ expect(parseJunitDurations(CASCADE_JUNIT)['stage 3: passes']).toBe(201);
459
+ });
460
+
461
+ test('unescapes an XML-escaped block name', () => {
462
+ const xml = '<testcase name="a &amp; b &lt;c&gt;" time="1.5" />';
463
+ expect(parseJunitDurations(xml)).toEqual({ 'a & b <c>': 1500 });
464
+ });
465
+
466
+ test('ignores a testcase with no time', () => {
467
+ expect(parseJunitDurations('<testcase name="x" />')).toEqual({});
468
+ });
469
+ });
470
+
471
+ describe('parseBlockDurations with a JUnit report', () => {
472
+ test('records the pass and the real failure, and drops the cascade-skip', () => {
473
+ expect(parseBlockDurations(CASCADE_STDOUT, CASCADE_JUNIT)).toEqual({
474
+ 'stage 1: the real work': 124,
475
+ 'stage 3: passes': 201,
476
+ });
477
+ });
478
+
479
+ test('falls back to stdout when a crash left no JUnit file', () => {
480
+ // Every bun JUnit failure is a bare <failure type="AssertionError" />, so
481
+ // nothing in the XML distinguishes a cascade-skip from a real failure, and
482
+ // the stdout fallback has only durations and markers. The record keys on
483
+ // the duration: stage 2's "Skipped:" marker is visible here and changes
484
+ // nothing.
485
+ expect(parseBlockDurations(CASCADE_STDOUT, undefined)).toEqual({
486
+ 'stage 1: the real work': 124,
487
+ });
488
+ });
489
+ });
490
+
491
+ // wireguard-manager-private, run 2026-09-05T09-30-56. bun prints a six-line
492
+ // source excerpt before the message when the throw carries a stack, so
493
+ // `error: Skipped:` lands well past where a short window would look. The
494
+ // four-line window this replaced recorded all six of these skips at 0.0s.
495
+ const SKIP_WITH_SOURCE_EXCERPT =
496
+ `(fail) wireguard-manager > stage 3: the control-plane tunnel [10.62ms]
497
+ 67 | let net: NetworkHandle;
498
+ 68 | let stageError: string | null = null;
499
+ 69 | let alphaKey = '';
500
+ 70 |
501
+ 71 | function requireStage(stage: string): void {
502
+ 72 | if (stageError) throw new Error(\`Skipped: \${stage} stage failed — \${stageError}\`);
503
+ ^
504
+ error: Skipped: tunnel stage failed — idp: celilo module import failed (exit 1)
505
+
506
+ at requireStage (/repo/modules/wireguard-manager/e2e/x.test.ts:72:29)
507
+ (fail) wireguard-manager > stage 4: internal resolver [0.85ms]
508
+ 67 | let net: NetworkHandle;
509
+ ^
510
+ error: Skipped: resolver stage failed — idp: celilo module import failed (exit 1)
511
+ `.split('\n');
512
+
513
+ describe('a cascade-skip behind a source excerpt', () => {
514
+ test('is dropped from the record by its did-no-work duration, marker or no marker', () => {
515
+ // This log's layout is the OPPOSITE of the census alerting log's: the skip
516
+ // message sits below its own (fail) line here, above it there. No window
517
+ // over the text attributes soundly across both; the duration does.
518
+ const junit = `<testcase name="stage 3: the control-plane tunnel" time="0.010616" />
519
+ <testcase name="stage 4: internal resolver" time="0.000852" />
520
+ <testcase name="stage 1: firewall, registrar and public ingress" time="300.001024" />`;
521
+ expect(parseBlockDurations(SKIP_WITH_SOURCE_EXCERPT, junit)).toEqual({
522
+ 'stage 1: firewall, registrar and public ingress': 300001,
523
+ });
524
+ });
525
+ });
526
+
527
+ describe('a failed block that did no work is not a measurement', () => {
528
+ // wireguard-manager-private 2026-09-05T09-42-54. bun prints the message AFTER
529
+ // the result line as each test completes and BEFORE it in the end-of-file
530
+ // listing, so the last failing block's "Skipped:" is above its own line in
531
+ // both. It leaked through the marker filter at 0.25ms.
532
+ const lines = [
533
+ '(fail) wireguard-manager > stage 8: pausing the manager does not disturb the tunnel [0.25ms]',
534
+ '[progress:start] stopping network | network stopped',
535
+ ' 0 pass',
536
+ ' 8 fail',
537
+ ];
538
+ const junit =
539
+ '<testcase name="stage 8: pausing the manager does not disturb the tunnel" time="0.00025" />';
540
+
541
+ test('is dropped even when the marker is nowhere the filter can see it', () => {
542
+ expect(parseBlockDurations(lines, junit)).toEqual({});
543
+ });
544
+
545
+ test('a fast block that PASSED is kept — that is a real measurement', () => {
546
+ const passing = ['(fail) other > stage 1 [500.00ms]'];
547
+ const j = '<testcase name="quick assertion" time="0.002" />';
548
+ expect(parseBlockDurations(passing, j)).toEqual({ 'quick assertion': 2 });
549
+ });
550
+
551
+ test('a failed block that did real work is kept', () => {
552
+ const j = '<testcase name="stage 1: deploy" time="300.001" />';
553
+ const l = [
554
+ '(fail) suite > stage 1: deploy [300001.00ms]',
555
+ ' ^ this test timed out after 300000ms.',
556
+ ];
557
+ expect(parseBlockDurations(l, j)).toEqual({ 'stage 1: deploy': 300001 });
558
+ });
559
+ });