Skip to content
Merged
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
175 changes: 175 additions & 0 deletions src/__tests__/daemon-registration-owner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { afterEach, test, vi } from 'vitest';
import { readCurrentOwnerIdentity } from '@agent-device/host-kit/process';
import { tryAcquireDaemonRegistration } from '../daemon-registration-owner.ts';
import { resolveDaemonPaths, type DaemonPaths } from '../daemon-resolution.ts';
import { readRegisteredDaemonOwnership } from '../daemon-registration.ts';
import { readDaemonShutdownReport } from '../daemon-shutdown-report.ts';
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';

const fields = {
socketPort: 4210,
token: 'token',
version: '0.0.0-test',
codeOrigin: 'checkout' as const,
codeSignature: 'signature',
};
const ownIdentity = readCurrentOwnerIdentity();
const report = {
providerReleases: { released: [], pending: [] },
claims: { released: [], orphaned: [], superseded: [], unattributable: [] },
};

afterEach(() => vi.restoreAllMocks());

async function acquire(
paths = resolveDaemonPaths(mkdtempForTestSync('agent-device-registration-owner-')),
) {
const attempt = await tryAcquireDaemonRegistration(paths);
assert.equal(attempt.status, 'acquired');
if (attempt.status !== 'acquired') throw new Error('registration refused');
return { paths, owner: attempt.owner };
}

function replaceInfo(paths: DaemonPaths, pid: unknown, startTime?: string | null) {
fs.writeFileSync(
paths.infoPath,
JSON.stringify({ pid, processStartTime: startTime, token: 'token', port: 4210 }),
);
}

test('publication is atomic, binds identity and paths, and excludes a second acquisition', async () => {
const { paths, owner } = await acquire();
owner.publish(fields);
const before = fs.statSync(paths.infoPath).ino;
owner.publish({ ...fields, httpPort: 4310 });
assert.notEqual(fs.statSync(paths.infoPath).ino, before);
assert.equal(readRegisteredDaemonOwnership(paths.infoPath, ownIdentity).state, 'match');
assert.equal(JSON.parse(fs.readFileSync(paths.infoPath, 'utf8')).transport, 'dual');
assert.equal((await tryAcquireDaemonRegistration(paths)).status, 'busy');
assert.deepEqual(await owner.finish(report), { state: 'removed' });
assert.equal(fs.existsSync(paths.infoPath), false);
assert.equal(fs.existsSync(paths.lockPath), false);
assert.deepEqual(readDaemonShutdownReport(paths.baseDir), report);
});

test('publication truncates the log the daemon is already appending to', async () => {
const { paths, owner } = await acquire();
const daemonOutput = fs.openSync(paths.logPath, 'a');
try {
fs.writeSync(daemonOutput, 'previous run\n');
owner.publish(fields);
fs.writeSync(daemonOutput, 'listening\n');
} finally {
fs.closeSync(daemonOutput);
}
assert.equal(fs.readFileSync(paths.logPath, 'utf8'), 'listening\n');
await owner.finish(report);
});

for (const [pid, startTime, reason] of [
[999_999_999, 'successor-start', 'replaced'],
[process.pid, 'recycled-start', 'replaced'],
[process.pid, undefined, 'unproven'],
[null, undefined, 'ownerless'],
] as const) {
test(`finish retains ${reason} registration (${String(pid)}, ${String(startTime)})`, async () => {
const { paths, owner } = await acquire();
owner.publish(fields);
replaceInfo(paths, pid, startTime);
const before = fs.readFileSync(paths.infoPath, 'utf8');
assert.deepEqual(await owner.finish(), {
state: reason,
...(reason === 'replaced' ? { identity: { pid, startTime } } : {}),
});
assert.equal(fs.readFileSync(paths.infoPath, 'utf8'), before);
assert.equal(fs.existsSync(paths.lockPath), false);
});
}

test('absent and corrupt registrations are distinct and corruption is retained', async () => {
const first = await acquire();
assert.deepEqual(await first.owner.finish(), { state: 'absent' });
const second = await acquire();
fs.writeFileSync(second.paths.infoPath, '{not json');
assert.deepEqual(await second.owner.finish(), { state: 'ownerless' });
assert.equal(fs.readFileSync(second.paths.infoPath, 'utf8'), '{not json');
});

