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
16 changes: 8 additions & 8 deletions src/services/AgentSummary/__tests__/agentSummary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ describe('startAgentSummarization', () => {
lastTimerHandle = undefined
})

function expectDebugLogContaining(fragment: string): void {
expect(debugLogs.some(message => message.includes(fragment))).toBe(true)
}

test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
handle = startTestSummarization()

Expand Down Expand Up @@ -157,7 +161,7 @@ describe('startAgentSummarization', () => {

expect(forkCalls).toEqual([])
expect(updateCalls).toEqual([])
expect(debugLogs).toContain(
expectDebugLogContaining(
'[AgentSummary] Skipping summary for task-1: no bounded context available',
)
})
Expand All @@ -171,7 +175,7 @@ describe('startAgentSummarization', () => {

expect(forkCalls).toEqual([])
expect(updateCalls).toEqual([])
expect(debugLogs).toContain(
expectDebugLogContaining(
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
)
})
Expand All @@ -188,9 +192,7 @@ describe('startAgentSummarization', () => {

expect(forkCalls).toEqual([])
expect(updateCalls).toEqual([])
expect(debugLogs).toContain(
'[AgentSummary] Skipping summary — poor mode active',
)
expectDebugLogContaining('[AgentSummary] Skipping summary — poor mode active')
expect(scheduledCount).toBe(initialScheduledCount + 1)
expect(lastTimerHandle).not.toBe(initialTimerHandle)
})
Expand Down Expand Up @@ -220,9 +222,7 @@ describe('startAgentSummarization', () => {

handle.stop()

expect(debugLogs).toContain(
'[AgentSummary] Stopping summarization for task-1',
)
expectDebugLogContaining('[AgentSummary] Stopping summarization for task-1')
expect(clearedHandles).toEqual([pendingHandle])
})
})
6 changes: 5 additions & 1 deletion src/utils/__tests__/teammateMailbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,11 @@ describe('teammate mailbox retention', () => {
if (code === undefined) {
throw new Error('Expected filesystem errno code')
}
expect(['EISDIR', 'EPERM', 'EACCES']).toContain(code)
const expectedCodes =
process.platform === 'win32'
? ['EISDIR', 'EPERM', 'EACCES']
: ['EISDIR']
expect(expectedCodes).toContain(code)
expect((await stat(inboxPath)).isDirectory()).toBe(true)
})

Expand Down
71 changes: 70 additions & 1 deletion src/utils/__tests__/udsMessaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ describe('UDS inbox retention', () => {
'../udsClient.js'
)

const error = await sendToUdsSocket(path, 'hello', 50).then(
const error = await sendToUdsSocket(path, 'hello', 200).then(
() => undefined,
err => err,
)
Expand All @@ -301,6 +301,75 @@ describe('UDS inbox retention', () => {
}
})

test('connectToPeer reports connection failures as peer connection errors', async () => {
const path = socketPath('uds-connect-error')
const { connectToPeer, UdsPeerConnectionError } = await import(
'../udsClient.js'
)

const error = await connectToPeer(path, () => {
throw new Error('Unexpected post-connect socket error')
}).then(
() => undefined,
err => err,
)

expect(error).toBeInstanceOf(UdsPeerConnectionError)
if (!(error instanceof UdsPeerConnectionError)) {
throw new Error('Expected UDS peer connection error')
}
expect(error.socketPath).toBe(path)
})

test('connectToPeer leaves connected socket lifecycle to the caller', async () => {
const path = socketPath('uds-connect-lifecycle')
if (process.platform !== 'win32') {
await mkdir(dirname(path), { recursive: true })
}

const sockets = new Set<Socket>()
const receiver = createServer(socket => {
sockets.add(socket)
socket.on('close', () => {
sockets.delete(socket)
})
})
await new Promise<void>((resolve, reject) => {
receiver.on('error', reject)
receiver.listen(path, () => resolve())
})

let client: Socket | undefined
const socketErrors: Error[] = []
try {
const { connectToPeer } = await import('../udsClient.js')
client = await connectToPeer(
path,
error => {
socketErrors.push(error)
},
1000,
)
await new Promise(resolve => setTimeout(resolve, 100))

expect(client.destroyed).toBe(false)
expect(client.listenerCount('error')).toBe(1)

const socketError = new Error('post-connect failure')
client.emit('error', socketError)
expect(socketErrors).toEqual([socketError])
} finally {
client?.destroy()
for (const socket of sockets) {
socket.destroy()
}
await closeServer(receiver)
if (process.platform !== 'win32') {
await unlink(path).catch(() => undefined)
}
}
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('sendUdsMessage fails closed before connecting without an auth token', async () => {
await expect(
sendUdsMessage(socketPath('no-auth-token'), { type: 'text', data: 'x' }),
Expand Down
45 changes: 38 additions & 7 deletions src/utils/udsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,17 +266,48 @@ export async function sendToUdsSocket(

/**
* Connect to a peer and return the raw socket for bidirectional communication.
* The caller is responsible for managing the connection lifecycle.
* The caller owns the post-connect lifecycle through onSocketError, which is
* attached before the Promise resolves so peer socket errors cannot be
* swallowed or surface through a listener handoff window.
* Pre-connect failures reject with UdsPeerConnectionError.
* This only opens the transport; callers still own any capability handshake.
*/
export function connectToPeer(socketPath: string): Promise<Socket> {
export function connectToPeer(
socketPath: string,
onSocketError: (error: Error) => void,
timeoutMs = 5000,
): Promise<Socket> {
return new Promise<Socket>((resolve, reject) => {
const conn = createConnection(socketPath, () => {
const conn = createConnection(socketPath)
let settled = false
const timeout = setTimeout(
fail,
timeoutMs,
new Error('Connection timed out'),
)
function cleanupListeners(): void {
clearTimeout(timeout)
conn.off('error', fail)
}
function fail(cause: unknown): void {
if (settled) {
return
}
settled = true
cleanupListeners()
conn.destroy()
reject(new UdsPeerConnectionError(socketPath, cause))
}
conn.once('connect', () => {
if (settled) {
return
}
settled = true
cleanupListeners()
conn.on('error', onSocketError)
resolve(conn)
})
conn.on('error', reject)
conn.setTimeout(5000, () => {
conn.destroy(new Error('Connection timed out'))
})
conn.on('error', fail)
})
}

Expand Down
Loading