@plosson/agentio 0.7.2 → 0.7.4

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,637 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import type { Subprocess } from 'bun';
6
+
7
+ /**
8
+ * Integration tests for the agentio HTTP server daemon.
9
+ *
10
+ * Each test:
11
+ * 1. mkdtemps an isolated HOME so the spawned daemon never touches the
12
+ * developer's real ~/.config/agentio.
13
+ * 2. Picks a free port from the OS via Bun.serve({port: 0}).
14
+ * 3. Spawns `bun run src/index.ts server start --foreground` in a
15
+ * subprocess with that HOME, parses stdout for the API key + ready
16
+ * line, then exercises the daemon over a real socket.
17
+ * 4. Sends SIGTERM in afterEach and removes the temp HOME.
18
+ *
19
+ * The goal is to nail down behavior I asserted but didn't actually
20
+ * verify in Phase 2: the source-priority chain for port/host/api-key,
21
+ * persistence semantics, and adversarial start conditions.
22
+ */
23
+
24
+ interface RunningServer {
25
+ proc: Subprocess<'ignore', 'pipe', 'pipe'>;
26
+ apiKey: string;
27
+ /** Full stdout buffer captured up to the "Server ready" line. */
28
+ startupLog: string;
29
+ }
30
+
31
+ let tempHome = '';
32
+ let active: RunningServer | null = null;
33
+
34
+ beforeEach(async () => {
35
+ tempHome = await mkdtemp(join(tmpdir(), 'agentio-server-test-'));
36
+ });
37
+
38
+ afterEach(async () => {
39
+ if (active) {
40
+ try {
41
+ await shutdown(active.proc, 'SIGTERM');
42
+ } catch {
43
+ try {
44
+ active.proc.kill('SIGKILL');
45
+ await active.proc.exited;
46
+ } catch {
47
+ /* ignore */
48
+ }
49
+ }
50
+ active = null;
51
+ }
52
+ if (tempHome) {
53
+ await rm(tempHome, { recursive: true, force: true }).catch(() => {});
54
+ tempHome = '';
55
+ }
56
+ });
57
+
58
+ /* ------------------------------------------------------------------ */
59
+ /* helpers */
60
+ /* ------------------------------------------------------------------ */
61
+
62
+ async function findFreePort(): Promise<number> {
63
+ const probe = Bun.serve({ port: 0, fetch: () => new Response('') });
64
+ const port = probe.port;
65
+ probe.stop(true);
66
+ if (typeof port !== 'number') {
67
+ throw new Error('Bun.serve did not return a numeric port');
68
+ }
69
+ return port;
70
+ }
71
+
72
+ interface StartOpts {
73
+ port?: number;
74
+ extraArgs?: string[];
75
+ extraEnv?: Record<string, string>;
76
+ expectFailure?: boolean;
77
+ /** Override HOME for this run (defaults to the per-test tempHome). */
78
+ home?: string;
79
+ }
80
+
81
+ async function startServer(opts: StartOpts = {}): Promise<RunningServer> {
82
+ const port = opts.port ?? (await findFreePort());
83
+ const home = opts.home ?? tempHome;
84
+
85
+ // Strip variables from the parent env that would otherwise pollute the
86
+ // child's behavior. We want a hermetic run.
87
+ const env: Record<string, string> = {
88
+ ...process.env,
89
+ HOME: home,
90
+ AGENTIO_SERVER_PORT: '',
91
+ AGENTIO_SERVER_HOST: '',
92
+ AGENTIO_SERVER_API_KEY: '',
93
+ ...(opts.extraEnv ?? {}),
94
+ };
95
+ // Bun's spawn treats empty-string env vars as set; delete the ones we
96
+ // explicitly want unset (unless extraEnv re-set them).
97
+ for (const k of [
98
+ 'AGENTIO_SERVER_PORT',
99
+ 'AGENTIO_SERVER_HOST',
100
+ 'AGENTIO_SERVER_API_KEY',
101
+ ]) {
102
+ if (!opts.extraEnv?.[k]) delete env[k];
103
+ }
104
+
105
+ const proc = Bun.spawn(
106
+ [
107
+ 'bun',
108
+ 'run',
109
+ 'src/index.ts',
110
+ 'server',
111
+ 'start',
112
+ '--foreground',
113
+ '--port',
114
+ String(port),
115
+ ...(opts.extraArgs ?? []),
116
+ ],
117
+ { stdout: 'pipe', stderr: 'pipe', env }
118
+ );
119
+
120
+ if (opts.expectFailure) {
121
+ return {
122
+ proc: proc as Subprocess<'ignore', 'pipe', 'pipe'>,
123
+ apiKey: '',
124
+ startupLog: '',
125
+ };
126
+ }
127
+
128
+ // Race: read stdout until "Server ready", or proc exits, or 10s timeout.
129
+ const decoder = new TextDecoder();
130
+ let buffer = '';
131
+ const reader = proc.stdout.getReader();
132
+ const deadline = Date.now() + 10_000;
133
+
134
+ try {
135
+ while (!buffer.includes('Server ready')) {
136
+ if (Date.now() > deadline) {
137
+ proc.kill('SIGKILL');
138
+ throw new Error(`startup timeout. stdout so far:\n${buffer}`);
139
+ }
140
+ const readPromise = reader.read();
141
+ const timeoutMs = Math.max(100, deadline - Date.now());
142
+ const { done, value } = await Promise.race([
143
+ readPromise,
144
+ new Promise<{ done: true; value: undefined }>((resolve) =>
145
+ setTimeout(() => resolve({ done: true, value: undefined }), timeoutMs)
146
+ ),
147
+ ]);
148
+ if (done) {
149
+ const stderrText = await new Response(proc.stderr).text();
150
+ throw new Error(
151
+ `child exited or timed out before ready. stdout:\n${buffer}\nstderr:\n${stderrText}`
152
+ );
153
+ }
154
+ buffer += decoder.decode(value);
155
+ }
156
+ } finally {
157
+ reader.releaseLock();
158
+ }
159
+
160
+ const apiKeyMatch = buffer.match(/API Key: (\S+)/);
161
+ const apiKey = apiKeyMatch ? apiKeyMatch[1] : '';
162
+
163
+ const running: RunningServer = {
164
+ proc: proc as Subprocess<'ignore', 'pipe', 'pipe'>,
165
+ apiKey,
166
+ startupLog: buffer,
167
+ };
168
+ active = running;
169
+ return running;
170
+ }
171
+
172
+ async function shutdown(
173
+ proc: Subprocess<'ignore', 'pipe', 'pipe'>,
174
+ signal: 'SIGTERM' | 'SIGINT' = 'SIGTERM'
175
+ ): Promise<number> {
176
+ proc.kill(signal);
177
+ const result = await Promise.race([
178
+ proc.exited.then((code) => ({ ok: true, code })),
179
+ new Promise<{ ok: false }>((resolve) =>
180
+ setTimeout(() => resolve({ ok: false }), 5000)
181
+ ),
182
+ ]);
183
+ if (!result.ok) {
184
+ proc.kill('SIGKILL');
185
+ await proc.exited;
186
+ throw new Error(`process did not exit on ${signal} within 5s`);
187
+ }
188
+ return result.code;
189
+ }
190
+
191
+ async function readPersistedConfig(home: string): Promise<{
192
+ raw: string;
193
+ parsed: { server?: { apiKey?: string; port?: number; host?: string } };
194
+ }> {
195
+ const path = join(home, '.config', 'agentio', 'config.json');
196
+ const raw = await readFile(path, 'utf8');
197
+ return { raw, parsed: JSON.parse(raw) };
198
+ }
199
+
200
+ /* ------------------------------------------------------------------ */
201
+ /* tests */
202
+ /* ------------------------------------------------------------------ */
203
+
204
+ describe('first run — API key generation + persistence', () => {
205
+ test('auto-generates an API key with srv_ prefix and ~33 base64url chars', async () => {
206
+ const { apiKey, startupLog } = await startServer();
207
+ expect(apiKey).toMatch(/^srv_[A-Za-z0-9_-]+$/);
208
+ // 24 random bytes encoded as base64url = 32 chars (no padding)
209
+ expect(apiKey.length).toBe(4 + 32);
210
+ expect(startupLog).toContain('Generated new API key');
211
+ expect(startupLog).toContain(`API Key: ${apiKey}`);
212
+ });
213
+
214
+ test('persists the generated key to config.server.apiKey', async () => {
215
+ const { apiKey } = await startServer();
216
+ const { parsed } = await readPersistedConfig(tempHome);
217
+ expect(parsed.server?.apiKey).toBe(apiKey);
218
+ });
219
+
220
+ test('config file is created with mode 0600', async () => {
221
+ await startServer();
222
+ const path = join(tempHome, '.config', 'agentio', 'config.json');
223
+ const st = await stat(path);
224
+ // Mask out non-permission bits.
225
+ const mode = st.mode & 0o777;
226
+ expect(mode).toBe(0o600);
227
+ });
228
+
229
+ test('config dir is auto-created when it did not exist', async () => {
230
+ // tempHome was just mkdtemped — it definitely has no .config/agentio yet.
231
+ await startServer();
232
+ const dirStat = await stat(join(tempHome, '.config', 'agentio'));
233
+ expect(dirStat.isDirectory()).toBe(true);
234
+ });
235
+
236
+ test('Listening line shows the actual bound host:port', async () => {
237
+ const port = await findFreePort();
238
+ const { startupLog } = await startServer({ port });
239
+ expect(startupLog).toContain(`Listening on http://0.0.0.0:${port}`);
240
+ });
241
+ });
242
+
243
+ describe('second run — persistence across restarts', () => {
244
+ test('reuses the persisted API key on a fresh process', async () => {
245
+ const first = await startServer();
246
+ await shutdown(first.proc);
247
+ active = null;
248
+
249
+ const second = await startServer();
250
+ expect(second.apiKey).toBe(first.apiKey);
251
+ expect(second.startupLog).not.toContain('Generated new API key');
252
+ });
253
+
254
+ test('deleting apiKey from config and restarting generates a fresh one', async () => {
255
+ const first = await startServer();
256
+ await shutdown(first.proc);
257
+ active = null;
258
+
259
+ // Wipe just the apiKey field, keep the rest of the config.
260
+ const cfgPath = join(tempHome, '.config', 'agentio', 'config.json');
261
+ const raw = await readFile(cfgPath, 'utf8');
262
+ const cfg = JSON.parse(raw);
263
+ delete cfg.server.apiKey;
264
+ await writeFile(cfgPath, JSON.stringify(cfg, null, 2));
265
+
266
+ const second = await startServer();
267
+ expect(second.apiKey).not.toBe(first.apiKey);
268
+ expect(second.apiKey).toMatch(/^srv_/);
269
+ expect(second.startupLog).toContain('Generated new API key');
270
+ });
271
+
272
+ test('corrupted config.json does not crash; daemon resets and generates new key', async () => {
273
+ // Pre-create a config dir with garbage.
274
+ await mkdir(join(tempHome, '.config', 'agentio'), {
275
+ recursive: true,
276
+ mode: 0o700,
277
+ });
278
+ await writeFile(
279
+ join(tempHome, '.config', 'agentio', 'config.json'),
280
+ '{ this is not valid json',
281
+ { mode: 0o600 }
282
+ );
283
+
284
+ const { apiKey, startupLog } = await startServer();
285
+ expect(apiKey).toMatch(/^srv_/);
286
+ expect(startupLog).toContain('Generated new API key');
287
+
288
+ // The corrupted file should have been backed up.
289
+ const backupStat = await stat(
290
+ join(tempHome, '.config', 'agentio', 'config.json.backup')
291
+ );
292
+ expect(backupStat.isFile()).toBe(true);
293
+ });
294
+ });
295
+
296
+ describe('source priority — CLI > env > config > default', () => {
297
+ test('--api-key overrides stored key without persisting', async () => {
298
+ // Seed: generate a stored key.
299
+ const first = await startServer();
300
+ const stored = first.apiKey;
301
+ await shutdown(first.proc);
302
+ active = null;
303
+
304
+ // Run again with --api-key override.
305
+ const second = await startServer({
306
+ extraArgs: ['--api-key', 'srv_cli_override_value'],
307
+ });
308
+ expect(second.apiKey).toBe('srv_cli_override_value');
309
+
310
+ // The persisted key MUST not have been overwritten.
311
+ const { parsed } = await readPersistedConfig(tempHome);
312
+ expect(parsed.server?.apiKey).toBe(stored);
313
+ });
314
+
315
+ test('AGENTIO_SERVER_API_KEY env overrides stored key without persisting', async () => {
316
+ const first = await startServer();
317
+ const stored = first.apiKey;
318
+ await shutdown(first.proc);
319
+ active = null;
320
+
321
+ const second = await startServer({
322
+ extraEnv: { AGENTIO_SERVER_API_KEY: 'srv_env_override_value' },
323
+ });
324
+ expect(second.apiKey).toBe('srv_env_override_value');
325
+
326
+ const { parsed } = await readPersistedConfig(tempHome);
327
+ expect(parsed.server?.apiKey).toBe(stored);
328
+ });
329
+
330
+ test('--api-key beats AGENTIO_SERVER_API_KEY env (CLI wins)', async () => {
331
+ const { apiKey } = await startServer({
332
+ extraArgs: ['--api-key', 'cli_wins'],
333
+ extraEnv: { AGENTIO_SERVER_API_KEY: 'env_loses' },
334
+ });
335
+ expect(apiKey).toBe('cli_wins');
336
+ });
337
+
338
+ test('--port flag wins over default', async () => {
339
+ const port = await findFreePort();
340
+ const { startupLog } = await startServer({ port });
341
+ expect(startupLog).toContain(`Listening on http://0.0.0.0:${port}`);
342
+ expect(startupLog).not.toContain('Listening on http://0.0.0.0:9999');
343
+ });
344
+
345
+ test('--host 127.0.0.1 binds to loopback only', async () => {
346
+ const port = await findFreePort();
347
+ const { startupLog } = await startServer({
348
+ port,
349
+ extraArgs: ['--host', '127.0.0.1'],
350
+ });
351
+ expect(startupLog).toContain(`Listening on http://127.0.0.1:${port}`);
352
+ // Verify it's actually reachable on loopback.
353
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
354
+ expect(res.status).toBe(200);
355
+ });
356
+ });
357
+
358
+ describe('HTTP behavior over a real socket', () => {
359
+ test('GET /health returns 200 {ok:true}', async () => {
360
+ const port = await findFreePort();
361
+ await startServer({ port });
362
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
363
+ expect(res.status).toBe(200);
364
+ expect(res.headers.get('content-type')).toBe('application/json');
365
+ expect(await res.json()).toEqual({ ok: true });
366
+ });
367
+
368
+ test('GET /.well-known/oauth-protected-resource returns RFC 9728 metadata', async () => {
369
+ const port = await findFreePort();
370
+ await startServer({ port });
371
+ const res = await fetch(
372
+ `http://127.0.0.1:${port}/.well-known/oauth-protected-resource`
373
+ );
374
+ expect(res.status).toBe(200);
375
+ const body = (await res.json()) as Record<string, unknown>;
376
+ expect(body.resource).toBe(`http://127.0.0.1:${port}/mcp`);
377
+ expect(body.authorization_servers).toEqual([
378
+ `http://127.0.0.1:${port}`,
379
+ ]);
380
+ expect(body.bearer_methods_supported).toEqual(['header']);
381
+ });
382
+
383
+ test('GET /.well-known/oauth-authorization-server returns RFC 8414 metadata', async () => {
384
+ const port = await findFreePort();
385
+ await startServer({ port });
386
+ const res = await fetch(
387
+ `http://127.0.0.1:${port}/.well-known/oauth-authorization-server`
388
+ );
389
+ expect(res.status).toBe(200);
390
+ const body = (await res.json()) as Record<string, unknown>;
391
+ expect(body.issuer).toBe(`http://127.0.0.1:${port}`);
392
+ expect(body.authorization_endpoint).toBe(
393
+ `http://127.0.0.1:${port}/authorize`
394
+ );
395
+ expect(body.token_endpoint).toBe(`http://127.0.0.1:${port}/token`);
396
+ expect(body.registration_endpoint).toBe(
397
+ `http://127.0.0.1:${port}/register`
398
+ );
399
+ expect(body.code_challenge_methods_supported).toEqual(['S256']);
400
+ expect(body.grant_types_supported).toEqual(['authorization_code']);
401
+ });
402
+
403
+ test('POST to a metadata endpoint returns 404 (GET-only routing)', async () => {
404
+ const port = await findFreePort();
405
+ await startServer({ port });
406
+ const res = await fetch(
407
+ `http://127.0.0.1:${port}/.well-known/oauth-protected-resource`,
408
+ { method: 'POST' }
409
+ );
410
+ expect(res.status).toBe(404);
411
+ });
412
+
413
+ test('GET /nonsense returns 404 {error:"not found"}', async () => {
414
+ const port = await findFreePort();
415
+ await startServer({ port });
416
+ const res = await fetch(`http://127.0.0.1:${port}/nonsense`);
417
+ expect(res.status).toBe(404);
418
+ expect(await res.json()).toEqual({ error: 'not found' });
419
+ });
420
+
421
+ test('50 concurrent /health requests all return 200', async () => {
422
+ const port = await findFreePort();
423
+ await startServer({ port });
424
+ const results = await Promise.all(
425
+ Array.from({ length: 50 }, () =>
426
+ fetch(`http://127.0.0.1:${port}/health`)
427
+ )
428
+ );
429
+ expect(results.every((r) => r.status === 200)).toBe(true);
430
+ // Drain bodies so the test doesn't leak handles.
431
+ await Promise.all(results.map((r) => r.text()));
432
+ });
433
+
434
+ test('mixed concurrent /health and 404 requests resolve correctly', async () => {
435
+ const port = await findFreePort();
436
+ await startServer({ port });
437
+ const results = await Promise.all(
438
+ Array.from({ length: 40 }, (_, i) =>
439
+ fetch(`http://127.0.0.1:${port}${i % 2 === 0 ? '/health' : '/nope'}`)
440
+ )
441
+ );
442
+ const oks = results.filter((r) => r.status === 200).length;
443
+ const fourofours = results.filter((r) => r.status === 404).length;
444
+ expect(oks).toBe(20);
445
+ expect(fourofours).toBe(20);
446
+ await Promise.all(results.map((r) => r.text()));
447
+ });
448
+
449
+ test('POST /health (any method) returns 200', async () => {
450
+ const port = await findFreePort();
451
+ await startServer({ port });
452
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
453
+ method: 'POST',
454
+ body: 'ignored',
455
+ });
456
+ expect(res.status).toBe(200);
457
+ });
458
+ });
459
+
460
+ describe('lifecycle — signals + restart', () => {
461
+ test('SIGTERM exits with code 0', async () => {
462
+ const { proc } = await startServer();
463
+ const code = await shutdown(proc, 'SIGTERM');
464
+ expect(code).toBe(0);
465
+ active = null;
466
+ });
467
+
468
+ test('SIGINT exits with code 0', async () => {
469
+ const { proc } = await startServer();
470
+ const code = await shutdown(proc, 'SIGINT');
471
+ expect(code).toBe(0);
472
+ active = null;
473
+ });
474
+
475
+ test('repeated SIGTERM is idempotent (shutdownRequested guard)', async () => {
476
+ const { proc } = await startServer();
477
+ proc.kill('SIGTERM');
478
+ proc.kill('SIGTERM'); // Second one should be a no-op, not a crash.
479
+ const code = await Promise.race([
480
+ proc.exited,
481
+ new Promise<number>((_, reject) =>
482
+ setTimeout(() => reject(new Error('did not exit')), 5000)
483
+ ),
484
+ ]);
485
+ expect(code).toBe(0);
486
+ active = null;
487
+ });
488
+ });
489
+
490
+ describe('adversarial start conditions', () => {
491
+ test('binding to an already-occupied port fails non-zero', async () => {
492
+ const port = await findFreePort();
493
+ // Squat on the port with a local Bun.serve so the spawned daemon
494
+ // can't grab it.
495
+ const squatter = Bun.serve({ port, fetch: () => new Response('squat') });
496
+ try {
497
+ const proc = Bun.spawn(
498
+ [
499
+ 'bun',
500
+ 'run',
501
+ 'src/index.ts',
502
+ 'server',
503
+ 'start',
504
+ '--foreground',
505
+ '--port',
506
+ String(port),
507
+ ],
508
+ {
509
+ stdout: 'pipe',
510
+ stderr: 'pipe',
511
+ env: { ...process.env, HOME: tempHome },
512
+ }
513
+ );
514
+
515
+ // Wait up to 5s for it to fail.
516
+ const result = await Promise.race([
517
+ proc.exited.then((code) => ({ exited: true, code })),
518
+ new Promise<{ exited: false }>((resolve) =>
519
+ setTimeout(() => resolve({ exited: false }), 5000)
520
+ ),
521
+ ]);
522
+
523
+ if (!result.exited) {
524
+ proc.kill('SIGKILL');
525
+ await proc.exited;
526
+ throw new Error('daemon stayed up despite occupied port');
527
+ }
528
+ expect(result.code).not.toBe(0);
529
+ } finally {
530
+ squatter.stop(true);
531
+ }
532
+ });
533
+
534
+ test('--port with non-numeric value crashes early (does not silently use default)', async () => {
535
+ const proc = Bun.spawn(
536
+ [
537
+ 'bun',
538
+ 'run',
539
+ 'src/index.ts',
540
+ 'server',
541
+ 'start',
542
+ '--foreground',
543
+ '--port',
544
+ 'not-a-number',
545
+ ],
546
+ {
547
+ stdout: 'pipe',
548
+ stderr: 'pipe',
549
+ env: { ...process.env, HOME: tempHome },
550
+ }
551
+ );
552
+
553
+ const result = await Promise.race([
554
+ proc.exited.then((code) => ({ exited: true, code })),
555
+ new Promise<{ exited: false }>((resolve) =>
556
+ setTimeout(() => resolve({ exited: false }), 5000)
557
+ ),
558
+ ]);
559
+
560
+ if (!result.exited) {
561
+ proc.kill('SIGKILL');
562
+ await proc.exited;
563
+ throw new Error('daemon stayed up despite NaN port');
564
+ }
565
+ // Either it crashed (non-zero) or somehow ignored the value. Lock in
566
+ // crash behavior — silently substituting the default would be a footgun.
567
+ expect(result.code).not.toBe(0);
568
+ });
569
+
570
+ test('--port with negative value crashes early', async () => {
571
+ const proc = Bun.spawn(
572
+ [
573
+ 'bun',
574
+ 'run',
575
+ 'src/index.ts',
576
+ 'server',
577
+ 'start',
578
+ '--foreground',
579
+ '--port',
580
+ '-1',
581
+ ],
582
+ {
583
+ stdout: 'pipe',
584
+ stderr: 'pipe',
585
+ env: { ...process.env, HOME: tempHome },
586
+ }
587
+ );
588
+
589
+ const result = await Promise.race([
590
+ proc.exited.then((code) => ({ exited: true, code })),
591
+ new Promise<{ exited: false }>((resolve) =>
592
+ setTimeout(() => resolve({ exited: false }), 5000)
593
+ ),
594
+ ]);
595
+
596
+ if (!result.exited) {
597
+ proc.kill('SIGKILL');
598
+ await proc.exited;
599
+ throw new Error('daemon stayed up despite negative port');
600
+ }
601
+ expect(result.code).not.toBe(0);
602
+ });
603
+
604
+ test('--port 99999 (out of range) crashes early', async () => {
605
+ const proc = Bun.spawn(
606
+ [
607
+ 'bun',
608
+ 'run',
609
+ 'src/index.ts',
610
+ 'server',
611
+ 'start',
612
+ '--foreground',
613
+ '--port',
614
+ '99999',
615
+ ],
616
+ {
617
+ stdout: 'pipe',
618
+ stderr: 'pipe',
619
+ env: { ...process.env, HOME: tempHome },
620
+ }
621
+ );
622
+
623
+ const result = await Promise.race([
624
+ proc.exited.then((code) => ({ exited: true, code })),
625
+ new Promise<{ exited: false }>((resolve) =>
626
+ setTimeout(() => resolve({ exited: false }), 5000)
627
+ ),
628
+ ]);
629
+
630
+ if (!result.exited) {
631
+ proc.kill('SIGKILL');
632
+ await proc.exited;
633
+ throw new Error('daemon stayed up despite out-of-range port');
634
+ }
635
+ expect(result.code).not.toBe(0);
636
+ });
637
+ });