From 3305da0d499478a119c104a494a522d386744ba3 Mon Sep 17 00:00:00 2001 From: unraid Date: Mon, 27 Apr 2026 16:13:04 +0800 Subject: [PATCH] test: enforce structured UDS timeout failures CodeRabbit's follow-up surfaced a real consistency gap: UDS send socket errors used UdsPeerConnectionError while response timeouts still rejected a generic Error. Timeouts now use the same structured peer failure contract, and the test exercises that path through a short explicit timeout instead of waiting for the production default. The AgentSummary unchanged-fingerprint test now also asserts that the second unchanged tick does not log errors, preserving the existing behavior checks without changing production scheduling semantics. Constraint: Keep the production timeout default at 5000ms while allowing tests to exercise the timeout path quickly. Rejected: Leave timeout failures as generic Error | callers would need separate handling for the same peer connection failure class. Confidence: high Scope-risk: narrow Directive: Keep UDS send timeout and socket-error branches on the same structured error contract. Tested: bun test src/services/AgentSummary/__tests__/agentSummary.test.ts src/utils/__tests__/udsMessaging.test.ts Tested: bunx tsc --noEmit --pretty false Tested: bun run lint Tested: bun run test:all Tested: bun test --coverage --coverage-reporter lcov --coverage-dir coverage Tested: bun run build Tested: bun run build:vite Not-tested: GitHub-hosted CodeRabbit refresh until pushed. --- .../__tests__/agentSummary.test.ts | 1 + src/utils/__tests__/udsMessaging.test.ts | 60 ++++++++++++++++++- src/utils/udsClient.ts | 10 +++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/services/AgentSummary/__tests__/agentSummary.test.ts b/src/services/AgentSummary/__tests__/agentSummary.test.ts index cc3c87d36..421219a94 100644 --- a/src/services/AgentSummary/__tests__/agentSummary.test.ts +++ b/src/services/AgentSummary/__tests__/agentSummary.test.ts @@ -134,6 +134,7 @@ describe('startAgentSummarization', () => { expect(forkCalls).toHaveLength(1) expect(updateCalls).toHaveLength(1) + expect(loggedErrors).toEqual([]) }) test('skips summarization when filtering leaves too little bounded context', async () => { diff --git a/src/utils/__tests__/udsMessaging.test.ts b/src/utils/__tests__/udsMessaging.test.ts index 3f0b6dd3d..bebaa495d 100644 --- a/src/utils/__tests__/udsMessaging.test.ts +++ b/src/utils/__tests__/udsMessaging.test.ts @@ -11,7 +11,7 @@ import { writeFile, } from 'node:fs/promises' import { createHash } from 'node:crypto' -import { createConnection, createServer } from 'node:net' +import { createConnection, createServer, type Socket } from 'node:net' import { dirname, join } from 'node:path' import { tmpdir } from 'node:os' import { @@ -243,6 +243,64 @@ describe('UDS inbox retention', () => { expect(error.message).not.toContain('test-token') }) + test('udsClient send reports response timeouts as peer connection errors', async () => { + const path = socketPath('uds-client-timeout') + const capabilityDir = join(tempConfigDir, 'messaging-capabilities') + const capabilityName = `${createHash('sha256').update(path).digest('hex')}.json` + await mkdir(capabilityDir, { recursive: true, mode: 0o700 }) + await writeFile( + join(capabilityDir, capabilityName), + JSON.stringify({ socketPath: path, authToken: 'test-token' }), + 'utf-8', + ) + if (process.platform !== 'win32') { + await mkdir(dirname(path), { recursive: true }) + } + + const sockets = new Set() + const receiver = createServer(socket => { + sockets.add(socket) + socket.on('close', () => { + sockets.delete(socket) + }) + socket.on('data', () => undefined) + }) + await new Promise((resolve, reject) => { + receiver.on('error', reject) + receiver.listen(path, () => resolve()) + }) + + try { + const { sendToUdsSocket, UdsPeerConnectionError } = await import( + '../udsClient.js' + ) + + const error = await sendToUdsSocket(path, 'hello', 50).then( + () => undefined, + err => err, + ) + expect(error).toBeInstanceOf(UdsPeerConnectionError) + if (!(error instanceof UdsPeerConnectionError)) { + throw new Error('Expected UDS peer connection timeout error') + } + expect(error.socketPath).toBe(path) + expect(error.cause).toBeInstanceOf(Error) + if (!(error.cause instanceof Error)) { + throw new Error('Expected timeout cause') + } + expect(error.cause.message).toBe('Connection timed out') + expect(error.message).not.toContain('test-token') + } finally { + for (const socket of sockets) { + socket.destroy() + } + await closeServer(receiver) + if (process.platform !== 'win32') { + await unlink(path).catch(() => undefined) + } + } + }) + test('sendUdsMessage fails closed before connecting without an auth token', async () => { await expect( sendUdsMessage(socketPath('no-auth-token'), { type: 'text', data: 'x' }), diff --git a/src/utils/udsClient.ts b/src/utils/udsClient.ts index 1e06756bb..54d88f7fc 100644 --- a/src/utils/udsClient.ts +++ b/src/utils/udsClient.ts @@ -206,6 +206,7 @@ export async function isPeerAlive( export async function sendToUdsSocket( targetSocketPath: string, message: string | Record, + timeoutMs = 5000, ): Promise { const { parseUdsTarget } = await import('./udsMessaging.js') const target = parseUdsTarget(targetSocketPath) @@ -252,8 +253,13 @@ export async function sendToUdsSocket( formatSocketError: err => new UdsPeerConnectionError(target.socketPath, err), }) - conn.setTimeout(5000, () => { - finish(new Error('Connection timed out')) + conn.setTimeout(timeoutMs, () => { + finish( + new UdsPeerConnectionError( + target.socketPath, + new Error('Connection timed out'), + ), + ) }) }) }