Skip to content

Commit 19f624b

Browse files
committed
refactor: share best-effort daemon test cleanup
1 parent dca1dc7 commit 19f624b

4 files changed

Lines changed: 47 additions & 35 deletions

File tree

‎test/integration/daemon-replace-exit-flush.test.ts‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cleanupDaemonTestState } from './support/daemon-test-cleanup.ts';
12
import test from 'node:test';
23
import assert from 'node:assert/strict';
34
import fs from 'node:fs';
@@ -84,20 +85,8 @@ test('daemon replace mid-command returns a structured, parseable error and exits
8485
// `finally` retry below rather than have
8586
// the state dir removed out from under a daemon that is still running.
8687
await assertNoDaemonLeaks({ stateDir, daemonPids, phase: 'after-shutdown' });
87-
info = null;
8888
} finally {
89-
try {
90-
if (info) {
91-
const termination = await stopDaemonProcess(
92-
{ pid: info.pid, startTime: info.processStartTime ?? null },
93-
{ mode: 'graceful', termTimeoutMs: 1_500, killTimeoutMs: 1_500 },
94-
);
95-
assert.notEqual(termination.status, 'retained', JSON.stringify(termination));
96-
}
97-
fs.rmSync(stateDir, { recursive: true, force: true });
98-
} catch (error) {
99-
console.warn('Daemon test cleanup retained state:', stateDir, error);
100-
}
89+
await cleanupDaemonTestState(stateDir, info);
10190
}
10291
});
10392

‎test/integration/smoke-daemon-clean.test.ts‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cleanupDaemonTestState } from './support/daemon-test-cleanup.ts';
12
import test from 'node:test';
23
import assert from 'node:assert/strict';
34
import fs from 'node:fs';
@@ -6,7 +7,6 @@ import path from 'node:path';
67
import { skipWhenLoopbackUnavailable } from '../../src/__tests__/test-utils/loopback.ts';
78
import { runCmdSync } from '@agent-device/host-kit/command';
89
import { isProcessAlive, readProcessStartTime } from '@agent-device/host-kit/process';
9-
import { stopDaemonProcess } from '../../src/daemon-process.ts';
1010

1111
import { assertNoDaemonLeaks } from './support/daemon-leak-oracle.ts';
1212
import { runCliJson } from './test-helpers.ts';
@@ -77,18 +77,7 @@ test('clean daemon script stops a live daemon before removing metadata', async (
7777
// leave only classified artifacts in its state dir.
7878
await assertNoDaemonLeaks({ stateDir, daemonPids: [info.pid], phase: 'after-shutdown' });
7979
} finally {
80-
try {
81-
if (info) {
82-
const termination = await stopDaemonProcess(
83-
{ pid: info.pid, startTime: info.processStartTime ?? null },
84-
{ mode: 'graceful', termTimeoutMs: 1_500, killTimeoutMs: 1_500 },
85-
);
86-
assert.notEqual(termination.status, 'retained', JSON.stringify(termination));
87-
}
88-
fs.rmSync(stateDir, { recursive: true, force: true });
89-
} catch (error) {
90-
console.warn('Daemon test cleanup retained state:', stateDir, error);
91-
}
80+
await cleanupDaemonTestState(stateDir, info);
9281
}
9382
});
9483

‎test/integration/smoke-daemon-http.test.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cleanupDaemonTestState } from './support/daemon-test-cleanup.ts';
12
import test from 'node:test';
23
import assert from 'node:assert/strict';
34
import fs from 'node:fs';
@@ -24,6 +25,7 @@ test('daemon HTTP transport starts from CLI and accepts a command RPC', async (t
2425
}
2526

2627
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-http-smoke-'));
28+
let info: DaemonInfo | null = null;
2729
try {
2830
const args = [
2931
'session',
@@ -46,7 +48,7 @@ test('daemon HTTP transport starts from CLI and accepts a command RPC', async (t
4648
assert.equal(cli.status, 0, formatResultDebug('start HTTP daemon', ['session', 'list'], cli));
4749
assert.equal(cli.json?.success, true, JSON.stringify(cli.json));
4850

49-
const info = readDaemonInfo(stateDir);
51+
info = readDaemonInfo(stateDir);
5052
assert.equal(info.transport, 'http');
5153
assert.equal(typeof info.httpPort, 'number');
5254
assert.ok((info.httpPort ?? 0) > 0);
@@ -73,14 +75,7 @@ test('daemon HTTP transport starts from CLI and accepts a command RPC', async (t
7375
await stopDaemon(info);
7476
await assertNoDaemonLeaks({ stateDir, daemonPids: [info.pid], phase: 'after-shutdown' });
7577
} finally {
76-
try {
77-
if (fs.existsSync(path.join(stateDir, 'daemon.json'))) {
78-
await stopDaemon(readDaemonInfo(stateDir));
79-
}
80-
fs.rmSync(stateDir, { recursive: true, force: true });
81-
} catch (error) {
82-
console.warn('Daemon test cleanup retained state:', stateDir, error);
83-
}
78+
await cleanupDaemonTestState(stateDir, info);
8479
}
8580
});
8681

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { normalizeError } from '@agent-device/kernel/errors';
4+
import { stopDaemonProcess } from '../../../src/daemon-process.ts';
5+
6+
type TestDaemonIdentity = { pid: number; processStartTime?: string };
7+
8+
/** Best-effort cleanup keeps primary test failures and unconfirmed daemon state. */
9+
export async function cleanupDaemonTestState(
10+
stateDir: string,
11+
observed: TestDaemonIdentity | null,
12+
): Promise<void> {
13+
try {
14+
const identity = observed ?? readIdentity(stateDir);
15+
if (!identity) throw new Error('No daemon lifetime was observed');
16+
const termination = await stopDaemonProcess(
17+
{ pid: identity.pid, startTime: identity.processStartTime ?? null },
18+
{ mode: 'graceful', termTimeoutMs: 1_500, killTimeoutMs: 1_500 },
19+
);
20+
if (termination.status !== 'exited') {
21+
console.warn('Daemon test cleanup retained state:', stateDir, termination);
22+
return;
23+
}
24+
fs.rmSync(stateDir, { recursive: true, force: true });
25+
} catch (error) {
26+
console.warn('Daemon test cleanup retained state:', stateDir, normalizeError(error));
27+
}
28+
}
29+
30+
function readIdentity(stateDir: string): TestDaemonIdentity | null {
31+
try {
32+
return JSON.parse(
33+
fs.readFileSync(path.join(stateDir, 'daemon.json'), 'utf8'),
34+
) as TestDaemonIdentity;
35+
} catch (error) {
36+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
37+
throw error;
38+
}
39+
}

0 commit comments

Comments
 (0)