import { beforeEach, describe, expect, it, vi } from 'vitest' const authMocks = vi.hoisted(() => ({ authenticateUserToken: vi.fn(), userCanAccessProfile: vi.fn(), })) const clientSocketMocks = vi.hoisted(() => { function createLocalSocket(id: string) { const handlers = new Map void>() const socket: any = { id, __handlers: handlers, on: vi.fn((event: string, handler: (...args: any[]) => void) => { handlers.set(event, handler) return socket }), emit: vi.fn(), removeAllListeners: vi.fn(), disconnect: vi.fn(), } return socket } const localSockets: any[] = [] const clientIo = vi.fn(() => { const socket = createLocalSocket(`local-socket-${localSockets.length + 1}`) localSockets.push(socket) return socket }) const reset = () => { localSockets.length = 0 clientIo.mockClear() } return { clientIo, localSockets, reset, } }) const chatRunMocks = vi.hoisted(() => ({ getChatRunServer: vi.fn(), clearSessionHistory: vi.fn(), })) vi.mock('../../packages/server/src/middleware/user-auth', () => ({ authenticateUserToken: authMocks.authenticateUserToken, })) vi.mock('../../packages/server/src/db/hermes/users-store', () => ({ userCanAccessProfile: authMocks.userCanAccessProfile, })) vi.mock('socket.io-client', () => ({ io: clientSocketMocks.clientIo, })) vi.mock('../../packages/server/src/routes/hermes/chat-run', () => ({ getChatRunServer: chatRunMocks.getChatRunServer, })) function createMockNamespace() { const middleware: Array<(socket: any, next: (err?: Error) => void) => void> = [] const handlers = new Map void>() const nsp: any = { use: vi.fn((fn: (socket: any, next: (err?: Error) => void) => void) => { middleware.push(fn) return nsp }), on: vi.fn((event: string, handler: (...args: any[]) => void) => { handlers.set(event, handler) return nsp }), emit: vi.fn(), __middleware: middleware, __handlers: handlers, } return nsp } function createMockSocket(id: string, auth: Record = {}, headers: Record = {}) { const handlers = new Map void>() const socket: any = { id, data: {}, handshake: { auth, headers }, broadcast: { emit: vi.fn() }, on: vi.fn((event: string, handler: (...args: any[]) => void) => { handlers.set(event, handler) return socket }), emit: vi.fn((_event: string, payload: unknown, ack?: (response: unknown) => void) => { ack?.({ id: (payload as any)?.id, status: 200, body: '{"ok":true}' }) return socket }), disconnect: vi.fn(), __handlers: handlers, } return socket } async function waitForMockCalls(mock: { mock: { calls: unknown[] } }, count: number): Promise { const startedAt = Date.now() while (mock.mock.calls.length < count && Date.now() - startedAt < 1000) { await new Promise(resolve => setTimeout(resolve, 5)) } } async function waitForMockCallWith( mock: { mock: { calls: unknown[][] } }, predicate: (call: unknown[]) => boolean, ): Promise { const startedAt = Date.now() while (Date.now() - startedAt < 1000) { if (mock.mock.calls.some(call => predicate(call))) return await new Promise(resolve => setTimeout(resolve, 5)) } throw new Error('Timed out waiting for matching mock call') } describe('GlobalAgentServer', () => { beforeEach(() => { vi.resetModules() vi.clearAllMocks() clientSocketMocks.reset() chatRunMocks.getChatRunServer.mockReturnValue({ clearSessionHistory: chatRunMocks.clearSessionHistory, }) chatRunMocks.clearSessionHistory.mockReturnValue({ deleted: 2, hadMemoryState: true }) authMocks.authenticateUserToken.mockResolvedValue(null) authMocks.userCanAccessProfile.mockReturnValue(false) }) it('registers a local control namespace with token auth', async () => { const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() expect(io.of).toHaveBeenCalledWith('/global-agent') expect(nsp.use).toHaveBeenCalledTimes(1) expect(nsp.on).toHaveBeenCalledWith('connection', expect.any(Function)) const denied = createMockSocket('socket-denied', { token: 'wrong' }) const deniedNext = vi.fn() await nsp.__middleware[0](denied, deniedNext) expect(deniedNext.mock.calls[0][0]).toBeInstanceOf(Error) const deniedAgent = createMockSocket('socket-denied-agent', { token: 'wrong', role: 'hermes-studio' }) const deniedAgentNext = vi.fn() await nsp.__middleware[0](deniedAgent, deniedAgentNext) expect(deniedAgentNext.mock.calls[0][0]).toBeInstanceOf(Error) const allowed = createMockSocket('socket-allowed', { token: server.getAuthToken() }) const allowedNext = vi.fn() await nsp.__middleware[0](allowed, allowedNext) expect(allowedNext).toHaveBeenCalledWith() }) it('tracks connected clients and forwards requests with ack', async () => { const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const socket = createMockSocket('socket-1', { token: server.getAuthToken(), instanceId: 'local-global-agent' }) nsp.__handlers.get('connection')?.(socket) expect(server.getClientIds()).toEqual(['local-global-agent']) const response = await server.httpRequest({ id: 'req-1', method: 'GET', path: '/api/auth/users', }, { clientId: 'local-global-agent' }) expect(socket.emit).toHaveBeenCalledWith( 'http.request', { id: 'req-1', method: 'GET', path: '/api/auth/users' }, expect.any(Function), ) expect(response).toEqual({ id: 'req-1', status: 200, body: '{"ok":true}' }) }) it('accepts frontend JWT clients and injects their token and profile into forwarded requests', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('agent-socket', { token: server.getAuthToken(), instanceId: 'local-global-agent' }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) const frontendSocket = createMockSocket('frontend-socket', { token: 'frontend-jwt', profile: 'research' }) await new Promise((resolve, reject) => { nsp.__middleware[0](frontendSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(frontendSocket) const httpAck = vi.fn() frontendSocket.__handlers.get('http.request')?.({ id: 'req-frontend', method: 'GET', path: '/api/auth/users', clientId: 'local-global-agent', }, httpAck) await new Promise(resolve => setTimeout(resolve, 0)) expect(agentSocket.emit).toHaveBeenCalledWith( 'http.request', { id: 'req-frontend', method: 'GET', path: '/api/auth/users', clientId: 'local-global-agent', headers: { authorization: 'Bearer frontend-jwt', 'x-hermes-profile': 'research', }, }, expect.any(Function), ) expect(httpAck).toHaveBeenCalledWith({ id: 'req-frontend', status: 200, body: '{"ok":true}' }) const openAck = vi.fn() frontendSocket.__handlers.get('socket.open')?.({ id: 'chat-1', namespace: '/chat-run', clientId: 'local-global-agent', }, openAck) await new Promise(resolve => setTimeout(resolve, 0)) expect(agentSocket.emit).toHaveBeenCalledWith( 'socket.open', { id: 'chat-1', namespace: '/chat-run', clientId: 'local-global-agent', auth: { token: 'frontend-jwt' }, query: { profile: 'research' }, }, expect.any(Function), ) }) it('accepts JWT agent clients and handles inbound HTTP and chat-run socket relay requests locally', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 202, headers: { 'content-type': 'application/json', 'x-result': 'accepted' }, })) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { localBaseUrl: 'http://127.0.0.1:8648', fetchImpl: fetchImpl as any, }) server.init() const agentSocket = createMockSocket('jwt-agent-socket', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) expect(server.getClientIds()).toEqual(['device-1']) const httpAck = vi.fn() agentSocket.__handlers.get('http.request')?.({ id: 'req-1', method: 'POST', path: '/api/hermes/sessions', headers: { authorization: 'Bearer attacker', 'content-type': 'application/json', }, body: { title: 'hello' }, }, httpAck) await new Promise(resolve => setTimeout(resolve, 0)) expect(fetchImpl).toHaveBeenCalledWith('http://127.0.0.1:8648/api/hermes/sessions', expect.objectContaining({ method: 'POST', body: JSON.stringify({ title: 'hello' }), })) expect(Array.from((fetchImpl.mock.calls[0][1].headers as Headers).entries())).toEqual([ ['authorization', 'Bearer user-jwt'], ['content-type', 'application/json'], ['x-hermes-profile', 'research'], ]) expect(httpAck).toHaveBeenCalledWith(expect.objectContaining({ id: 'req-1', status: 202, body: '{"ok":true}', })) const openAck = vi.fn() agentSocket.__handlers.get('socket.open')?.({ id: 'chat-1', namespace: '/chat-run', }, openAck) expect(clientSocketMocks.clientIo).toHaveBeenCalledWith('http://127.0.0.1:8648/chat-run', expect.objectContaining({ auth: { token: 'user-jwt' }, query: { profile: 'research' }, transports: ['websocket', 'polling'], })) expect(openAck).toHaveBeenCalledWith({ id: 'chat-1', ok: true, namespace: '/chat-run', stream: true }) const localSocket = clientSocketMocks.localSockets[0] const eventAck = vi.fn() agentSocket.__handlers.get('socket.event')?.({ id: 'chat-1', event: 'run', payload: { session_id: 's1', input: 'hi' }, }, eventAck) expect(localSocket.emit).toHaveBeenCalledWith('run', { session_id: 's1', input: 'hi', profile: 'research', }) expect(eventAck).toHaveBeenCalledWith({ id: 'chat-1', ok: true, namespace: '/chat-run', event: 'run', stream: true }) localSocket.__handlers.get('message.delta')?.({ session_id: 's1', delta: 'hello' }) expect(agentSocket.emit).toHaveBeenCalledWith('socket.event', { id: 'chat-1', namespace: '/chat-run', event: 'message.delta', payload: { session_id: 's1', delta: 'hello' }, }) }) it('replaces the previous agent socket for the same instance id', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { localBaseUrl: 'http://127.0.0.1:8648' }) server.init() const first = createMockSocket('agent-old', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](first, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(first) first.__handlers.get('socket.open')?.({ id: 'chat-1', namespace: '/chat-run' }, vi.fn()) expect(clientSocketMocks.localSockets).toHaveLength(1) const second = createMockSocket('agent-new', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](second, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(second) expect(first.disconnect).toHaveBeenCalledWith(true) expect(clientSocketMocks.localSockets[0].disconnect).toHaveBeenCalled() expect(server.getClientIds()).toEqual(['device-1']) }) it('pushes MCU events only to the selected agent socket and forwards MCU status events to frontends', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('jwt-agent-socket', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) const otherAgentSocket = createMockSocket('jwt-agent-socket-2', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-2', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](otherAgentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(otherAgentSocket) expect(server.emitMcuEvent({ type: 'audio.enqueue', interactionId: 'voice-1', segmentId: 'voice-1-tts-1', url: 'http://127.0.0.1/audio.pcm', }, { clientId: 'device-1' })).toBe(true) expect(agentSocket.emit).toHaveBeenCalledWith('audio.enqueue', { type: 'audio.enqueue', interactionId: 'voice-1', segmentId: 'voice-1-tts-1', url: 'http://127.0.0.1/audio.pcm', }) expect(otherAgentSocket.emit).not.toHaveBeenCalledWith('audio.enqueue', expect.anything()) expect(agentSocket.broadcast.emit).not.toHaveBeenCalled() agentSocket.__handlers.get('audio.queued')?.({ interactionId: 'voice-1', segmentId: 'voice-1-tts-1', }) expect(agentSocket.broadcast.emit).not.toHaveBeenCalled() expect(otherAgentSocket.emit).not.toHaveBeenCalledWith('relay.socket.event', expect.anything()) }) it('keeps MCU voice stream in listening state until the upload is ended', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('jwt-agent-socket', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) agentSocket.__handlers.get('voice.stream.start')?.({ interactionId: 'voice-1', sampleRate: 16000, channels: 1, bitsPerSample: 16, }) expect(agentSocket.emit).toHaveBeenCalledWith('interaction.status', { type: 'interaction.status', interactionId: 'voice-1', status: 'listening', }) expect(agentSocket.emit).not.toHaveBeenCalledWith('interaction.status', expect.objectContaining({ interactionId: 'voice-1', status: 'transcribing', })) }) it('returns relative MCU audio URLs for device-side playback', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const fetchImpl = vi.fn(async () => new Response(Buffer.from('pcm-audio'), { status: 200, headers: { 'Content-Type': 'audio/x-pcm' }, })) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { fetchImpl: fetchImpl as any, localBaseUrl: 'http://127.0.0.1:8647', }) server.init() const audio = await (server as any).synthesizeMcuSpeech('hello', 'user-jwt', 'research') expect(audio.url).toMatch(/^\/api\/hermes\/mcu\/audio\/[a-f0-9-]+\.pcm$/) expect(fetchImpl.mock.calls[0][1]?.headers).toMatchObject({ 'X-Hermes-Profile': 'research', }) }) it('marks TTS requests as MCU playback without forcing every provider to PCM', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const fetchImpl = vi.fn(async () => new Response(Buffer.from('pcm-audio'), { status: 200, headers: { 'Content-Type': 'audio/x-pcm' }, })) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { fetchImpl: fetchImpl as any, localBaseUrl: 'http://127.0.0.1:8647', }) server.init() await (server as any).synthesizeMcuSpeech('hello', 'user-jwt', 'research') expect(fetchImpl.mock.calls[0][1]?.headers).toMatchObject({ 'X-Hermes-Profile': 'research', }) expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toMatchObject({ text: 'hello', options: { mcuPlayback: true, sampleRate: 24000, }, }) }) it('falls back to Edge TTS when active MCU TTS output cannot be converted to PCM', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => { const body = JSON.parse(String(init?.body || '{}')) if (body.provider === 'edge') { return new Response(Buffer.from('edge-pcm'), { status: 200, headers: { 'Content-Type': 'audio/x-pcm' }, }) } return new Response(Buffer.from('not-real-mp3'), { status: 200, headers: { 'Content-Type': 'audio/mpeg' }, }) }) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { fetchImpl: fetchImpl as any, localBaseUrl: 'http://127.0.0.1:8647', }) server.init() const audio = await (server as any).synthesizeMcuSpeech('hello', 'user-jwt', 'research') expect(audio.url).toMatch(/^\/api\/hermes\/mcu\/audio\/[a-f0-9-]+\.pcm$/) expect(fetchImpl).toHaveBeenCalledTimes(2) expect(fetchImpl.mock.calls[1][1]?.headers).toMatchObject({ 'X-Hermes-Profile': 'research', }) expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toMatchObject({ provider: 'edge', text: 'hello', options: { mcuPlayback: true, sampleRate: 24000, }, }) }) it('falls back to Edge TTS when the active MCU TTS provider fails', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => { const body = JSON.parse(String(init?.body || '{}')) if (body.provider === 'edge') { return new Response(Buffer.from('edge-pcm'), { status: 200, headers: { 'Content-Type': 'audio/x-pcm' }, }) } return new Response(JSON.stringify({ error: 'provider down' }), { status: 502, headers: { 'Content-Type': 'application/json' }, }) }) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { fetchImpl: fetchImpl as any, localBaseUrl: 'http://127.0.0.1:8647', }) server.init() const audio = await (server as any).synthesizeMcuSpeech('hello', 'user-jwt', 'research') expect(audio.url).toMatch(/^\/api\/hermes\/mcu\/audio\/[a-f0-9-]+\.pcm$/) expect(fetchImpl).toHaveBeenCalledTimes(2) expect(fetchImpl.mock.calls[1][1]?.headers).toMatchObject({ 'X-Hermes-Profile': 'research', }) expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toMatchObject({ provider: 'edge', text: 'hello', options: { mcuPlayback: true, sampleRate: 24000, }, }) }) it('synthesizes MCU speech at completed assistant-message boundaries', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const fetchImpl = vi.fn(async () => new Response(Buffer.from('pcm-audio'), { status: 200, headers: { 'Content-Type': 'audio/x-pcm' }, })) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { fetchImpl: fetchImpl as any, localBaseUrl: 'http://127.0.0.1:8647', }) server.init() const agentSocket = createMockSocket('jwt-agent-socket', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) server.startMcuVoiceChatTurn({ userToken: 'user-jwt', profile: 'research', interactionId: 'voice-1', transcript: 'hi', clientId: 'device-1', }) const localSocket = clientSocketMocks.localSockets.at(-1) localSocket.__handlers.get('connect')?.() localSocket.__handlers.get('message.delta')?.({ delta: '好嘞,这就去查。' }) expect(fetchImpl).not.toHaveBeenCalled() localSocket.__handlers.get('tool.started')?.({ tool: 'weather', preview: '厦门天气' }) await waitForMockCalls(fetchImpl, 1) expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toMatchObject({ text: '好嘞,这就去查。', }) await waitForMockCallWith(agentSocket.emit, ([event, payload]) => event === 'audio.enqueue' && (payload as { segmentId?: string })?.segmentId === 'voice-1-tts-1', ) expect(agentSocket.emit).toHaveBeenCalledWith('audio.enqueue', expect.objectContaining({ interactionId: 'voice-1', segmentId: 'voice-1-tts-1', url: expect.stringMatching(/^\/api\/hermes\/mcu\/audio\/[a-f0-9-]+\.pcm$/), completionManagedByServer: true, })) agentSocket.__handlers.get('audio.done')?.({ interactionId: 'voice-1', segmentId: 'voice-1-tts-1', }) localSocket.__handlers.get('tool.completed')?.({ tool: 'weather' }) localSocket.__handlers.get('message.delta')?.({ delta: '结果如下:\n| 名称 | 值 |\n' }) localSocket.__handlers.get('message.delta')?.({ delta: '| --- | --- |\n| foo | 1 |\n请确认。' }) localSocket.__handlers.get('run.completed')?.({}) await waitForMockCalls(fetchImpl, 2) expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toMatchObject({ text: '结果如下: 请确认。', }) await waitForMockCallWith(agentSocket.emit, ([event, payload]) => event === 'audio.enqueue' && (payload as { segmentId?: string })?.segmentId === 'voice-1-tts-2', ) expect(agentSocket.emit).toHaveBeenCalledWith('audio.enqueue', expect.objectContaining({ interactionId: 'voice-1', segmentId: 'voice-1-tts-2', url: expect.stringMatching(/^\/api\/hermes\/mcu\/audio\/[a-f0-9-]+\.pcm$/), completionManagedByServer: true, })) }) it('aborts in-flight MCU TTS synthesis when the device interrupts playback', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) let ttsSignal: AbortSignal | undefined const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => { ttsSignal = init?.signal || undefined return await new Promise((_resolve, reject) => { ttsSignal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true }) }) }) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { fetchImpl: fetchImpl as any, localBaseUrl: 'http://127.0.0.1:8647', }) server.init() const agentSocket = createMockSocket('jwt-agent-socket', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) server.startMcuVoiceChatTurn({ userToken: 'user-jwt', profile: 'research', interactionId: 'voice-abort', transcript: 'hi', clientId: 'device-1', }) const localSocket = clientSocketMocks.localSockets.at(-1) localSocket.__handlers.get('connect')?.() localSocket.__handlers.get('message.delta')?.({ delta: '这段正在合成。' }) await waitForMockCalls(fetchImpl, 1) expect(ttsSignal?.aborted).toBe(false) agentSocket.__handlers.get('audio.interrupted')?.({ interactionId: 'voice-abort', segmentId: 'voice-abort-tts-1', }) await vi.waitFor(() => { expect(ttsSignal?.aborted).toBe(true) }) expect(localSocket.emit).toHaveBeenCalledWith('abort', { session_id: 'mcu-device-1-research' }) expect(agentSocket.emit).not.toHaveBeenCalledWith('audio.enqueue', expect.anything()) }) it('auto-approves MCU chat-run approval requests using the same choice order as the client relay', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any, { localBaseUrl: 'http://127.0.0.1:8647' }) server.init() const agentSocket = createMockSocket('jwt-agent-socket', { token: 'user-jwt', role: 'hermes-studio', instanceId: 'device-1', profile: 'research', }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) server.startMcuVoiceChatTurn({ userToken: 'user-jwt', profile: 'research', interactionId: 'voice-1', transcript: '查天气', clientId: 'device-1', }) const localSocket = clientSocketMocks.localSockets.at(-1) localSocket.__handlers.get('connect')?.() localSocket.__handlers.get('approval.requested')?.({ approval_id: 'approval-1', choices: ['once', 'session', 'deny'], }) expect(agentSocket.emit).toHaveBeenCalledWith('tool.started', { type: 'tool.started', interactionId: 'voice-1', tool: 'approval', preview: 'session', }) expect(localSocket.emit).toHaveBeenCalledWith('approval.respond', { session_id: 'mcu-device-1-research', approval_id: 'approval-1', choice: 'session', }) localSocket.__handlers.get('approval.resolved')?.({ resolved: true }) expect(agentSocket.emit).toHaveBeenCalledWith('tool.completed', { type: 'tool.completed', interactionId: 'voice-1', tool: 'approval', error: undefined, }) localSocket.__handlers.get('tool.failed')?.({ tool: 'weather', error: 'permission denied' }) expect(agentSocket.emit).toHaveBeenCalledWith('tool.completed', { type: 'tool.completed', interactionId: 'voice-1', tool: 'weather', preview: undefined, error: 'permission denied', }) localSocket.__handlers.get('run.failed')?.({ error: 'done' }) }) it('does not forward reserved Socket.IO lifecycle events to frontend clients', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('agent-socket', { token: server.getAuthToken(), instanceId: 'local-global-agent' }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) const frontendSocket = createMockSocket('frontend-socket', { token: 'frontend-jwt', profile: 'research' }) await new Promise((resolve, reject) => { nsp.__middleware[0](frontendSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(frontendSocket) const bridgeId = 'frontend:frontend-socket:chat-run' const openAck = vi.fn() frontendSocket.__handlers.get('socket.open')?.({ id: bridgeId, namespace: '/chat-run', clientId: 'local-global-agent', }, openAck) await new Promise(resolve => setTimeout(resolve, 0)) frontendSocket.emit.mockClear() agentSocket.__handlers.get('socket.event')?.({ id: bridgeId, namespace: '/chat-run', event: 'connect', payload: { socketId: 'local-chat-run' }, }) expect(frontendSocket.emit).not.toHaveBeenCalledWith('connect', expect.anything()) agentSocket.__handlers.get('socket.event')?.({ id: bridgeId, namespace: '/chat-run', event: 'message.delta', payload: { session_id: 's1', delta: 'hi' }, }) expect(frontendSocket.emit).toHaveBeenCalledWith('message.delta', { session_id: 's1', delta: 'hi' }) }) it('broadcasts MCU session clears as chat session commands for frontend clients', async () => { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('agent-socket', { token: server.getAuthToken(), instanceId: 'device-1' }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) const frontendSocket = createMockSocket('frontend-socket', { token: 'frontend-jwt', profile: 'research' }) await new Promise((resolve, reject) => { nsp.__middleware[0](frontendSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(frontendSocket) agentSocket.__handlers.get('mcu.session.clear')?.({ interactionId: 'clear-1', profile: 'research', }) expect(chatRunMocks.clearSessionHistory).toHaveBeenCalledWith('mcu-device-1-research') expect(agentSocket.emit).toHaveBeenCalledWith('mcu.session.cleared', expect.objectContaining({ type: 'mcu.session.cleared', interactionId: 'clear-1', profile: 'research', sessionId: 'mcu-device-1-research', deleted: 2, memoryCleared: true, })) expect(frontendSocket.emit).toHaveBeenCalledWith('session.command', { event: 'session.command', session_id: 'mcu-device-1-research', command: 'clear', action: 'clear', clearHistory: true, ok: true, deleted: 2, memoryCleared: true, }) }) it('cancels a pending MCU interrupt when session clear arrives in the double-click window', async () => { vi.useFakeTimers() try { authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('agent-socket', { token: server.getAuthToken(), instanceId: 'device-1' }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) const runSocket = { emit: vi.fn() } ;(server as any).mcuSessionRuns.set('mcu-device-1-research', { interactionId: 'run-1', socket: runSocket, }) agentSocket.__handlers.get('mcu.interrupt')?.({ interactionId: 'run-1', profile: 'research', }) expect(runSocket.emit).not.toHaveBeenCalled() agentSocket.__handlers.get('mcu.session.clear')?.({ interactionId: 'clear-1', profile: 'research', }) await vi.advanceTimersByTimeAsync(300) expect(runSocket.emit).not.toHaveBeenCalled() expect(chatRunMocks.clearSessionHistory).toHaveBeenCalledWith('mcu-device-1-research') expect(agentSocket.emit).toHaveBeenCalledWith('mcu.session.cleared', expect.objectContaining({ type: 'mcu.session.cleared', interactionId: 'clear-1', profile: 'research', sessionId: 'mcu-device-1-research', })) } finally { vi.useRealTimers() } }) it('does not silently clear only the database when chat-run memory is unavailable', async () => { chatRunMocks.getChatRunServer.mockReturnValue(null) authMocks.authenticateUserToken.mockResolvedValue({ id: 7, username: 'ada', role: 'user' }) authMocks.userCanAccessProfile.mockReturnValue(true) const nsp = createMockNamespace() const io = { of: vi.fn(() => nsp) } const { GlobalAgentServer } = await import('../../packages/server/src/services/global-agent/server') const server = new GlobalAgentServer(io as any) server.init() const agentSocket = createMockSocket('agent-socket', { token: server.getAuthToken(), instanceId: 'device-1' }) await new Promise((resolve, reject) => { nsp.__middleware[0](agentSocket, (err?: Error) => err ? reject(err) : resolve()) }) nsp.__handlers.get('connection')?.(agentSocket) agentSocket.__handlers.get('mcu.session.clear')?.({ interactionId: 'clear-1', profile: 'research', }) expect(chatRunMocks.clearSessionHistory).not.toHaveBeenCalled() expect(agentSocket.emit).toHaveBeenCalledWith('mcu.session.cleared', expect.objectContaining({ type: 'mcu.session.cleared', sessionId: 'mcu-device-1-research', ok: false, error: 'chat_run_server_unavailable', })) }) })