@celilo/e2e 0.19.2 → 0.20.0

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 +171 -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 +54 -4
  14. package/src/cli/build.ts +204 -88
  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 +184 -0
  35. package/src/live-stack.ts +145 -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 +83 -32
  45. package/src/socks-proxy.ts +2 -0
  46. package/src/source-fingerprint.test.ts +213 -0
  47. package/src/source-fingerprint.ts +201 -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,366 @@
1
+ /**
2
+ * Per-`test()`-block durations, recorded where something can read them later.
3
+ *
4
+ * The 300s budget in `apps/celilo/CLAUDE.md` is PER TEST BLOCK, not per suite.
5
+ * `.e2e-timing.json` records suite totals only, so the one number the policy is
6
+ * written against was recorded nowhere. bun prints it — `(pass) suite > stage 1
7
+ * [155030.65ms]` — into `output.log`, which lives in `e2e/results/<ts>/` and is
8
+ * deleted with it. In practice only FAILED suites ever get read, so the
9
+ * population of blocks approaching the cap was unmeasured (celilo#1268).
10
+ *
11
+ * That gap is what let a table comparing SUITE totals against a PER-TEST cap
12
+ * stand as a finding for twenty minutes: there was no per-block number to check
13
+ * it against. Two suites falsified it by passing at durations the theory called
14
+ * impossible (`module-pause` 303s, `aspect-fanout-new-systems` 401s) — both
15
+ * spread over many blocks, neither anywhere near the cap.
16
+ *
17
+ * Pure parser + a merge, in its own module so it is testable: runner.ts calls
18
+ * main() at import time, so nothing declared there is reachable from a test.
19
+ */
20
+
21
+ import { stripAnsi } from './extract-failure';
22
+
23
+ /** suite name → test-block name → last measured duration in ms. */
24
+ export type BlockTiming = Record<string, Record<string, number>>;
25
+
26
+ /**
27
+ * One suite's per-block durations: what ran, and how long each block took.
28
+ *
29
+ * Durations come from bun's JUnit report when there is one, because that is the
30
+ * only source covering blocks that PASSED. Console output names a block only
31
+ * when it fails. A failed block that did no work is dropped by the duration
32
+ * guard below; there is no message-based skip filter, for the reason recorded
33
+ * there.
34
+ */
35
+ /**
36
+ * A failed block below this did no work, so its duration measures nothing.
37
+ *
38
+ * Nothing that stands up a container, deploys a module or waits on DNS returns
39
+ * in 50ms. A `requireStage` skip returns in a fraction of one.
40
+ */
41
+ const NOT_A_MEASUREMENT_MS = 50;
42
+
43
+ export function parseBlockDurations(lines: string[], junitXml?: string): Record<string, number> {
44
+ const failed = failedBlocks(lines);
45
+ const measured = junitXml ? parseJunitDurations(junitXml) : parseStdoutDurations(lines);
46
+ const out: Record<string, number> = {};
47
+ for (const [block, ms] of Object.entries(measured)) {
48
+ // A failed block that did no work is a cascade-skip, and recording it would
49
+ // enter a stage that never ran as a healthy fast one. The duration is the
50
+ // sound signal — NOT any "Skipped:" marker in the console text. bun prints
51
+ // a block's error message in two REAL and OPPOSITE layouts: below its own
52
+ // (fail) line (wireguard-manager-private 2026-09-05T09-30-56, where the
53
+ // message names its own stage) and ABOVE it, under the PREVIOUS block's
54
+ // line (alerting, wave-0 census e2e/results/2026-09-06T04-21-57, where the
55
+ // at-frames point into the NEXT stage's body). Same bun, days apart. Any
56
+ // window reading text next to a (fail) line attributes a sibling's message
57
+ // to this block in one layout or the other — and the forward version did
58
+ // exactly that on the census: stage 1 failed for real at 1021397ms, the
59
+ // filter read stage 2's skip message under its line, and deleted the one
60
+ // measurement this file exists to keep (celilo#1291). Worst case now is a
61
+ // skip that measured over 50ms entering the record as a fast healthy block
62
+ // — cosmetic noise nowhere near any cap — instead of a real measurement
63
+ // being deleted.
64
+ if (failed.has(block) && ms < NOT_A_MEASUREMENT_MS) continue;
65
+ out[block] = ms;
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Every block bun reported as failing, by name. */
71
+ function failedBlocks(lines: string[]): Set<string> {
72
+ const out = new Set<string>();
73
+ for (const line of lines.map(stripAnsi)) {
74
+ if (isResultLine(line) && line.startsWith('(fail)')) out.add(blockNameOf(line));
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * Per-testcase durations from bun's JUnit reporter.
81
+ *
82
+ * The reporter is the ONLY source that covers passing blocks. bun's console
83
+ * output prints `(fail) name [Nms]` as each test fails and nothing at all for a
84
+ * test that passes — measured across 108 recorded `output.log` files, of which
85
+ * ZERO contain a `(pass)` line. A record built from stdout therefore holds only
86
+ * the blocks that already broke, which is the exact opposite of the question
87
+ * ("which blocks are running out of room?").
88
+ *
89
+ * Found by shipping the stdout version and reading what it wrote: one entry, for
90
+ * the one stage that failed, out of seven that ran.
91
+ *
92
+ * `time` is seconds with microsecond precision.
93
+ */
94
+ export function parseJunitDurations(xml: string): Record<string, number> {
95
+ const out: Record<string, number> = {};
96
+ for (const m of xml.matchAll(/<testcase\b([^>]*)\/?>/g)) {
97
+ const attrs = m[1];
98
+ const name = attrs.match(/\bname="([^"]*)"/)?.[1];
99
+ const time = attrs.match(/\btime="([\d.]+)"/)?.[1];
100
+ if (!name || !time) continue;
101
+ out[unescapeXml(name)] = Math.round(Number(time) * 1000);
102
+ }
103
+ return out;
104
+ }
105
+
106
+ function unescapeXml(s: string): string {
107
+ return s
108
+ .replace(/&lt;/g, '<')
109
+ .replace(/&gt;/g, '>')
110
+ .replace(/&quot;/g, '"')
111
+ .replace(/&apos;/g, "'")
112
+ .replace(/&amp;/g, '&');
113
+ }
114
+
115
+ /**
116
+ * Fallback for a run that produced no JUnit file — a crash, or an older bun.
117
+ * Covers failures only, for the reason above.
118
+ */
119
+ function parseStdoutDurations(lines: string[]): Record<string, number> {
120
+ const plain = lines.map(stripAnsi);
121
+ const out: Record<string, number> = {};
122
+ for (const line of plain) {
123
+ if (!isResultLine(line)) continue;
124
+ const ms = line.match(/\[([\d.]+)ms\]\s*$/);
125
+ if (!ms) continue;
126
+ const block = blockNameOf(line);
127
+ if (!block) continue;
128
+ out[block] = Math.max(out[block] ?? 0, Math.round(Number(ms[1])));
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function isResultLine(l: string): boolean {
134
+ return /^\((?:pass|fail)\)\s+\S/.test(l);
135
+ }
136
+
137
+ /** bun renders nesting as "describe > block"; a cap is declared against the block. */
138
+ function blockNameOf(line: string): string {
139
+ const full = line.replace(/^\((?:pass|fail)\)\s+/, '').replace(/\s*\[[\d.]+ms\]\s*$/, '');
140
+ return (full.includes(' > ') ? full.slice(full.lastIndexOf(' > ') + 3) : full).trim();
141
+ }
142
+
143
+ /**
144
+ * Fold one suite's blocks into the stored record, replacing that suite's entry.
145
+ *
146
+ * The suite is replaced rather than merged so a split shows up on the next run:
147
+ * a block that no longer exists must stop being reported, or the record keeps
148
+ * a fixed suite looking broken forever.
149
+ */
150
+ export function mergeBlockTiming(
151
+ stored: BlockTiming,
152
+ suite: string,
153
+ blocks: Record<string, number>,
154
+ ): BlockTiming {
155
+ if (Object.keys(blocks).length === 0) return stored;
156
+ return { ...stored, [suite]: blocks };
157
+ }
158
+
159
+ /** Stable key order so the committed file diffs by value, not by iteration order. */
160
+ export function serializeBlockTiming(timing: BlockTiming): string {
161
+ const suites = Object.keys(timing).sort();
162
+ const ordered: BlockTiming = {};
163
+ for (const s of suites) {
164
+ const blocks = timing[s];
165
+ ordered[s] = Object.fromEntries(
166
+ Object.keys(blocks)
167
+ .sort()
168
+ .map((b) => [b, blocks[b]]),
169
+ );
170
+ }
171
+ return `${JSON.stringify(ordered, null, 2)}\n`;
172
+ }
173
+
174
+ // ─── Declared caps, and blocks that are running out of room ──────────
175
+
176
+ /**
177
+ * The per-block budget each block (test() or stage()) declares as its third
178
+ * argument.
179
+ *
180
+ * There is no other place to read it from. The runner spawns
181
+ * `bun test --timeout 3600000`, so the CLI value is a backstop for debug
182
+ * sessions; the number that governs a block is the literal in the source, and
183
+ * a block with no literal silently inherits the hour.
184
+ *
185
+ * Measured, not assumed: an inline third argument DOES override the CLI value
186
+ * (bun 1.3.3), including when the body swallows its own errors the way a staged
187
+ * e2e suite does.
188
+ *
189
+ * Handles both formats biome produces: the one-line open/close
190
+ * (`test('name', async () => {` ... `}, 300_000);`) and the wrapped one
191
+ * (`stage(\n 'name',\n async () => {` ... `},\n 300_000,\n);`).
192
+ */
193
+ export function declaredBlockCaps(source: string): Record<string, number> {
194
+ const caps: Record<string, number> = {};
195
+ for (const entry of declaredCapEntries(source)) {
196
+ if (entry.name !== null) caps[entry.name] = entry.capMs;
197
+ }
198
+ return caps;
199
+ }
200
+
201
+ export interface DeclaredCapEntry {
202
+ /**
203
+ * The block name as declared, or null when the closer could not be
204
+ * attributed to a keyed opener (a template-literal name interpolates at
205
+ * runtime, so the source text can never equal the name bun prints).
206
+ * The timeout-cap gate fails loudly on null-named over-bar entries rather
207
+ * than letting a real cap escape the manifest.
208
+ */
209
+ name: string | null;
210
+ capMs: number;
211
+ /** 1-indexed source line of the closer that declared the cap. */
212
+ line: number;
213
+ }
214
+
215
+ /**
216
+ * `declaredBlockCaps` with provenance: which line declared each cap, and
217
+ * which closers went unattributed. The e2e-timeout-cap gate consumes this
218
+ * so the manifest cites source lines and an over-bar closer the parser
219
+ * cannot key fails the gate instead of escaping silently.
220
+ */
221
+ export function declaredCapEntries(source: string): DeclaredCapEntry[] {
222
+ const lines = source.split('\n');
223
+ const entries: DeclaredCapEntry[] = [];
224
+ let pending: string | null = null; // the open block's name, if keyable
225
+ let awaitingName = false; // multi-line open: the name is on the next line
226
+ let closeIndent = -1; // the indent the block's closing `},` must sit at
227
+
228
+ // Biome's wrapped form indents the body close one level past the opener:
229
+ // stage( <- indent 2
230
+ // 'name',
231
+ // async () => { <- indent 4
232
+ // ... <- indent 6
233
+ // }, <- indent 4 = opener + 2
234
+ // 900_000,
235
+ // );
236
+ // The inline form closes at the opener's own indent:
237
+ // test('name', async () => { ... }, 300_000);
238
+ // A nested close (an object literal, a waitFor callback) is always DEEPER
239
+ // than the block's close indent, so matching on indent is what keeps a
240
+ // nested `},` from stealing the pending block's name — the failure that
241
+ // made stage 1 of aspect-fanout-new-systems read as unattributed.
242
+ const indentOf = (line: string) => line.length - line.trimStart().length;
243
+
244
+ for (const [index, line] of lines.entries()) {
245
+ // Any block-open clears `pending`, including one this parser cannot key on.
246
+ // Skipping the line instead would leave the PREVIOUS block's name armed, so
247
+ // the next `}, N)` would overwrite that block's cap with this one's.
248
+ // stage() is the shared wrapper from @celilo/e2e for staged suites (ce-3ei3);
249
+ // it takes the same (name, body, timeout) shape as test().
250
+ if (/^\s*(?:test|it|stage)(?:\.\w+)?\(/.test(line)) {
251
+ // A template-literal name interpolates at runtime, so the source text can
252
+ // never equal the name bun prints. Left uncapped rather than keyed on a
253
+ // string nothing will ever match.
254
+ pending = line.match(/^\s*(?:test|it|stage)(?:\.\w+)?\(\s*['"](.+?)['"]\s*,/)?.[1] ?? null;
255
+ awaitingName = /\(\s*$/.test(line);
256
+ closeIndent = indentOf(line) + (awaitingName ? 2 : 0);
257
+ continue;
258
+ }
259
+ if (awaitingName) {
260
+ const name = line.match(/^\s*['"](.+?)['"]\s*,?\s*$/);
261
+ pending = name?.[1] ?? null;
262
+ awaitingName = false;
263
+ continue;
264
+ }
265
+ if (closeIndent === -1 || indentOf(line) !== closeIndent) continue;
266
+ const bare = line.trimStart();
267
+
268
+ const close = bare.match(/^\},\s*([0-9_]+)\s*\)\s*;?\s*$/);
269
+ if (close) {
270
+ entries.push({ name: pending, capMs: Number(close[1].replace(/_/g, '')), line: index + 1 });
271
+ pending = null;
272
+ closeIndent = -1;
273
+ continue;
274
+ }
275
+ if (/^\},\s*$/.test(bare)) {
276
+ // Wrapped form: the timeout is the next line, at the same indent. Any
277
+ // other follower means this `},` closes the block with no cap at all.
278
+ const next = lines[index + 1];
279
+ const timeout = next?.match(/^\s*([0-9_]+)\s*,\s*$/);
280
+ if (timeout) {
281
+ entries.push({
282
+ name: pending,
283
+ capMs: Number(timeout[1].replace(/_/g, '')),
284
+ line: index + 2,
285
+ });
286
+ }
287
+ pending = null;
288
+ closeIndent = -1;
289
+ }
290
+ }
291
+ return entries;
292
+ }
293
+
294
+ export interface TightBlock {
295
+ block: string;
296
+ ms: number;
297
+ capMs: number;
298
+ fraction: number;
299
+ }
300
+
301
+ /**
302
+ * Blocks that finished inside their budget but had little of it left.
303
+ *
304
+ * This is the signal the census could not see. A block that BLOWS its cap says
305
+ * "this test timed out after 300000ms" and reads as a slow host; a block that
306
+ * lands at 95% says nothing at all, and is one ordinary night's load away from
307
+ * the first case. Median inflation on the grind host is 1.14x and p90 is 1.58x,
308
+ * so 80% of a cap is roughly a coin flip at p90.
309
+ *
310
+ * Blocks with no measurement, and blocks with no declared cap, are absent from
311
+ * the result rather than assumed healthy.
312
+ */
313
+ export function tightBlocks(
314
+ measured: Record<string, number>,
315
+ caps: Record<string, number>,
316
+ fraction = 0.8,
317
+ ): TightBlock[] {
318
+ const out: TightBlock[] = [];
319
+ for (const [block, ms] of Object.entries(measured)) {
320
+ const capMs = caps[block];
321
+ if (!capMs) continue;
322
+ if (ms < capMs * fraction) continue;
323
+ out.push({ block, ms, capMs, fraction: ms / capMs });
324
+ }
325
+ return out.sort((a, b) => b.fraction - a.fraction);
326
+ }
327
+
328
+ export interface OverBudgetBlock {
329
+ block: string;
330
+ ms: number;
331
+ capMs: number;
332
+ fraction: number;
333
+ }
334
+
335
+ /**
336
+ * Blocks whose measured duration EXCEEDS the cap they declare, and the run must
337
+ * fail on them.
338
+ *
339
+ * This reads the JUnit durations directly and deliberately bypasses
340
+ * `parseBlockDurations`, whose skip filter exists to keep stages that never ran
341
+ * out of the RECORD. A budget check has the opposite obligation: every block
342
+ * that ran must be measured against its declaration, and the census proved the
343
+ * cost of routing it through the record first. alerting stage 1 ran 1021397ms
344
+ * against its own 300000ms declaration (celilo#1291) and every downstream
345
+ * consumer saw nothing, because the skip filter misattributed a sibling's
346
+ * message and dropped the block before anything could compare it to a cap.
347
+ *
348
+ * bun's per-test timeout is NOT the enforcer of this number. It is an
349
+ * EventLoopTimer that can fail to fire while other timers in the same process
350
+ * fire on schedule (the census: the deploy's own 180s budget fired at exactly
351
+ * 180s into a block whose 300s bun timer never fired; oven-sh/bun#32056
352
+ * family). The declaration binds HERE, after the fact: the block still holds
353
+ * the rig while it overruns, but a run that let it happen does not pass.
354
+ */
355
+ export function overBudgetBlocks(junitXml: string, source: string): OverBudgetBlock[] {
356
+ const measured = parseJunitDurations(junitXml);
357
+ const caps = declaredBlockCaps(source);
358
+ const out: OverBudgetBlock[] = [];
359
+ for (const [block, ms] of Object.entries(measured)) {
360
+ const capMs = caps[block];
361
+ if (!capMs) continue;
362
+ if (ms <= capMs) continue;
363
+ out.push({ block, ms, capMs, fraction: ms / capMs });
364
+ }
365
+ return out.sort((a, b) => b.fraction - a.fraction);
366
+ }
@@ -1,8 +1,14 @@
1
1
  import { afterEach, beforeEach, expect, test } from 'bun:test';
2
- import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { stageNetappsFromRegistry } from './build';
5
+ import { gzipSync } from 'node:zlib';
6
+ import {
7
+ assertGzipValid,
8
+ stageNetappsFromRegistry,
9
+ verifyNetapp,
10
+ verifyStagedNetapps,
11
+ } from './build';
6
12
 
7
13
  let dir: string;
8
14
  const realFetch = globalThis.fetch;
@@ -20,6 +26,8 @@ afterEach(() => {
20
26
 
21
27
  test('fetches each module latest version to <name>.netapp via the download endpoint', async () => {
22
28
  const seen: string[] = [];
29
+ // Valid gzip payload — the staging step verifies every downloaded .netapp.
30
+ const netappBytes = gzipSync(Buffer.from('tar-payload'));
23
31
  // @ts-expect-error — minimal fetch stub for the two shapes we call.
24
32
  globalThis.fetch = async (url: string) => {
25
33
  seen.push(url);
@@ -33,15 +41,57 @@ test('fetches each module latest version to <name>.netapp via the download endpo
33
41
  }),
34
42
  );
35
43
  }
36
- return new Response(new Uint8Array([1, 2, 3]));
44
+ return new Response(new Uint8Array(netappBytes));
37
45
  };
38
46
 
39
47
  await stageNetappsFromRegistry(dir);
40
48
 
41
49
  expect(readdirSync(dir).sort()).toEqual(['caddy.netapp', 'namecheap.netapp']);
42
50
  expect(existsSync(join(dir, 'caddy.netapp'))).toBe(true);
43
- expect([...readFileSync(join(dir, 'caddy.netapp'))]).toEqual([1, 2, 3]);
51
+ expect([...readFileSync(join(dir, 'caddy.netapp'))]).toEqual([...netappBytes]);
44
52
  // Trailing slash stripped; download hits the module's max_version.
45
53
  expect(seen).toContain('https://example.test/registry/api/v1/modules/caddy/1.2.3/download');
46
54
  expect(seen).toContain('https://example.test/registry/api/v1/modules/namecheap/0.4.0/download');
47
55
  });
56
+
57
+ test('refuses a downloaded .netapp that is not a complete gzip stream (celilo#1257)', async () => {
58
+ const realExit = process.exit;
59
+ const exitCalls: unknown[] = [];
60
+ // Stub exit so the test survives the loud failure the corrupt download gets.
61
+ (process as unknown as { exit: (code?: number) => never }).exit = ((code?: number) => {
62
+ exitCalls.push(code);
63
+ throw new Error(`process.exit(${code})`);
64
+ }) as typeof process.exit;
65
+ // @ts-expect-error — minimal fetch stub for the two shapes we call.
66
+ globalThis.fetch = async (url: string) => {
67
+ if (url.includes('/api/v1/modules?')) {
68
+ return new Response(
69
+ JSON.stringify({ modules: [{ name: 'technitium', max_version: '9.9.9' }] }),
70
+ );
71
+ }
72
+ // A payload truncated mid-stream, the shape a wedged packaging write leaves.
73
+ return new Response(new Uint8Array([0x1f, 0x8b, 0x08, 0x00, 0x99]));
74
+ };
75
+
76
+ try {
77
+ await expect(stageNetappsFromRegistry(dir)).rejects.toThrow('process.exit(1)');
78
+ } finally {
79
+ process.exit = realExit;
80
+ }
81
+ expect(exitCalls).toEqual([1]);
82
+ });
83
+
84
+ test('verifyStagedNetapps accepts a valid .netapp and refuses a truncated one', () => {
85
+ const valid = gzipSync(Buffer.from('x'.repeat(2048)));
86
+ writeFileSync(join(dir, 'good.netapp'), valid);
87
+ verifyStagedNetapps(dir);
88
+ verifyNetapp(join(dir, 'good.netapp'));
89
+ assertGzipValid(join(dir, 'good.netapp'));
90
+
91
+ // Plant a truncated .netapp: valid gzip bytes cut at 60 percent, missing the
92
+ // trailer. The recurrence gate for celilo#1257.
93
+ writeFileSync(join(dir, 'technitium.netapp'), valid.subarray(0, Math.floor(valid.length * 0.6)));
94
+ expect(() => verifyStagedNetapps(dir)).toThrow(/technitium\.netapp.*gzip/s);
95
+ expect(() => verifyNetapp(join(dir, 'technitium.netapp'))).toThrow(/technitium\.netapp/s);
96
+ expect(() => assertGzipValid(join(dir, 'technitium.netapp'))).toThrow(/unexpected end of file/s);
97
+ });