Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 27 additions & 19 deletions src/daemon-client/__tests__/daemon-client-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { resolveDaemonPaths, type DaemonPaths } from '../../daemon-resolution.ts
import { sendToDaemon, type DaemonRequest, type DaemonResponse } from '../daemon-client.ts';
import { attachActiveSessionAddressHint } from '../daemon-client-lifecycle.ts';
import { sendRequest } from '../daemon-client-transport.ts';
import { readDaemonInfo } from '../daemon-client-metadata.ts';
import type { DaemonRetirementResult } from '../../daemon-registration-owner.ts';
import {
closeLoopbackServer,
listenOnLoopback,
Expand Down Expand Up @@ -615,12 +617,18 @@ test('sendRequest timeout cleanup uses resolved daemon paths instead of request
const daemonPaths = resolveDaemonPaths(daemonStateDir);
const requestFlagPaths = resolveDaemonPaths(requestFlagStateDir);
const daemon = await startHangingHttpDaemonFixture();
writeDaemonInfo(daemonPaths, {
httpPort: daemon.port,
transport: 'http',
pid: 999_999,
});
writeDaemonLock(daemonPaths, { pid: 999_999 });
mockSleep.mockImplementation(actualRetry.sleep);
const child = spawnRegisteredDaemonFixture(
daemonPaths,
{
httpPort: daemon.port,
token: 'local-secret',
version: readVersion(),
codeOrigin: 'checkout',
codeSignature: currentDaemonCodeSignature(),
},
undefined,
);
writeDaemonInfo(requestFlagPaths, {
httpPort: daemon.port,
transport: 'http',
Expand All @@ -638,34 +646,34 @@ test('sendRequest timeout cleanup uses resolved daemon paths instead of request
};

try {
let info = readDaemonInfo(daemonPaths.infoPath);
for (let attempt = 0; !info && attempt < 200; attempt += 1) {
Comment thread
thymikee marked this conversation as resolved.
await actualRetry.sleep(10);
info = readDaemonInfo(daemonPaths.infoPath);
}
assert.ok(info);
let thrown: unknown;
try {
await sendRequest(
{
token: 'local-secret',
pid: 999_999,
httpPort: daemon.port,
transport: 'http',
},
request,
'http',
daemonPaths,
50,
);
await sendRequest(info, request, 'http', daemonPaths, 50);
} catch (error) {
thrown = error;
}

assert.ok(thrown instanceof AppError);
assert.equal(thrown.message, 'Daemon request timed out');
assert.equal(
(thrown.details?.retirement as DaemonRetirementResult | undefined)?.status,
'retired',
);
await child.exited;
assert.deepEqual(daemon.seenPaths, ['POST /rpc']);
assert.equal(fs.existsSync(daemonPaths.infoPath), false);
assert.equal(fs.existsSync(daemonPaths.lockPath), false);
assert.equal(fs.existsSync(requestFlagPaths.infoPath), true);
assert.equal(fs.existsSync(requestFlagPaths.lockPath), true);
} finally {
await closeLoopbackServer(daemon.server);
fs.rmSync(daemonStateDir, { recursive: true, force: true });
await finishRegisteredDaemonFixture(daemonStateDir);
fs.rmSync(requestFlagStateDir, { recursive: true, force: true });
}
});
Expand Down
60 changes: 3 additions & 57 deletions src/daemon-client/__tests__/daemon-client-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
import assert from 'node:assert/strict';
import { AppError, normalizeError } from '@agent-device/kernel/errors';
import fs from 'node:fs';
import path from 'node:path';
import { afterEach, test, vi } from 'vitest';
import { test } from 'vitest';
import type { DaemonCodeOrigin } from '@agent-device/host-kit/code-signature';
import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts';
import {
stopAndRetireDaemon,
tryAcquireDaemonRegistration,
} from '../../daemon-registration-owner.ts';
import {
readDaemonInfo,
stopDaemonProcessForTakeover,
type DaemonInfo,
} from '../daemon-client-metadata.ts';
import { isAgentDeviceDaemonProcess, stopDaemonProcess } from '../../daemon-process.ts';
import { tryAcquireDaemonRegistration } from '../../daemon-registration-owner.ts';
import { readDaemonInfo, type DaemonInfo } from '../daemon-client-metadata.ts';
import { resolveDaemonPaths } from '../../daemon-resolution.ts';

vi.mock('../../daemon-process.ts', async (importOriginal) => ({
...(await importOriginal<typeof import('../../daemon-process.ts')>()),
isAgentDeviceDaemonProcess: vi.fn(),
stopDaemonProcess: vi.fn(),
}));
afterEach(() => vi.resetAllMocks());

// The reuse decision is only as good as the identity that survives the round trip
// through `daemon.json`: a client cannot compare what the file lost (#2458).

Expand Down Expand Up @@ -68,41 +52,3 @@ test('a registration this version did not write reads back unreported', () => {
assert.equal(readDaemonInfo(infoPath)?.codeOrigin, undefined);
}
});

for (const artifact of ['daemon.json', 'daemon.lock']) {
test(`unconfirmed startup stop retains ${artifact} without claiming cleanup`, async () => {
const [stateDir] = scratchStateDir();
const paths = resolveDaemonPaths(stateDir);
const file = path.join(stateDir, artifact);
const contents = JSON.stringify({
pid: 7,
processStartTime: 'start',
port: 1234,
token: 'secret',
});
fs.writeFileSync(file, contents);
vi.mocked(isAgentDeviceDaemonProcess).mockReturnValue(true);
vi.mocked(stopDaemonProcess).mockResolvedValue({ status: 'retained', reason: 'exit-timeout' });
const result = await stopAndRetireDaemon({
paths,
observed: { pid: 7, startTime: 'start' },
mode: 'graceful',
});
assert.equal(fs.readFileSync(file, 'utf8'), contents);
assert.equal(result.removedInfo, false);
assert.equal(result.status, 'retained');
if (result.status === 'retained') assert.equal(result.reason, 'exit-unconfirmed');
});
}

test('a retained takeover keeps its reason at the normalized error boundary', async () => {
vi.mocked(stopDaemonProcess).mockResolvedValue({ status: 'retained', reason: 'exit-timeout' });
await assert.rejects(
stopDaemonProcessForTakeover({ pid: 7, token: 'secret', processStartTime: 'start' }),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(normalizeError(error).details?.reason, 'daemon_exit_unconfirmed');
return true;
},
);
});
Loading
Loading