import { describe, test, expect, beforeEach, mock } from 'bun:test' import { mockConfigModule } from './helpers/mock-config' // Mock config — wsClientInactivityTimeout is read at module load time. mock.module('../config', () => mockConfigModule({ pollTimeout: 1, wsClientInactivityTimeout: 300, }), ) import { storeReset, storeCreateEnvironment, storeCreateSession, storeGetEnvironment, storeGetSession, } from '../store' import { getAllEventBuses, removeEventBus, removeAcpEventBus, } from '../transport/event-bus' import { handleAcpWsOpen, handleAcpWsMessage, handleAcpWsClose, } from '../transport/acp-ws-handler' function createMockWs() { return { readyState: 1, send: mock(() => {}), close: mock(() => {}), } as any } describe('ACP WS handler — shutdown signal', () => { beforeEach(() => { storeReset() for (const [key] of getAllEventBuses()) { removeEventBus(key) } }) test('shutdown message archives sessions and marks env disconnected immediately', () => { const env = storeCreateEnvironment({ secret: 's', workerType: 'acp', machineName: 'test-agent', bridgeId: 'group_shutdown_test', }) const session = storeCreateSession({ environmentId: env.id, title: 'ACP Agent', source: 'acp', }) const ws = createMockWs() const wsId = 'acp_ws_shutdown_test' handleAcpWsOpen(ws, wsId) // Bind WS → env via identify handleAcpWsMessage( ws, wsId, JSON.stringify({ type: 'identify', agent_id: env.id }), ) expect(storeGetEnvironment(env.id)?.status).toBe('active') // Agent sends shutdown before closing handleAcpWsMessage(ws, wsId, JSON.stringify({ type: 'shutdown' })) // Session archived immediately — no 5-min grace period expect(storeGetSession(session.id)?.status).toBe('archived') // Env marked disconnected (not offline — skips grace period) expect(storeGetEnvironment(env.id)?.status).toBe('disconnected') // WS close fires after shutdown — env must STAY disconnected, // not revert to offline (which would re-trigger the grace path) handleAcpWsClose(ws, wsId, 1000, 'client shutdown') expect(storeGetEnvironment(env.id)?.status).toBe('disconnected') removeAcpEventBus('group_shutdown_test') }) test('close without shutdown marks env offline (grace period path)', () => { const env = storeCreateEnvironment({ secret: 's', workerType: 'acp', machineName: 'test-agent', bridgeId: 'group_close_test', }) const ws = createMockWs() const wsId = 'acp_ws_close_test' handleAcpWsOpen(ws, wsId) handleAcpWsMessage( ws, wsId, JSON.stringify({ type: 'identify', agent_id: env.id }), ) expect(storeGetEnvironment(env.id)?.status).toBe('active') // Close WITHOUT prior shutdown — transient drop / crash scenario handleAcpWsClose(ws, wsId, 1006, 'network drop') // Env goes offline so the disconnect monitor's grace period applies // (gives acp-link a window to reconnect and reuse the env) expect(storeGetEnvironment(env.id)?.status).toBe('offline') removeAcpEventBus('group_close_test') }) test('shutdown before identify is a no-op (no agentId bound yet)', () => { const ws = createMockWs() const wsId = 'acp_ws_pre_identify' handleAcpWsOpen(ws, wsId) // Shutdown before identify — entry.agentId is still null handleAcpWsMessage(ws, wsId, JSON.stringify({ type: 'shutdown' })) // No crash, no state change (nothing to archive) handleAcpWsClose(ws, wsId, 1000, 'client shutdown') }) })