mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
Compare commits
3 Commits
codex/code
...
codex/memo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379928fa10 | ||
|
|
ee0d788e58 | ||
|
|
f353eb056a |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "1.10.4",
|
"version": "1.10.2",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|||||||
@@ -616,7 +616,10 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
case 'shutdown_response':
|
case 'shutdown_response':
|
||||||
return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}`
|
return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}`
|
||||||
case 'plan_approval_response':
|
case 'plan_approval_response':
|
||||||
return `plan_approval ${input.message.approve ? 'approve' : 'reject'} to ${recipient}`
|
const planApprovalDecision = input.message.approve
|
||||||
|
? 'approve'
|
||||||
|
: 'reject'
|
||||||
|
return `plan_approval ${planApprovalDecision} to ${recipient}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -834,10 +837,10 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
const { postInterClaudeMessage } =
|
const { postInterClaudeMessage } =
|
||||||
require('src/bridge/peerSessions.js') as typeof import('src/bridge/peerSessions.js')
|
require('src/bridge/peerSessions.js') as typeof import('src/bridge/peerSessions.js')
|
||||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||||
const result = (await postInterClaudeMessage(
|
const result = await postInterClaudeMessage(
|
||||||
addr.target,
|
addr.target,
|
||||||
input.message,
|
input.message,
|
||||||
)) as { ok: boolean; error?: string }
|
) as { ok: boolean; error?: string }
|
||||||
const preview = input.summary || truncate(input.message, 50)
|
const preview = input.summary || truncate(input.message, 50)
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -849,7 +852,6 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addr.scheme === 'uds') {
|
if (addr.scheme === 'uds') {
|
||||||
const recipient = recipientForDisplay(input.to)
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const { sendToUdsSocket } =
|
const { sendToUdsSocket } =
|
||||||
require('src/utils/udsClient.js') as typeof import('src/utils/udsClient.js')
|
require('src/utils/udsClient.js') as typeof import('src/utils/udsClient.js')
|
||||||
@@ -860,14 +862,14 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
success: true,
|
success: true,
|
||||||
message: `”${preview}” → ${recipient}`,
|
message: `”${preview}” → ${input.to}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Failed to send to ${recipient}: ${errorMessage(e)}`,
|
message: `Failed to send to ${input.to}: ${errorMessage(e)}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import {
|
|||||||
getOriginalCwd,
|
getOriginalCwd,
|
||||||
getSessionId,
|
getSessionId,
|
||||||
regenerateSessionId,
|
regenerateSessionId,
|
||||||
resetCostState,
|
|
||||||
setLastAPIRequest,
|
|
||||||
setLastAPIRequestMessages,
|
|
||||||
setLastClassifierRequests,
|
|
||||||
} from '../../bootstrap/state.js'
|
} from '../../bootstrap/state.js'
|
||||||
import type { SDKStatusMessage } from '../../entrypoints/sdk/coreTypes.js'
|
import type { SDKStatusMessage } from '../../entrypoints/sdk/coreTypes.js'
|
||||||
import {
|
import {
|
||||||
@@ -148,14 +144,6 @@ export async function clearConversation({
|
|||||||
// tracking) is retained so those agents keep functioning.
|
// tracking) is retained so those agents keep functioning.
|
||||||
clearSessionCaches(preservedAgentIds)
|
clearSessionCaches(preservedAgentIds)
|
||||||
|
|
||||||
// Clear large STATE-held data that outlives the message array.
|
|
||||||
// lastAPIRequestMessages can hold the full post-compaction conversation
|
|
||||||
// (hundreds of KB–MB) for /share; resetCostState clears modelUsage.
|
|
||||||
setLastAPIRequest(null)
|
|
||||||
setLastAPIRequestMessages(null)
|
|
||||||
setLastClassifierRequests(null)
|
|
||||||
resetCostState()
|
|
||||||
|
|
||||||
setCwd(getOriginalCwd())
|
setCwd(getOriginalCwd())
|
||||||
readFileState.clear()
|
readFileState.clear()
|
||||||
discoveredSkillNames?.clear()
|
discoveredSkillNames?.clear()
|
||||||
|
|||||||
@@ -3051,22 +3051,12 @@ export function REPL({
|
|||||||
// are O(n) per render, so drop everything before the previous
|
// are O(n) per render, so drop everything before the previous
|
||||||
// boundary to keep n bounded across multi-day sessions.
|
// boundary to keep n bounded across multi-day sessions.
|
||||||
if (isFullscreenEnvEnabled()) {
|
if (isFullscreenEnvEnabled()) {
|
||||||
setMessages(old => {
|
setMessages(old => [
|
||||||
const postBoundary = getMessagesAfterCompactBoundary(old, {
|
...getMessagesAfterCompactBoundary(old, {
|
||||||
includeSnipped: true,
|
includeSnipped: true,
|
||||||
})
|
}),
|
||||||
// Hard cap: keep at most 500 messages in fullscreen scrollback
|
newMessage,
|
||||||
// to prevent unbounded memory growth in multi-day sessions.
|
]);
|
||||||
// normalizeMessages/applyGrouping are O(n), and Ink fiber
|
|
||||||
// trees cost ~250KB RSS per message. Without this cap,
|
|
||||||
// scrollback after several compactions can reach thousands
|
|
||||||
// of messages (observed: 13k+, 1GB+ heap).
|
|
||||||
const MAX_FULLSCREEN_SCROLLBACK = 500
|
|
||||||
const kept = postBoundary.length > MAX_FULLSCREEN_SCROLLBACK
|
|
||||||
? postBoundary.slice(-MAX_FULLSCREEN_SCROLLBACK)
|
|
||||||
: postBoundary
|
|
||||||
return [...kept, newMessage]
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setMessages(() => [newMessage]);
|
setMessages(() => [newMessage]);
|
||||||
}
|
}
|
||||||
@@ -3092,24 +3082,18 @@ export function REPL({
|
|||||||
// history). Replacing those leaves the AgentTool UI stuck at
|
// history). Replacing those leaves the AgentTool UI stuck at
|
||||||
// "Initializing…" because it renders the full progress trail.
|
// "Initializing…" because it renders the full progress trail.
|
||||||
setMessages(oldMessages => {
|
setMessages(oldMessages => {
|
||||||
|
const last = oldMessages.at(-1);
|
||||||
|
const lastData = last?.data as Record<string, unknown> | undefined;
|
||||||
const newData = newMessage.data as Record<string, unknown>;
|
const newData = newMessage.data as Record<string, unknown>;
|
||||||
// Scan backwards to find the last ephemeral progress with matching
|
|
||||||
// parentToolUseID and type. Previously only checked the last message,
|
|
||||||
// so interleaved non-ephemeral messages caused duplicate progress
|
|
||||||
// entries to accumulate (observed 13k+ entries in sleep-heavy sessions).
|
|
||||||
for (let i = oldMessages.length - 1; i >= 0; i--) {
|
|
||||||
const m = oldMessages[i]!
|
|
||||||
if (m.type !== 'progress') break
|
|
||||||
const mData = m.data as Record<string, unknown> | undefined
|
|
||||||
if (
|
if (
|
||||||
m.parentToolUseID === newMessage.parentToolUseID &&
|
last?.type === 'progress' &&
|
||||||
mData?.type === newData.type
|
last.parentToolUseID === newMessage.parentToolUseID &&
|
||||||
|
lastData?.type === newData.type
|
||||||
) {
|
) {
|
||||||
const copy = oldMessages.slice();
|
const copy = oldMessages.slice();
|
||||||
copy[i] = newMessage;
|
copy[copy.length - 1] = newMessage;
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return [...oldMessages, newMessage];
|
return [...oldMessages, newMessage];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ describe('startAgentSummarization', () => {
|
|||||||
let debugLogs: string[]
|
let debugLogs: string[]
|
||||||
let loggedErrors: Error[]
|
let loggedErrors: Error[]
|
||||||
let clearedHandles: unknown[]
|
let clearedHandles: unknown[]
|
||||||
let scheduledCount: number
|
|
||||||
let lastTimerHandle: unknown
|
|
||||||
|
|
||||||
function startTestSummarization(
|
function startTestSummarization(
|
||||||
dependencies: AgentSummaryDependencies = {},
|
dependencies: AgentSummaryDependencies = {},
|
||||||
@@ -83,10 +81,8 @@ describe('startAgentSummarization', () => {
|
|||||||
if (typeof callback !== 'function') {
|
if (typeof callback !== 'function') {
|
||||||
throw new Error('Expected timer callback')
|
throw new Error('Expected timer callback')
|
||||||
}
|
}
|
||||||
scheduledCount += 1
|
|
||||||
scheduled = callback as () => void | Promise<void>
|
scheduled = callback as () => void | Promise<void>
|
||||||
lastTimerHandle = { id: scheduledCount }
|
return 1 as unknown as ReturnType<typeof setTimeout>
|
||||||
return lastTimerHandle as ReturnType<typeof setTimeout>
|
|
||||||
}) as unknown as typeof setTimeout,
|
}) as unknown as typeof setTimeout,
|
||||||
updateAgentSummary: (taskId: string, summary: string) => {
|
updateAgentSummary: (taskId: string, summary: string) => {
|
||||||
updateCalls.push({ taskId, summary })
|
updateCalls.push({ taskId, summary })
|
||||||
@@ -105,14 +101,8 @@ describe('startAgentSummarization', () => {
|
|||||||
debugLogs = []
|
debugLogs = []
|
||||||
loggedErrors = []
|
loggedErrors = []
|
||||||
clearedHandles = []
|
clearedHandles = []
|
||||||
scheduledCount = 0
|
|
||||||
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 () => {
|
test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
|
||||||
handle = startTestSummarization()
|
handle = startTestSummarization()
|
||||||
|
|
||||||
@@ -138,7 +128,6 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toHaveLength(1)
|
expect(forkCalls).toHaveLength(1)
|
||||||
expect(updateCalls).toHaveLength(1)
|
expect(updateCalls).toHaveLength(1)
|
||||||
expect(loggedErrors).toEqual([])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('skips summarization when filtering leaves too little bounded context', async () => {
|
test('skips summarization when filtering leaves too little bounded context', async () => {
|
||||||
@@ -161,7 +150,9 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expectDebugLogContaining('no bounded context available')
|
expect(debugLogs).toContain(
|
||||||
|
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('skips summarization before building context when transcript is too short', async () => {
|
test('skips summarization before building context when transcript is too short', async () => {
|
||||||
@@ -173,7 +164,9 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expectDebugLogContaining('not enough messages (2)')
|
expect(debugLogs).toContain(
|
||||||
|
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('skips and reschedules while poor mode is active', async () => {
|
test('skips and reschedules while poor mode is active', async () => {
|
||||||
@@ -182,18 +175,16 @@ describe('startAgentSummarization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(typeof scheduled).toBe('function')
|
expect(typeof scheduled).toBe('function')
|
||||||
const initialScheduledCount = scheduledCount
|
|
||||||
const initialTimerHandle = lastTimerHandle
|
|
||||||
await scheduled!()
|
await scheduled!()
|
||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expectDebugLogContaining('poor mode active')
|
expect(debugLogs).toContain(
|
||||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
'[AgentSummary] Skipping summary — poor mode active',
|
||||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('logs summary errors and schedules the next timer', async () => {
|
test('logs summary errors and keeps the next timer owned by the summarizer', async () => {
|
||||||
const error = new Error('fork failed')
|
const error = new Error('fork failed')
|
||||||
handle = startTestSummarization({
|
handle = startTestSummarization({
|
||||||
runForkedAgent: async () => {
|
runForkedAgent: async () => {
|
||||||
@@ -202,23 +193,20 @@ describe('startAgentSummarization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(typeof scheduled).toBe('function')
|
expect(typeof scheduled).toBe('function')
|
||||||
const initialScheduledCount = scheduledCount
|
|
||||||
const initialTimerHandle = lastTimerHandle
|
|
||||||
await scheduled!()
|
await scheduled!()
|
||||||
|
|
||||||
expect(loggedErrors).toEqual([error])
|
expect(loggedErrors).toEqual([error])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
|
||||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('stop clears the pending summary timer', () => {
|
test('stop clears the pending summary timer', () => {
|
||||||
handle = startTestSummarization()
|
handle = startTestSummarization()
|
||||||
const pendingHandle = lastTimerHandle
|
|
||||||
|
|
||||||
handle.stop()
|
handle.stop()
|
||||||
|
|
||||||
expectDebugLogContaining('Stopping summarization for task-1')
|
expect(debugLogs).toContain(
|
||||||
expect(clearedHandles).toEqual([pendingHandle])
|
'[AgentSummary] Stopping summarization for task-1',
|
||||||
|
)
|
||||||
|
expect(clearedHandles).toEqual([1])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
import { mkdtempSync } from 'node:fs'
|
import { mkdtempSync } from 'node:fs'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import type { Message } from 'src/types/message.js'
|
import type { Message } from 'src/types/message.js'
|
||||||
import { getErrnoCode } from 'src/utils/errors.js'
|
|
||||||
import {
|
import {
|
||||||
compactMailboxMessages,
|
compactMailboxMessages,
|
||||||
getLastPeerDmSummary,
|
getLastPeerDmSummary,
|
||||||
@@ -347,7 +346,8 @@ describe('teammate mailbox retention', () => {
|
|||||||
const inboxPath = getInboxPath('worker', 'alpha')
|
const inboxPath = getInboxPath('worker', 'alpha')
|
||||||
await mkdir(inboxPath, { recursive: true })
|
await mkdir(inboxPath, { recursive: true })
|
||||||
|
|
||||||
const error = await writeToMailbox(
|
await expect(
|
||||||
|
writeToMailbox(
|
||||||
'worker',
|
'worker',
|
||||||
{
|
{
|
||||||
from: 'team-lead',
|
from: 'team-lead',
|
||||||
@@ -355,22 +355,8 @@ describe('teammate mailbox retention', () => {
|
|||||||
timestamp: new Date(5).toISOString(),
|
timestamp: new Date(5).toISOString(),
|
||||||
},
|
},
|
||||||
'alpha',
|
'alpha',
|
||||||
).then(
|
),
|
||||||
() => undefined,
|
).rejects.toThrow()
|
||||||
err => err,
|
|
||||||
)
|
|
||||||
|
|
||||||
const code = getErrnoCode(error)
|
|
||||||
expect(code).toBeDefined()
|
|
||||||
if (code === undefined) {
|
|
||||||
throw new Error('Expected filesystem errno code')
|
|
||||||
}
|
|
||||||
const expectedCodes =
|
|
||||||
process.platform === 'win32'
|
|
||||||
? ['EISDIR', 'EPERM', 'EACCES']
|
|
||||||
: ['EISDIR']
|
|
||||||
expect(expectedCodes).toContain(code)
|
|
||||||
expect((await stat(inboxPath)).isDirectory()).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('readMailbox fails closed on corrupt mailbox content', async () => {
|
test('readMailbox fails closed on corrupt mailbox content', async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
writeFile,
|
writeFile,
|
||||||
} from 'node:fs/promises'
|
} from 'node:fs/promises'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { createConnection, createServer, type Socket } from 'node:net'
|
import { createConnection, createServer } from 'node:net'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import {
|
import {
|
||||||
@@ -227,134 +227,11 @@ describe('UDS inbox retention', () => {
|
|||||||
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
const { sendToUdsSocket, UdsPeerConnectionError } = await import(
|
const { sendToUdsSocket } = await import('../udsClient.js')
|
||||||
'../udsClient.js'
|
|
||||||
|
await expect(sendToUdsSocket(path, 'hello')).rejects.toThrow(
|
||||||
|
'Failed to connect to peer',
|
||||||
)
|
)
|
||||||
|
|
||||||
const error = await sendToUdsSocket(path, 'hello').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)
|
|
||||||
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<Socket>()
|
|
||||||
const receiver = createServer(socket => {
|
|
||||||
sockets.add(socket)
|
|
||||||
socket.on('close', () => {
|
|
||||||
sockets.delete(socket)
|
|
||||||
})
|
|
||||||
socket.on('data', () => undefined)
|
|
||||||
})
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
receiver.on('error', reject)
|
|
||||||
receiver.listen(path, () => resolve())
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { sendToUdsSocket, UdsPeerConnectionError } = await import(
|
|
||||||
'../udsClient.js'
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = await sendToUdsSocket(path, 'hello', 200).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('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).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
|
|
||||||
try {
|
|
||||||
const { connectToPeer } = await import('../udsClient.js')
|
|
||||||
client = await connectToPeer(path, 50)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
expect(client.destroyed).toBe(false)
|
|
||||||
expect(client.listenerCount('error')).toBe(0)
|
|
||||||
} finally {
|
|
||||||
client?.destroy()
|
|
||||||
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 () => {
|
test('sendUdsMessage fails closed before connecting without an auth token', async () => {
|
||||||
|
|||||||
@@ -36,19 +36,6 @@ export type PeerSession = {
|
|||||||
alive: boolean
|
alive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UdsPeerConnectionError extends Error {
|
|
||||||
readonly socketPath: string
|
|
||||||
|
|
||||||
constructor(socketPath: string, cause: unknown) {
|
|
||||||
super(
|
|
||||||
`Failed to connect to peer at ${socketPath}: ${errorMessage(cause)}`,
|
|
||||||
{ cause },
|
|
||||||
)
|
|
||||||
this.name = 'UdsPeerConnectionError'
|
|
||||||
this.socketPath = socketPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Session directory
|
// Session directory
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -206,7 +193,6 @@ export async function isPeerAlive(
|
|||||||
export async function sendToUdsSocket(
|
export async function sendToUdsSocket(
|
||||||
targetSocketPath: string,
|
targetSocketPath: string,
|
||||||
message: string | Record<string, unknown>,
|
message: string | Record<string, unknown>,
|
||||||
timeoutMs = 5000,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { parseUdsTarget } = await import('./udsMessaging.js')
|
const { parseUdsTarget } = await import('./udsMessaging.js')
|
||||||
const target = parseUdsTarget(targetSocketPath)
|
const target = parseUdsTarget(targetSocketPath)
|
||||||
@@ -251,15 +237,12 @@ export async function sendToUdsSocket(
|
|||||||
maxFrameBytes: MAX_UDS_FRAME_BYTES,
|
maxFrameBytes: MAX_UDS_FRAME_BYTES,
|
||||||
onSettled: finish,
|
onSettled: finish,
|
||||||
formatSocketError: err =>
|
formatSocketError: err =>
|
||||||
new UdsPeerConnectionError(target.socketPath, err),
|
new Error(
|
||||||
})
|
`Failed to connect to peer at ${target.socketPath}: ${errorMessage(err)}`,
|
||||||
conn.setTimeout(timeoutMs, () => {
|
|
||||||
finish(
|
|
||||||
new UdsPeerConnectionError(
|
|
||||||
target.socketPath,
|
|
||||||
new Error('Connection timed out'),
|
|
||||||
),
|
),
|
||||||
)
|
})
|
||||||
|
conn.setTimeout(5000, () => {
|
||||||
|
finish(new Error('Connection timed out'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -268,30 +251,14 @@ export async function sendToUdsSocket(
|
|||||||
* Connect to a peer and return the raw socket for bidirectional communication.
|
* Connect to a peer and return the raw socket for bidirectional communication.
|
||||||
* The caller is responsible for managing the connection lifecycle.
|
* The caller is responsible for managing the connection lifecycle.
|
||||||
*/
|
*/
|
||||||
export function connectToPeer(
|
export function connectToPeer(socketPath: string): Promise<Socket> {
|
||||||
socketPath: string,
|
|
||||||
timeoutMs = 5000,
|
|
||||||
): Promise<Socket> {
|
|
||||||
return new Promise<Socket>((resolve, reject) => {
|
return new Promise<Socket>((resolve, reject) => {
|
||||||
const conn = createConnection(socketPath)
|
const conn = createConnection(socketPath, () => {
|
||||||
let settled = false
|
|
||||||
const fail = (cause: unknown) => {
|
|
||||||
if (settled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
settled = true
|
|
||||||
conn.destroy()
|
|
||||||
reject(new UdsPeerConnectionError(socketPath, cause))
|
|
||||||
}
|
|
||||||
conn.once('connect', () => {
|
|
||||||
settled = true
|
|
||||||
conn.setTimeout(0)
|
|
||||||
conn.off('error', fail)
|
|
||||||
resolve(conn)
|
resolve(conn)
|
||||||
})
|
})
|
||||||
conn.on('error', fail)
|
conn.on('error', reject)
|
||||||
conn.setTimeout(timeoutMs, () => {
|
conn.setTimeout(5000, () => {
|
||||||
fail(new Error('Connection timed out'))
|
conn.destroy(new Error('Connection timed out'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user