feat: implement daemon connectivity updates and relay protocol versioning

This commit is contained in:
Mohamed Boudra
2026-02-20 19:27:56 +07:00
parent 6dad3212da
commit 0350f00840
49 changed files with 4146 additions and 1351 deletions
+207 -33
View File
@@ -83,7 +83,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -166,12 +166,177 @@ describe('DaemonClient', () => {
})
})
test('does not reconnect after close when ensureConnected is called', async () => {
const logger = createMockLogger()
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
})
clients.push(client)
const connectPromise = client.connect()
mock.triggerOpen()
await connectPromise
expect(client.getConnectionState().status).toBe('connected')
await client.close()
expect(client.getConnectionState().status).toBe('disposed')
client.ensureConnected()
expect(client.getConnectionState().status).toBe('disposed')
})
test('transitions out of connecting when connect timeout elapses', async () => {
vi.useFakeTimers()
try {
const logger = createMockLogger()
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
connectTimeoutMs: 100,
transportFactory: () => mock.transport,
})
clients.push(client)
const pendingConnect = client.connect().then(
() => ({ ok: true as const }),
(error) => ({ ok: false as const, error })
)
expect(client.getConnectionState().status).toBe('connecting')
await vi.advanceTimersByTimeAsync(120)
const result = await pendingConnect
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.error).toBeInstanceOf(Error)
expect((result.error as Error).message).toContain('Connection timed out')
}
expect(client.getConnectionState().status).toBe('disconnected')
} finally {
vi.useRealTimers()
}
})
test('reconnects after relay close with replaced-by-new-connection reason', async () => {
vi.useFakeTimers()
try {
const logger = createMockLogger()
const first = createMockTransport()
const second = createMockTransport()
const transports = [first, second]
let transportIndex = 0
const client = new DaemonClient({
url: 'ws://relay.test/ws?role=client&serverId=srv_test&clientId=clsk_test&v=2',
logger,
reconnect: {
enabled: true,
baseDelayMs: 5,
maxDelayMs: 5,
},
transportFactory: () => {
const next = transports[Math.min(transportIndex, transports.length - 1)]
transportIndex += 1
return next.transport
},
})
clients.push(client)
const connectPromise = client.connect()
first.triggerOpen()
await connectPromise
expect(client.getConnectionState().status).toBe('connected')
first.triggerClose({ code: 1008, reason: 'Replaced by new connection' })
expect(client.getConnectionState().status).toBe('disconnected')
await vi.advanceTimersByTimeAsync(10)
expect(client.getConnectionState().status).toBe('connecting')
second.triggerOpen()
expect(client.getConnectionState().status).toBe('connected')
} finally {
vi.useRealTimers()
}
})
test('requires explicit relay session identity when URL clientId is missing', () => {
expect(() => {
new DaemonClient({
url: 'ws://relay.test/ws?role=client&serverId=srv_test&v=2',
reconnect: { enabled: false },
})
}).toThrow('Relay client requires clientSessionKey or URL clientId')
})
test('rejects relay identity mismatch between URL clientId and clientSessionKey', () => {
expect(() => {
new DaemonClient({
url: 'ws://relay.test/ws?role=client&serverId=srv_test&clientId=clsk_a&v=2',
clientSessionKey: 'clsk_b',
reconnect: { enabled: false },
})
}).toThrow('Relay clientId and clientSessionKey must match when both are provided')
})
test('requires explicit direct session identity when URL clientSessionKey is missing', () => {
expect(() => {
new DaemonClient({
url: 'ws://127.0.0.1:6767/ws',
reconnect: { enabled: false },
})
}).toThrow('Direct client requires clientSessionKey or URL clientSessionKey')
})
test('rejects direct identity mismatch between URL and config clientSessionKey', () => {
expect(() => {
new DaemonClient({
url: 'ws://127.0.0.1:6767/ws?clientSessionKey=clsk_a',
clientSessionKey: 'clsk_b',
reconnect: { enabled: false },
})
}).toThrow('Direct URL clientSessionKey and config clientSessionKey must match when both are provided')
})
test('logs configured runtime generation in connection transition events', async () => {
const logger = createMockLogger()
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
runtimeGeneration: 7,
transportFactory: () => mock.transport,
})
clients.push(client)
const connectPromise = client.connect()
mock.triggerOpen()
await connectPromise
const transitionPayloads = logger.info.mock.calls
.filter(([, message]) => message === 'DaemonClientTransition')
.map(([payload]) => payload as { generation?: number | null })
expect(transitionPayloads.length).toBeGreaterThan(0)
for (const payload of transitionPayloads) {
expect(payload.generation).toBe(7)
}
})
test('subscribes to checkout diff updates via RPC handshake', async () => {
const logger = createMockLogger()
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -234,7 +399,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -302,7 +467,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -361,7 +526,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -432,7 +597,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -494,7 +659,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -540,7 +705,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -621,7 +786,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -664,7 +829,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -716,7 +881,7 @@ describe('DaemonClient', () => {
})
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory,
@@ -743,7 +908,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -797,7 +962,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -872,7 +1037,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -928,7 +1093,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -974,7 +1139,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1016,7 +1181,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1037,7 +1202,12 @@ describe('DaemonClient', () => {
})
)
expect(mock.sent).toHaveLength(0)
expect(mock.sent).toHaveLength(1)
const bufferedAck = decodeBinaryMuxFrame(asUint8Array(mock.sent[0])!)
expect(bufferedAck?.messageType).toBe(TerminalBinaryMessageType.Ack)
expect(bufferedAck?.streamId).toBe(19)
expect(bufferedAck?.offset).toBe(8)
mock.sent.length = 0
const seen: string[] = []
client.onTerminalStreamData(19, (chunk) => {
@@ -1045,11 +1215,7 @@ describe('DaemonClient', () => {
})
expect(seen.join('')).toContain('buffered')
expect(mock.sent).toHaveLength(1)
const ackFrame = decodeBinaryMuxFrame(asUint8Array(mock.sent[0])!)
expect(ackFrame?.messageType).toBe(TerminalBinaryMessageType.Ack)
expect(ackFrame?.streamId).toBe(19)
expect(ackFrame?.offset).toBe(8)
expect(mock.sent).toHaveLength(0)
})
test('stops delivering and acking after terminal_stream_exit', async () => {
@@ -1057,7 +1223,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1106,7 +1272,11 @@ describe('DaemonClient', () => {
)
expect(seen).toEqual(['before-exit'])
expect(mock.sent).toHaveLength(0)
expect(mock.sent).toHaveLength(1)
const postExitAck = decodeBinaryMuxFrame(asUint8Array(mock.sent[0])!)
expect(postExitAck?.messageType).toBe(TerminalBinaryMessageType.Ack)
expect(postExitAck?.streamId).toBe(23)
expect(postExitAck?.offset).toBe(22)
unsubscribe()
})
@@ -1115,7 +1285,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1165,7 +1335,11 @@ describe('DaemonClient', () => {
)
expect(seen).toEqual([])
expect(mock.sent).toHaveLength(0)
expect(mock.sent).toHaveLength(1)
const detachedAck = decodeBinaryMuxFrame(asUint8Array(mock.sent[0])!)
expect(detachedAck?.messageType).toBe(TerminalBinaryMessageType.Ack)
expect(detachedAck?.streamId).toBe(31)
expect(detachedAck?.offset).toBe(12)
})
test('parses canonical agent_stream tool_call payloads without crashing', async () => {
@@ -1173,7 +1347,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1244,7 +1418,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1296,7 +1470,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1388,7 +1562,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1458,7 +1632,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,
@@ -1494,7 +1668,7 @@ describe('DaemonClient', () => {
const mock = createMockTransport()
const client = new DaemonClient({
url: 'ws://test',
url: 'ws://test?clientSessionKey=clsk_unit_test',
logger,
reconnect: { enabled: false },
transportFactory: () => mock.transport,