test.skipIf(process.getuid?.() === 0)('unreadable metadata is retained', async () => {
const { paths, owner } = await acquire();
owner.publish(fields);
fs.chmodSync(paths.infoPath, 0o000);
try {
assert.deepEqual(await owner.finish(), { state: 'unreadable' });
assert.equal(fs.existsSync(paths.infoPath), true);
} finally {
fs.chmodSync(paths.infoPath, 0o600);
}
});

test('legacy lock files are refused and retained', async () => {
const paths = resolveDaemonPaths(mkdtempForTestSync('agent-device-registration-legacy-'));
const contents = JSON.stringify({ pid: process.pid, processStartTime: ownIdentity.startTime });
fs.writeFileSync(paths.lockPath, contents);
const result = await tryAcquireDaemonRegistration(paths);
assert.equal(result.status, 'unproven');
assert.equal(fs.readFileSync(paths.lockPath, 'utf8'), contents);
});

test('spent acquisitions cannot publish, remove metadata or write a report', async () => {
const { paths, owner } = await acquire();
owner.publish(fields);
await owner.finish();
const successor = await acquire(paths);
successor.owner.publish({ ...fields, token: 'successor-token' });
fs.writeFileSync(`${paths.baseDir}/daemon-shutdown.json`, JSON.stringify(report));
const beforeInfo = fs.readFileSync(paths.infoPath, 'utf8');
const beforeReport = fs.readFileSync(`${paths.baseDir}/daemon-shutdown.json`, 'utf8');
assert.throws(() => owner.publish(fields));
await assert.rejects(owner.finish(report));
assert.equal(fs.readFileSync(paths.infoPath, 'utf8'), beforeInfo);
assert.equal(fs.readFileSync(`${paths.baseDir}/daemon-shutdown.json`, 'utf8'), beforeReport);
await successor.owner.finish();
});

test('startup clears a prior report while held; a failed clear releases the acquisition', async () => {
const paths = resolveDaemonPaths(mkdtempForTestSync('agent-device-registration-clear-'));
const reportPath = `${paths.baseDir}/daemon-shutdown.json`;
fs.writeFileSync(reportPath, 'old report');
const first = await acquire(paths);
assert.equal(fs.existsSync(reportPath), false);
await first.owner.finish();
const primary = Object.assign(new Error('clear failure'), { code: 'EACCES' });
const original = fs.rmSync;
vi.spyOn(fs, 'rmSync').mockImplementation((target, options) => {
if (target === reportPath) throw primary;
return original(target, options);
});
await assert.rejects(tryAcquireDaemonRegistration(paths), (error) => error === primary);
assert.equal(fs.existsSync(paths.lockPath), false);
});

test('unlink disappearance is settled, other failures remain primary even when release fails', async () => {
const first = await acquire();
first.owner.publish(fields);
const original = fs.unlinkSync;
vi.spyOn(fs, 'unlinkSync').mockImplementation((target) => {
if (target === first.paths.infoPath)
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
return original(target);
});
assert.deepEqual(await first.owner.finish(), { state: 'removed' });
vi.restoreAllMocks();
const second = await acquire();
second.owner.publish(fields);
const primary = new Error('metadata failure');
vi.spyOn(fs, 'unlinkSync').mockImplementation((target) => {
if (target === second.paths.infoPath) throw primary;
throw new Error('release failure');
});
await assert.rejects(second.owner.finish(), (error) => error === primary);
assert.equal(fs.existsSync(second.paths.infoPath), true);
assert.match(fs.readFileSync(second.paths.logPath, 'utf8'), /daemon_registration_release_failed/);
});
10 changes: 1 addition & 9 deletions src/__tests__/daemon-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
readRegisteredDaemonIdentity,
readRegisteredDaemonOwnership,
} from '../daemon-registration.ts';
import { writeInfo } from '../daemon/server/server-lifecycle.ts';
import { publishDaemonRegistration } from './test-utils/device-claim-store.ts';
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';

