import { describe, expect, it, vi } from 'vitest'; import { ChatGateway } from './chat.gateway.js'; const CONVERSATION_ID = 'conversation-1'; const CANARY = 'sk_canary12345678'; type GatewayInternals = { clientSessions: Map; relayEvent(client: unknown, conversationId: string, event: unknown): void; }; function buildGateway() { const brain = { conversations: { addMessage: vi.fn().mockResolvedValue(undefined), }, }; const agentService = { getSession: vi.fn().mockReturnValue(undefined), }; const gateway = new ChatGateway( agentService as never, {} as never, brain as never, {} as never, {} as never, {} as never, ); return { gateway: gateway as unknown as GatewayInternals, brain }; } describe('ChatGateway redaction boundary', (): void => { it('redacts a secret split across assistant deltas before egress and persistence', (): void => { const { gateway } = buildGateway(); const client = { connected: true, id: 'client-1', data: { user: { id: 'user-1' } }, emit: vi.fn(), }; const session = { conversationId: CONVERSATION_ID, cleanup: vi.fn(), assistantText: '', toolCalls: [], pendingToolCalls: new Map(), scope: { userId: 'user-1', tenantId: 'tenant-1' }, }; gateway.clientSessions.set(client.id, session); gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'sk_canary' }, }); expect(JSON.stringify(client.emit.mock.calls)).not.toContain('sk_canary'); gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: '12345678 ' }, }); expect(client.emit).toHaveBeenCalledWith('agent:text', { conversationId: CONVERSATION_ID, text: '[REDACTED_SECRET] ', }); expect(session.assistantText).toBe(`${CANARY} `); expect(JSON.stringify(client.emit.mock.calls)).not.toContain(CANARY); }); it('retains a split secret label until its value can be redacted', (): void => { const { gateway } = buildGateway(); const client = { connected: true, id: 'client-1', data: { user: { id: 'user-1' } }, emit: vi.fn(), }; gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'token ' }, }); gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: '=canaryvalue123 ' }, }); expect(client.emit).toHaveBeenCalledWith('agent:text', { conversationId: CONVERSATION_ID, text: '[REDACTED_SECRET] ', }); expect(JSON.stringify(client.emit.mock.calls)).not.toContain('canaryvalue123'); }); it('holds a streamed private key until it can be redacted', (): void => { const { gateway } = buildGateway(); const client = { connected: true, id: 'client-1', data: { user: { id: 'user-1' } }, emit: vi.fn(), }; gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: '-----BEGIN PRIVATE KEY-----\ncanary' }, }); gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: '\n-----END PRIVATE KEY-----' }, }); expect(client.emit).toHaveBeenCalledWith('agent:text', { conversationId: CONVERSATION_ID, text: '[REDACTED_SECRET]', }); expect(JSON.stringify(client.emit.mock.calls)).not.toContain('canary'); }); it('drops an oversized unterminated stream fragment rather than retaining it', (): void => { const { gateway } = buildGateway(); const client = { connected: true, id: 'client-1', data: { user: { id: 'user-1' } }, emit: vi.fn(), }; gateway.relayEvent(client, CONVERSATION_ID, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'x'.repeat(8_193) }, }); expect(client.emit).toHaveBeenCalledWith('agent:text', { conversationId: CONVERSATION_ID, text: '[REDACTED_STREAM_OVERFLOW]', }); }); it('persists only redacted assistant content with classifications', (): void => { const { gateway, brain } = buildGateway(); const client = { connected: true, id: 'client-1', data: { user: { id: 'user-1' } }, emit: vi.fn(), }; gateway.clientSessions.set(client.id, { conversationId: CONVERSATION_ID, cleanup: vi.fn(), assistantText: CANARY, toolCalls: [], pendingToolCalls: new Map(), scope: { userId: 'user-1', tenantId: 'tenant-1' }, }); gateway.relayEvent(client, CONVERSATION_ID, { type: 'agent_end' }); expect(brain.conversations.addMessage).toHaveBeenCalledWith( expect.objectContaining({ content: '[REDACTED_SECRET]', metadata: expect.objectContaining({ classifications: ['secret'] }), }), 'user-1', ); expect(JSON.stringify(brain.conversations.addMessage.mock.calls)).not.toContain(CANARY); }); });