test: harden daemon lifecycle and e2e reliability across app/cli/server
This commit is contained in:
@@ -206,6 +206,119 @@ describe('DaemonClient', () => {
|
||||
expect(client.getConnectionState().status).toBe('disposed')
|
||||
})
|
||||
|
||||
test('sends explicit shutdown_server_request via shutdownServer', async () => {
|
||||
const logger = createMockLogger()
|
||||
const mock = createMockTransport()
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: 'ws://test',
|
||||
clientId: 'clsk_unit_test',
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
})
|
||||
clients.push(client)
|
||||
|
||||
const connectPromise = client.connect()
|
||||
mock.triggerOpen()
|
||||
await connectPromise
|
||||
|
||||
const lifecycleClient = client as unknown as {
|
||||
shutdownServer: (requestId?: string) => Promise<{
|
||||
status: 'shutdown_requested'
|
||||
clientId: string
|
||||
requestId: string
|
||||
}>
|
||||
}
|
||||
|
||||
expect(typeof lifecycleClient.shutdownServer).toBe('function')
|
||||
const promise = lifecycleClient.shutdownServer('req-shutdown-1')
|
||||
|
||||
expect(mock.sent).toHaveLength(1)
|
||||
const request = JSON.parse(mock.sent[0]) as {
|
||||
type: 'session'
|
||||
message: {
|
||||
type: string
|
||||
requestId: string
|
||||
}
|
||||
}
|
||||
expect(request.message).toEqual({
|
||||
type: 'shutdown_server_request',
|
||||
requestId: 'req-shutdown-1',
|
||||
})
|
||||
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: 'status',
|
||||
payload: {
|
||||
status: 'shutdown_requested',
|
||||
clientId: 'clsk_unit_test',
|
||||
requestId: 'req-shutdown-1',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
status: 'shutdown_requested',
|
||||
clientId: 'clsk_unit_test',
|
||||
requestId: 'req-shutdown-1',
|
||||
})
|
||||
})
|
||||
|
||||
test('restartServer remains restart-only and sends restart_server_request', async () => {
|
||||
const logger = createMockLogger()
|
||||
const mock = createMockTransport()
|
||||
|
||||
const client = new DaemonClient({
|
||||
url: 'ws://test',
|
||||
clientId: 'clsk_unit_test',
|
||||
logger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => mock.transport,
|
||||
})
|
||||
clients.push(client)
|
||||
|
||||
const connectPromise = client.connect()
|
||||
mock.triggerOpen()
|
||||
await connectPromise
|
||||
|
||||
const promise = client.restartServer('settings_update', 'req-restart-1')
|
||||
|
||||
expect(mock.sent).toHaveLength(1)
|
||||
const request = JSON.parse(mock.sent[0]) as {
|
||||
type: 'session'
|
||||
message: {
|
||||
type: string
|
||||
reason?: string
|
||||
requestId: string
|
||||
}
|
||||
}
|
||||
expect(request.message).toEqual({
|
||||
type: 'restart_server_request',
|
||||
reason: 'settings_update',
|
||||
requestId: 'req-restart-1',
|
||||
})
|
||||
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: 'status',
|
||||
payload: {
|
||||
status: 'restart_requested',
|
||||
clientId: 'clsk_unit_test',
|
||||
reason: 'settings_update',
|
||||
requestId: 'req-restart-1',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
await expect(promise).resolves.toEqual({
|
||||
status: 'restart_requested',
|
||||
clientId: 'clsk_unit_test',
|
||||
reason: 'settings_update',
|
||||
requestId: 'req-restart-1',
|
||||
})
|
||||
})
|
||||
|
||||
test('transitions out of connecting when connect timeout elapses', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AgentRefreshedStatusPayloadSchema,
|
||||
AgentResumedStatusPayloadSchema,
|
||||
RestartRequestedStatusPayloadSchema,
|
||||
ShutdownRequestedStatusPayloadSchema,
|
||||
SessionInboundMessageSchema,
|
||||
type WSWelcomeMessage,
|
||||
WSOutboundMessageSchema,
|
||||
@@ -275,6 +276,7 @@ export type FetchAgentTimelineOptions = {
|
||||
|
||||
type AgentRefreshedStatusPayload = z.infer<typeof AgentRefreshedStatusPayloadSchema>
|
||||
type RestartRequestedStatusPayload = z.infer<typeof RestartRequestedStatusPayloadSchema>
|
||||
type ShutdownRequestedStatusPayload = z.infer<typeof ShutdownRequestedStatusPayloadSchema>
|
||||
type FetchAgentsPayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: 'fetch_agents_response' }
|
||||
@@ -1542,6 +1544,33 @@ export class DaemonClient {
|
||||
})
|
||||
}
|
||||
|
||||
async shutdownServer(requestId?: string): Promise<ShutdownRequestedStatusPayload> {
|
||||
const resolvedRequestId = this.createRequestId(requestId)
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: 'shutdown_server_request',
|
||||
requestId: resolvedRequestId,
|
||||
})
|
||||
return this.sendRequest({
|
||||
requestId: resolvedRequestId,
|
||||
message,
|
||||
timeout: 10000,
|
||||
options: { skipQueue: true },
|
||||
select: (msg) => {
|
||||
if (msg.type !== 'status') {
|
||||
return null
|
||||
}
|
||||
const shutdown = ShutdownRequestedStatusPayloadSchema.safeParse(msg.payload)
|
||||
if (!shutdown.success) {
|
||||
return null
|
||||
}
|
||||
if (shutdown.data.requestId !== resolvedRequestId) {
|
||||
return null
|
||||
}
|
||||
return shutdown.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Audio / Voice
|
||||
// ============================================================================
|
||||
@@ -2203,7 +2232,8 @@ export class DaemonClient {
|
||||
cwd: options?.cwd,
|
||||
},
|
||||
responseType: 'list_provider_models_response',
|
||||
timeout: 30000,
|
||||
// Provider SDK cold starts (especially model discovery) can exceed 30s.
|
||||
timeout: 45000,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user