Expand All @@ -29,14 +28,7 @@ function infoPathOf(stateDir: string): string {

test('reads back the identity a running daemon publishes for its state dir', () => {
const stateDir = useStateDir();
writeInfo(stateDir, infoPathOf(stateDir), path.join(stateDir, 'daemon.log'), {
socketPort: 1234,
token: 'token',
codeOrigin: 'checkout',
version: '0.0.0-test',
codeSignature: 'signature',
processStartTime: 'published-start',
});
publishDaemonRegistration(stateDir, { pid: process.pid, startTime: 'published-start' });

assert.deepEqual(readRegisteredDaemonIdentity(infoPathOf(stateDir)), {
pid: process.pid,
Expand Down
14 changes: 4 additions & 10 deletions src/__tests__/daemon-shutdown-report.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { expect, test } from 'vitest';
import {
clearDaemonShutdownReport,
readDaemonShutdownReport,
writeDaemonShutdownReport,
} from '../daemon-shutdown-report.ts';
import { readDaemonShutdownReport, buildDaemonShutdownReport } from '../daemon-shutdown-report.ts';
import { LeaseRegistry } from '../daemon/lease-registry.ts';
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';

Expand All @@ -25,11 +21,12 @@ test('round-trips provider release and device claim records without lease creden
});

try {
writeDaemonShutdownReport(stateDir, {
const report = buildDaemonShutdownReport({
providerReleases: { released: [lease], pending: [lease] },
claims: { released: [claim], orphaned: [], superseded: [claim], unattributable: [] },
});

fs.writeFileSync(path.join(stateDir, 'daemon-shutdown.json'), JSON.stringify(report));
expect(readDaemonShutdownReport(stateDir)).toEqual({
providerReleases: {
released: [{ leaseId: lease.leaseId, provider: 'limrun' }],
Expand Down Expand Up @@ -61,7 +58,7 @@ test('a report written before claim reporting still reads its provider releases'
}
});

test('ignores malformed shutdown reports and clear removes a prior report', () => {
test('ignores malformed shutdown reports', () => {
const stateDir = mkdtempForTestSync('agent-device-shutdown-report-');
const reportPath = path.join(stateDir, 'daemon-shutdown.json');

Expand All @@ -75,9 +72,6 @@ test('ignores malformed shutdown reports and clear removes a prior report', () =
JSON.stringify({ providerReleases: { released: [{}], pending: [] } }),
);
expect(readDaemonShutdownReport(stateDir)).toBeNull();

clearDaemonShutdownReport(stateDir);
expect(fs.existsSync(reportPath)).toBe(false);
} finally {
fs.rmSync(stateDir, { recursive: true, force: true });
}
Expand Down
19 changes: 12 additions & 7 deletions src/daemon-client/__tests__/daemon-client-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import path from 'node:path';
import { afterEach, test, vi } from 'vitest';
import type { DaemonCodeOrigin } from '@agent-device/host-kit/code-signature';
import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts';
import { writeInfo } from '../../daemon/server/server-lifecycle.ts';
import { tryAcquireDaemonRegistration } from '../../daemon-registration-owner.ts';
import {
readDaemonInfo,
cleanupFailedDaemonStartupMetadata,
stopDaemonProcessForTakeover,
type DaemonInfo,
} from '../daemon-client-metadata.ts';
import { isAgentDeviceDaemonProcess, stopDaemonProcess } from '../../daemon-process.ts';
import { resolveDaemonPaths } from '../../daemon-resolution.ts';
Expand All @@ -29,22 +30,26 @@ function scratchStateDir(): [stateDir: string, infoPath: string] {
return [stateDir, path.join(stateDir, 'daemon.json')];
}

function publishInfo(codeOrigin: DaemonCodeOrigin): string {
async function publishInfo(codeOrigin: DaemonCodeOrigin): Promise<DaemonInfo | null> {
const [stateDir, infoPath] = scratchStateDir();
writeInfo(stateDir, infoPath, path.join(stateDir, 'daemon.log'), {
const registration = await tryAcquireDaemonRegistration(resolveDaemonPaths(stateDir));
assert.equal(registration.status, 'acquired');
if (registration.status !== 'acquired') throw new Error('registration refused');
registration.owner.publish({
httpPort: 41_234,
token: 'local-secret',
version: '0.0.0-test',
codeOrigin,
codeSignature: 'graph:1:abc',
processStartTime: 'start',
});
return infoPath;
const published = readDaemonInfo(infoPath);
await registration.owner.finish();
return published;
}

test('a daemon publishes the code origin its client reads back', () => {
test('a daemon publishes the code origin its client reads back', async () => {
for (const codeOrigin of ['installed', 'checkout'] as const) {
assert.equal(readDaemonInfo(publishInfo(codeOrigin))?.codeOrigin, codeOrigin);
assert.equal((await publishInfo(codeOrigin))?.codeOrigin, codeOrigin);
}
});

Expand Down
Loading
Loading