import { setImmediate } from 'node:timers/promises'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { HostInventory } from '../../src/lib/hosts.js'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; const epoch = Date.parse('2026-10-05T12:00:00Z'); const inventory = { hostname: 'request-cache-host', protocolVersion: 1, dockerRunning: true, directoriesWritable: true, storageAvailable: 100 * 1024 ** 3, preparedRuntimeImages: ['plutainer:test'], libraries: [{ id: 'test-library', state: 'ready' }] }; beforeEach(async () => { vi.resetModules(); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(epoch); fixture = await newFixture([]); }); afterEach(async () => { vi.restoreAllMocks(); vi.useRealTimers(); await fixture.cleanup(); }); async function enrolled() { const hosts = await import('../../src/lib/server/hosts/index.js'); const context = await import('../../src/lib/server/hosts/request-context.js'); const host = hosts.createHost({ name: 'Request cache host' }); const { credential } = hosts.enrollHost(hosts.createEnrollment(host.id).token, inventory); // Finish enrollment's independent availability refresh before counting parses. await setImmediate(); return { ...hosts, ...context, hostId: host.id, credential }; } describe('agent preparation admission', () => { it('skips fleet preparation lookups for empty, health, read and unlimited library polls', async () => { const api = await enrolled(); const { db } = await import('../../src/lib/server/db/index.js'); const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js'); saveFleetSettings({ ...fleetSettings(), preparationConcurrency: 1 }); const prepare = vi.spyOn(db(), 'prepare'); expect(api.pendingHostOperations(api.hostId)).toEqual([]); api.queueHostOperation(api.hostId, 'instance.inspect', {}); api.pendingHostOperations(api.hostId, 'health'); api.queueHostOperation(api.hostId, 'files.read', { path: 'server.cfg' }); api.pendingHostOperations(api.hostId, 'read'); saveFleetSettings({ ...fleetSettings(), preparationConcurrency: 0 }); const download = api.queueHostOperation(api.hostId, 'library.download', {}); expect(api.pendingHostOperations(api.hostId, 'write').map((op) => op.id)).toContain( download.id ); expect(prepare.mock.calls.some(([sql]) => sql.includes('SELECT DISTINCT host_id'))).toBe( false ); }); it('uses an indexed lookup and counts delivered preparation by host until all its jobs finish', async () => { const api = await enrolled(); const other = api.createHost({ name: 'Other host' }); api.enrollHost(api.createEnrollment(other.id).token, inventory); const { db } = await import('../../src/lib/server/db/index.js'); const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js'); saveFleetSettings({ ...fleetSettings(), preparationConcurrency: 1 }); const download = api.queueHostOperation(api.hostId, 'library.download', {}); const imported = api.queueHostOperation(api.hostId, 'library.import', {}); const waiting = api.queueHostOperation(other.id, 'library.download', {}); const prepare = vi.spyOn(db(), 'prepare'); expect(api.pendingHostOperations(api.hostId, 'write').map((op) => op.id)).toEqual([ download.id, imported.id ]); const sql = prepare.mock.calls.find(([sql]) => sql.includes('SELECT DISTINCT host_id'))![0]; const plan = db().prepare(`EXPLAIN QUERY PLAN ${sql}`).all() as { detail: string }[]; expect(plan.map((row) => row.detail).join('\n')).toContain( 'USING INDEX ec_host_operations_kind_state (kind=? AND state=?)' ); expect(api.pendingHostOperations(other.id, 'write')).toEqual([]); api.reportHostOperation(api.hostId, download.id, { state: 'completed' }); expect(api.pendingHostOperations(other.id, 'write')).toEqual([]); api.reportHostOperation(api.hostId, imported.id, { state: 'failed' }); expect(api.pendingHostOperations(other.id, 'write').map((op) => op.id)).toEqual([ waiting.id ]); }); }); describe('agent credential usage writes', () => { it('writes on first use and at most once per minute, including after reopening the database', async () => { const api = await enrolled(); const { db, closeDb } = await import('../../src/lib/server/db/index.js'); db().exec(`CREATE TABLE credential_usage_writes (credential_id TEXT, at TEXT); CREATE TRIGGER count_credential_usage AFTER UPDATE OF last_used_at ON ec_host_credentials BEGIN INSERT INTO credential_usage_writes VALUES (NEW.id, NEW.last_used_at); END;`); const writes = () => db().prepare('SELECT at FROM credential_usage_writes').all(); const authenticate = () => expect(api.authenticateCredential(api.credential)?.id).toBe(api.hostId); authenticate(); authenticate(); authenticate(); expect(writes()).toEqual([{ at: new Date(epoch).toISOString() }]); vi.setSystemTime(epoch + 59_999); authenticate(); closeDb(); authenticate(); expect(writes()).toHaveLength(1); vi.setSystemTime(epoch + 60_000); authenticate(); authenticate(); expect(writes()).toEqual([ { at: new Date(epoch).toISOString() }, { at: new Date(epoch + 60_000).toISOString() } ]); expect(api.authenticateCredential('invalid')).toBeNull(); expect(writes()).toHaveLength(2); }); it('checks revocation immediately and records first use of a replacement credential', async () => { const api = await enrolled(); const { db } = await import('../../src/lib/server/db/index.js'); await api.withHostRequestContext(() => { expect(api.authenticateCredential(api.credential)?.id).toBe(api.hostId); vi.setSystemTime(epoch + 1000); api.queueHostOperation(api.hostId, 'credentials.rotate', {}); const replacement = api.rotateCredential(api.hostId); expect(api.authenticateCredential(api.credential)).toBeNull(); expect(api.authenticateCredential(replacement)?.id).toBe(api.hostId); expect( db() .prepare( 'SELECT last_used_at FROM ec_host_credentials WHERE revoked_at IS NULL' ) .get() ).toEqual({ last_used_at: new Date(epoch + 1000).toISOString() }); api.revokeCredentials(api.hostId); expect(api.authenticateCredential(replacement)).toBeNull(); expect(api.getHost(api.hostId)?.credentialActive).toBe(false); }); }); }); describe('request-scoped host inventory parsing', () => { it('parses once across authentication, host reads, and operation handling within a request', async () => { const api = await enrolled(); const serialized = JSON.stringify(inventory); const parse = vi.spyOn(JSON, 'parse'); await api.withHostRequestContext(async () => { const authenticated = api.authenticateCredential(api.credential)!; const operation = api.queueHostOperation(api.hostId, 'instance.inspect', { name: 'server' }); api.pendingHostOperationRows(api.hostId, 'read', false); api.reportHostOperationStatus(api.hostId, operation.id, { state: 'accepted' }); await Promise.resolve(); for (let i = 0; i < 10; i++) expect(api.getHost(api.hostId)?.inventory).toBe(authenticated.inventory); expect(api.requireReadyHost(api.hostId).inventory).toBe(authenticated.inventory); expect(api.listHosts()[0].inventory).toBe(authenticated.inventory); }); expect(parse.mock.calls.filter(([value]) => value === serialized)).toHaveLength(1); await api.withHostRequestContext(() => api.getHost(api.hostId)); expect(parse.mock.calls.filter(([value]) => value === serialized)).toHaveLength(2); }); it('keeps host metadata fresh and reparses changed inventory, including after rollback', async () => { const api = await enrolled(); const { db } = await import('../../src/lib/server/db/index.js'); await api.withHostRequestContext(() => { const original = api.getHost(api.hostId)!; const updated = api.updateHost(api.hostId, { name: 'Renamed host', mode: 'draining' }); expect(updated).toMatchObject({ name: 'Renamed host', mode: 'draining', readiness: 'draining' }); expect(updated.inventory).toBe(original.inventory); const heartbeat = api.heartbeat(api.hostId, { ...inventory, storageAvailable: 123 }); expect(heartbeat.inventory).not.toBe(original.inventory); expect(api.getHost(api.hostId)?.inventory).toBe(heartbeat.inventory); expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(123); expect(() => db().transaction(() => { db() .prepare('UPDATE ec_hosts SET inventory=? WHERE id=?') .run(JSON.stringify({ ...inventory, storageAvailable: 456 }), api.hostId); expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(456); throw new Error('rollback'); })() ).toThrow('rollback'); expect(api.getHost(api.hostId)?.inventory.storageAvailable).toBe(123); }); }); it('isolates overlapping requests and does not cache reads outside a request', async () => { const api = await enrolled(); let release!: () => void; const gate = new Promise((resolve) => { release = resolve; }); let first!: HostInventory; const request = api.withHostRequestContext(async () => { first = api.getHost(api.hostId)!.inventory; await gate; expect(api.getHost(api.hostId)!.inventory).toBe(first); }); try { await api.withHostRequestContext(async () => { const second = api.getHost(api.hostId)!.inventory; expect(second).not.toBe(first); await Promise.resolve(); expect(api.getHost(api.hostId)!.inventory).toBe(second); }); expect(api.getHost(api.hostId)!.inventory).not.toBe(api.getHost(api.hostId)!.inventory); } finally { release(); await request; } }); it.each([false, true])( 'releases cached inventory from inherited work after request completion (failed: %s)', async (failed) => { const api = await enrolled(); let release!: () => void; const gate = new Promise((resolve) => { release = resolve; }); let detached!: Promise; let original!: HostInventory; const request = api.withHostRequestContext(() => { original = api.getHost(api.hostId)!.inventory; detached = gate.then(() => { const later = api.getHost(api.hostId)!.inventory; expect(later).not.toBe(original); expect(api.getHost(api.hostId)!.inventory).not.toBe(later); }); if (failed) throw new Error('request failed'); }); if (failed) await expect(request).rejects.toThrow('request failed'); else await request; release(); await detached; } ); });