import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SessionBridge } from './session-bridge.js'; import type { SessionBridgeConfig } from './session-bridge.js'; // Minimal mocks const mockSession = { id: 'test', addMessage: vi.fn(), getHistory: vi.fn(() => []), clear: vi.fn(), replaceHistory: vi.fn(), getConfig: vi.fn((_key: string) => undefined as string | undefined), setConfig: vi.fn(), deleteConfig: vi.fn(), }; const mockSessionManager = { getSession: vi.fn(() => mockSession), listSessions: vi.fn(() => []), transferSession: vi.fn(), closeSession: vi.fn(), }; const mockModelClient = { chat: vi.fn(async () => ({ content: 'test', stopReason: 'end_turn', usage: { inputTokens: 0, outputTokens: 0 }, })), }; const mockToolRegistry = { register: vi.fn(), get: vi.fn(), list: vi.fn(() => []), toAnthropicFormat: vi.fn(() => []), toOpenAIFormat: vi.fn(() => []), }; const mockToolExecutor = { execute: vi.fn(), }; function createBridge(): SessionBridge { return new SessionBridge({ sessionManager: mockSessionManager as unknown as SessionBridgeConfig['sessionManager'], modelClient: mockModelClient, systemPrompt: 'test prompt', toolRegistry: mockToolRegistry as unknown as SessionBridgeConfig['toolRegistry'], toolExecutor: mockToolExecutor as unknown as SessionBridgeConfig['toolExecutor'], }); } function createBridgeWithConfig(config: SessionBridgeConfig['config']): SessionBridge { return new SessionBridge({ sessionManager: mockSessionManager as unknown as SessionBridgeConfig['sessionManager'], modelClient: mockModelClient, systemPrompt: 'test prompt', toolRegistry: mockToolRegistry as unknown as SessionBridgeConfig['toolRegistry'], toolExecutor: mockToolExecutor as unknown as SessionBridgeConfig['toolExecutor'], config, }); } describe('SessionBridge', () => { beforeEach(() => { vi.clearAllMocks(); mockSession.getConfig.mockImplementation((_key: string) => undefined); }); it('connect assigns a connection ID', () => { const bridge = createBridge(); const id = bridge.connect('conn-1'); expect(id).toBe('conn-1'); expect(bridge.connectionCount).toBe(1); }); it('connect auto-generates ID when not provided', () => { const bridge = createBridge(); const id = bridge.connect(); expect(typeof id).toBe('string'); expect(id.length).toBeGreaterThan(0); }); it('getAgent returns agent for connected client', () => { const bridge = createBridge(); bridge.connect('conn-1'); const agent = bridge.getAgent('conn-1'); expect(agent).toBeDefined(); }); it('getSessionId returns session ID for connected client', () => { const bridge = createBridge(); bridge.connect('conn-1'); expect(bridge.getSessionId('conn-1')).toBe('ws:conn-1'); }); it('disconnect removes client but preserves session', () => { const bridge = createBridge(); bridge.connect('conn-1'); bridge.disconnect('conn-1'); expect(bridge.connectionCount).toBe(0); expect(bridge.getAgent('conn-1')).toBeUndefined(); }); it('tracks busy state', () => { const bridge = createBridge(); bridge.connect('conn-1'); expect(bridge.isBusy('conn-1')).toBe(false); bridge.setBusy('conn-1', true); expect(bridge.isBusy('conn-1')).toBe(true); bridge.setBusy('conn-1', false); expect(bridge.isBusy('conn-1')).toBe(false); }); it('cancel returns false when no active operation exists', () => { const bridge = createBridge(); bridge.connect('conn-1'); expect(bridge.cancel('conn-1')).toBe(false); }); it('cancel requests cancellation when connection is busy', () => { const bridge = createBridge(); bridge.connect('conn-1'); const agent = bridge.getAgent('conn-1'); const cancelSpy = vi.spyOn(agent!, 'cancel'); bridge.setBusy('conn-1', true); expect(bridge.cancel('conn-1')).toBe(true); expect(cancelSpy).toHaveBeenCalledTimes(1); }); it('switchSession changes session for a connection', () => { const bridge = createBridge(); bridge.connect('conn-1'); expect(bridge.getSessionId('conn-1')).toBe('ws:conn-1'); bridge.switchSession('conn-1', 'custom-session'); expect(bridge.getSessionId('conn-1')).toBe('custom-session'); }); it('switchSession throws when busy', () => { const bridge = createBridge(); bridge.connect('conn-1'); bridge.setBusy('conn-1', true); expect(() => bridge.switchSession('conn-1', 'other')).toThrow('Cannot switch session while agent is busy'); }); it('switchSession throws for unknown connection', () => { const bridge = createBridge(); expect(() => bridge.switchSession('unknown', 'other')).toThrow('Unknown connection'); }); it('listSessions groups connections by session', () => { const bridge = createBridge(); bridge.connect('conn-1'); bridge.connect('conn-2'); // Switch conn-2 to share conn-1's session bridge.switchSession('conn-2', 'ws:conn-1'); const sessions = bridge.listSessions(); expect(sessions).toEqual([{ sessionId: 'ws:conn-1', connections: 2 }]); }); it('shared sessions keep agent alive when one client disconnects', () => { const bridge = createBridge(); bridge.connect('conn-1'); bridge.connect('conn-2'); bridge.switchSession('conn-2', 'ws:conn-1'); bridge.disconnect('conn-1'); // conn-2 still has the session expect(bridge.getAgent('conn-2')).toBeDefined(); expect(bridge.connectionCount).toBe(1); }); it('loads model tier from per-session config when creating a session agent', () => { mockSession.getConfig.mockImplementation((key: string) => (key === 'modelTier' ? 'local' : undefined)); const bridge = createBridgeWithConfig({ agents: { primary_tier: 'default', delegation: { compaction: 'fast', memory_extraction: 'fast', classification: 'fast', tool_summarisation: 'fast', complex_reasoning: 'complex', }, max_delegation_depth: 3, }, compaction: { enabled: false }, models: { default: { provider: 'anthropic', model: 'claude-3-haiku' } }, } as any); bridge.connect('conn-tier'); const agent = bridge.getAgent('conn-tier'); expect(agent?.getModelTier()).toBe('local'); }); it('keeps different sessions isolated by persisted model tier', () => { const sessionById: Record = {}; const localSessionManager = { ...mockSessionManager, getSession: vi.fn((frontend: string, sessionId: string) => { const fullId = `${frontend}:${sessionId}`; if (!sessionById[fullId]) { const tier = fullId === 'ws:ws:conn-a' ? 'fast' : 'complex'; sessionById[fullId] = { ...mockSession, id: fullId, getConfig: vi.fn((key: string) => (key === 'modelTier' ? tier : undefined)), }; } return sessionById[fullId]; }), }; const bridge = new SessionBridge({ sessionManager: localSessionManager as unknown as SessionBridgeConfig['sessionManager'], modelClient: mockModelClient, systemPrompt: 'test prompt', toolRegistry: mockToolRegistry as unknown as SessionBridgeConfig['toolRegistry'], toolExecutor: mockToolExecutor as unknown as SessionBridgeConfig['toolExecutor'], config: { agents: { primary_tier: 'default', delegation: { compaction: 'fast', memory_extraction: 'fast', classification: 'fast', tool_summarisation: 'fast', complex_reasoning: 'complex', }, max_delegation_depth: 3, }, compaction: { enabled: false }, models: { default: { provider: 'anthropic', model: 'claude-3-haiku' } }, } as any, }); bridge.connect('conn-a'); bridge.connect('conn-b'); const agentA = bridge.getAgent('conn-a'); const agentB = bridge.getAgent('conn-b'); expect(agentA?.getModelTier()).toBe('fast'); expect(agentB?.getModelTier()).toBe('complex'); }); });