import { afterEach, describe, expect, it, vi } from 'vitest'; import { createDiscordIngressEnvelope, verifyDiscordIngressEnvelope, DiscordPlugin, type DiscordIngressPayload, parseDiscordInteractionBindings, resolveDiscordInteractionActorId, resolveDiscordInteractionBinding, } from '@mosaicstack/discord-plugin'; import { RuntimeProviderService } from '../agent/runtime-provider-registry.service.js'; import { ChatGateway } from '../chat/chat.gateway.js'; import { CommandAuthorizationService } from '../commands/command-authorization.service.js'; import { validateDiscordServiceToken } from '../chat/chat.gateway-auth.js'; import { ChatRuntimeRouter } from '../chat/chat-runtime-router.js'; import { EmbeddedChatRuntime } from '../chat/embedded-chat.runtime.js'; import { HarnessChatRuntime } from '../chat/harness-chat.runtime.js'; import { HarnessRegistry } from '../harness/harness.registry.js'; import { DiscordReplayProtector } from './discord-replay-protector.js'; const SERVICE_TOKEN = 'test-service-token'; const ENV_KEYS = [ 'DISCORD_SERVICE_TOKEN', 'DISCORD_SERVICE_USER_ID', 'DISCORD_SERVICE_TENANT_ID', 'DISCORD_INTERACTION_BINDINGS', 'DISCORD_ALLOWED_GUILD_IDS', 'DISCORD_ALLOWED_CHANNEL_IDS', 'DISCORD_ALLOWED_USER_IDS', 'MOSAIC_AGENT_NAME', 'MOSAIC_AGENT_CONFIG_ID', 'CHAT_HARNESS_RUNTIME', ] as const; const savedEnv = new Map(); function configureDiscordEnv(role: 'admin' | 'member' = 'admin'): void { for (const key of ENV_KEYS) savedEnv.set(key, process.env[key]); process.env['DISCORD_SERVICE_TOKEN'] = SERVICE_TOKEN; process.env['DISCORD_SERVICE_USER_ID'] = 'discord-service'; process.env['DISCORD_SERVICE_TENANT_ID'] = 'tenant-discord'; process.env['MOSAIC_AGENT_NAME'] = 'Nova'; process.env['MOSAIC_AGENT_CONFIG_ID'] = 'agent-config-nova'; process.env['DISCORD_ALLOWED_GUILD_IDS'] = 'guild-001'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_ALLOWED_USER_IDS'] = 'user-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: role === 'admin' ? 'admin' : 'operator', mosaicUserId: 'mosaic-admin-001', }, }, }, ]); } afterEach((): void => { for (const key of ENV_KEYS) { const value = savedEnv.get(key); if (value === undefined) delete process.env[key]; else process.env[key] = value; } savedEnv.clear(); }); function commandAuthorization(role: 'admin' | 'member'): CommandAuthorizationService { const entries = new Map(); const db = { select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ role }] }) }) }), }; const redis = { get: async (key: string) => entries.get(key) ?? null, set: async (key: string, value: string) => entries.set(key, value), del: async (key: string) => Number(entries.delete(key)), }; return new CommandAuthorizationService(db as never, redis); } function discordGateway(role: 'admin' | 'member'): { gateway: ChatGateway; client: { data: { discordService: boolean }; emit: ReturnType }; consumedActions: Array<{ actorId: string; correlationId: string }>; durable: { getSnapshot: ReturnType }; audit: { record: ReturnType }; } { const authorization = commandAuthorization(role); const consumedActions: Array<{ actorId: string; correlationId: string }> = []; const durable = { getSnapshot: vi.fn().mockResolvedValue({ identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }), }; const audit = { record: vi.fn().mockResolvedValue(undefined) }; const runtimeRegistry = new RuntimeProviderService( { require: () => ({ capabilities: async () => ({ supported: ['session.terminate'] }), terminate: async () => undefined, }), } as never, { record: async () => undefined } as never, { consume: async (approvalId, action) => { consumedActions.push({ actorId: action.actorId, correlationId: action.correlationId }); return authorization.consumeRuntimeTerminationApproval(approvalId, action); }, }, ); return { gateway: new ChatGateway( {} as never, {} as never, {} as never, {} as never, {} as never, {} as never, authorization, runtimeRegistry, durable as never, audit as never, ), client: { data: { discordService: true }, emit: vi.fn() }, consumedActions, durable, audit, }; } function ingressEnvelope( content: string, messageId: string, overrides: Partial = {}, ): ReturnType { return createDiscordIngressEnvelope( createPayload({ content, messageId, ...overrides }), SERVICE_TOKEN, ); } function createPayload(overrides: Partial = {}): DiscordIngressPayload { return { correlationId: 'correlation-001', messageId: 'discord-message-001', guildId: 'guild-001', channelId: 'channel-001', userId: 'user-001', conversationId: 'Nova:discord:channel-001', content: 'hello Tess', ...overrides, }; } /** * Task 5 fence (C): the Discord SEND path runs through the exclusive {@link ChatRuntimeRouter}, * constructed here in `pi-rpc` mode with a fully-resolved runtime (`active` = harness). A verified * Discord *service* turn must nonetheless execute on the {@link EmbeddedChatRuntime} — never the * harness, never the routing engine — per the Q1/Q2 adjudication: the router owns a dedicated * verified-ingress dispatch that delegates to embedded regardless of mode, with zero harness * fallback. The gateway is given the router in the former direct-`AgentService` constructor slot. * * RED today: production still reads that slot as a bare `AgentService`, so `this.agentService` * resolves to the router, `getSession(...)` is not a function, the send path throws and is caught * (an `error` is emitted and the handler returns) BEFORE it ever reaches the embedded runtime. The * failure is behavioural wiring — collection, DI, and `onModuleInit` all succeed. GREEN re-routes * the verified Discord dispatch through the router into the embedded runtime, satisfying the * preserved create/prompt assertions without weakening any control. `harnessConversations.append` * proves the harness path is never touched even though the pi-rpc router resolved it as `active`. * * Correction #4 is proved behaviourally, not by naming an accessor: the verified-ingress dispatch * is reachable only from the fully-verified `discordService` branch (the create/prompt tests below) * and never from a browser-emittable socket event (the browser-forgery refusal test). */ function readyPiRpcRegistry(): HarnessRegistry { const registry = new HarnessRegistry(); // A registered 'pi' adapter + an available (non-sentinel) conversation service let the pi-rpc // router resolve `active` = harness instead of failing closed at init, so these tests model the // real hostile condition — the harness runtime IS live — rather than a degraded router. registry.register({ id: 'pi' } as never); return registry; } function piRpcRouterFronting( agentService: unknown, harnessConversations: { append: ReturnType }, ): ChatRuntimeRouter { const routerConversationServiceTripwire = { append: () => { throw new Error('router conversation service must not be resolved on the Discord path'); }, }; const embedded = new EmbeddedChatRuntime(agentService as never); const harness = new HarnessChatRuntime(harnessConversations as never); const router = new ChatRuntimeRouter( readyPiRpcRegistry(), routerConversationServiceTripwire as never, embedded, harness, 'pi-rpc', ); router.onModuleInit(); return router; } describe('Discord ingress security', () => { it('keeps legacy role-only bindings valid while withholding privileged actor identity', () => { const [binding] = parseDiscordInteractionBindings( JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': 'admin' }, }, ]), ); expect( resolveDiscordInteractionBinding([binding!], 'guild-001', 'channel-001', 'user-001', 'send'), ).toEqual(binding); expect(resolveDiscordInteractionActorId(binding!, 'user-001')).toBeNull(); }); it('binds a differently named configured interaction instance without code changes', () => { const binding = resolveDiscordInteractionBinding( [ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' } }, }, ], 'guild-001', 'channel-001', 'user-001', 'send', ); expect(binding?.instanceId).toBe('Nova'); }); it('accepts only the configured Discord service identity', () => { expect(validateDiscordServiceToken(SERVICE_TOKEN, SERVICE_TOKEN)).toBe(true); expect(validateDiscordServiceToken('wrong-service-token', SERVICE_TOKEN)).toBe(false); expect(validateDiscordServiceToken(undefined, SERVICE_TOKEN)).toBe(false); }); it('rejects unauthenticated or tampered service envelopes', () => { const envelope = createDiscordIngressEnvelope(createPayload(), SERVICE_TOKEN); expect(verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN)).toEqual(createPayload()); expect(verifyDiscordIngressEnvelope(envelope, 'wrong-service-token')).toBeNull(); expect( verifyDiscordIngressEnvelope( { ...envelope, payload: { ...envelope.payload, content: 'forged command' } }, SERVICE_TOKEN, ), ).toBeNull(); }); it.each([ ['guild', { guildId: 'unlisted-guild' }], ['channel', { channelId: 'unlisted-channel' }], ['user', { userId: 'unlisted-user' }], ])( 'rejects an unallowlisted Discord %s', (_kind: string, overrides: Partial) => { const envelope = createDiscordIngressEnvelope(createPayload(overrides), SERVICE_TOKEN); expect( verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN, { guildIds: ['guild-001'], channelIds: ['channel-001'], userIds: ['user-001'], }), ).toBeNull(); }, ); it('retains Discord message and correlation IDs after authenticated allowlisted validation', () => { const payload = createPayload({ correlationId: 'correlation-trace-123', messageId: 'discord-snowflake-987', }); const envelope = createDiscordIngressEnvelope(payload, SERVICE_TOKEN); expect( verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN, { guildIds: ['guild-001'], channelIds: ['channel-001'], userIds: ['user-001'], }), ).toEqual(payload); }); it('rejects a replayed Discord message ID while retaining bounded replay state', () => { const replayProtector = new DiscordReplayProtector(60_000, 2); expect(replayProtector.claim('discord-message-001')).toBe(true); expect(replayProtector.claim('discord-message-001')).toBe(false); expect(replayProtector.claim('discord-message-002')).toBe(true); expect(replayProtector.claim('discord-message-003')).toBe(true); expect(replayProtector.size).toBe(2); }); it('consumes the exact target once when approval and stop are separate Discord messages', async () => { configureDiscordEnv(); const { gateway, client, consumedActions } = discordGateway('admin'); await gateway.handleDiscordApproval( client as never, ingressEnvelope('/approve', 'approve-message', { correlationId: 'approval-ingress-correlation', }), ); const approval = client.emit.mock.calls.find( ([event]) => event === 'discord:approval', )?.[1] as { approvalId: string; success: boolean; }; expect(approval.success).toBe(true); await gateway.handleDiscordStop( client as never, ingressEnvelope(`/stop ${approval.approvalId}`, 'stop-message', { correlationId: 'stop-ingress-correlation', }), ); expect(client.emit).toHaveBeenCalledWith('discord:stop', { correlationId: 'stop-ingress-correlation', success: true, }); expect(consumedActions).toEqual([ { actorId: 'mosaic-admin-001', correlationId: expect.stringMatching(/^discord-action:v1:/), }, ]); }); it('audits a Discord mint-side authorization denial', async () => { configureDiscordEnv(); const { gateway, client, audit } = discordGateway('member'); await gateway.handleDiscordApproval( client as never, ingressEnvelope('/approve', 'denied-approve'), ); expect(client.emit).toHaveBeenCalledWith('discord:approval', { correlationId: 'correlation-001', success: false, approvalId: undefined, expiresAt: undefined, }); expect(audit.record).toHaveBeenCalledWith( expect.objectContaining({ outcome: 'denied', operation: 'session.terminate', errorCode: 'policy_denied', }), ); }); it('rejects approval when the durable session targets a different logical agent', async () => { configureDiscordEnv(); const { gateway, client, durable } = discordGateway('admin'); durable.getSnapshot.mockResolvedValueOnce({ identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }); await gateway.handleDiscordApproval( client as never, ingressEnvelope('/approve', 'mismatched-agent-approve'), ); expect(client.emit).toHaveBeenCalledWith('discord:approval', { correlationId: 'correlation-001', success: false, approvalId: undefined, expiresAt: undefined, }); }); it('rejects privileged envelopes with a forged current conversation route', async () => { configureDiscordEnv(); const { gateway, client } = discordGateway('admin'); await gateway.handleDiscordApproval( client as never, ingressEnvelope('/approve', 'forged-approval-route', { conversationId: 'Nova:discord:other-channel', }), ); await gateway.handleDiscordStop( client as never, ingressEnvelope('/stop forged', 'forged-stop-route', { conversationId: 'Nova:discord:other-channel', }), ); expect(client.emit).not.toHaveBeenCalledWith('discord:approval', expect.anything()); expect(client.emit).not.toHaveBeenCalledWith('discord:stop', expect.anything()); }); it('rejects unpaired and non-admin Discord users for approval and stop', async () => { configureDiscordEnv(); const { gateway, client } = discordGateway('member'); await gateway.handleDiscordApproval( client as never, ingressEnvelope('/approve', 'member-approve'), ); expect(client.emit).toHaveBeenCalledWith('discord:approval', { correlationId: 'correlation-001', success: false, approvalId: undefined, expiresAt: undefined, }); process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([]); await gateway.handleDiscordStop( client as never, ingressEnvelope('/stop forged', 'unpaired-stop'), ); expect(client.emit).not.toHaveBeenCalledWith('discord:stop', expect.anything()); }); it('rejects replaying a Discord-created termination approval', async () => { configureDiscordEnv(); const { gateway, client } = discordGateway('admin'); await gateway.handleDiscordApproval( client as never, ingressEnvelope('/approve', 'replay-approve', { correlationId: 'replay-approval-correlation', }), ); const approval = client.emit.mock.calls.find( ([event]) => event === 'discord:approval', )?.[1] as { approvalId: string; }; await gateway.handleDiscordStop( client as never, ingressEnvelope(`/stop ${approval.approvalId}`, 'replay-stop-one', { correlationId: 'replay-stop-correlation-one', }), ); await gateway.handleDiscordStop( client as never, ingressEnvelope(`/stop ${approval.approvalId}`, 'replay-stop-two', { correlationId: 'replay-stop-correlation-two', }), ); const stopResults = client.emit.mock.calls.filter(([event]) => event === 'discord:stop'); expect(stopResults.map(([, result]) => (result as { success: boolean }).success)).toEqual([ true, false, ]); }); it.each([ 'https://user:password@cdn.example.test/diagram.png', 'https://cdn.example.test/diagram.png?token=secret', 'https://cdn.example.test/diagram.png?X-Amz-Signature=secret', 'https://cdn.example.test/diagram.png?auth=secret', 'https://cdn.example.test/diagram.png?hm=secret', ])('rejects credential-bearing attachment URLs before gateway dispatch', async (url) => { configureDiscordEnv(); const { gateway, client } = discordGateway('admin'); await gateway.handleMessage( client as never, ingressEnvelope('', `credential-url-${url.length}`, { conversationId: 'Nova:discord:channel-001', attachments: [ { id: 'attachment-credential', name: 'diagram.png', url, contentType: 'image/png' }, ], }), ); expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything()); }); it("selects each binding's trusted logical-agent config when creating Discord sessions", async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001,channel-002'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, { instanceId: 'Orion', agentConfigId: 'agent-config-orion', guildId: 'guild-001', channelId: 'channel-002', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); const session = { provider: 'configured-provider', modelId: 'configured-model', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const createSession = vi.fn().mockResolvedValue(session); const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), prompt: vi.fn().mockResolvedValue(undefined), }; const brain = { agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: id === 'agent-config-orion' ? 'Orion' : 'Nova', }), ), }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage: vi.fn().mockResolvedValue(undefined), }, }; const routingEngine = { resolve: vi.fn() }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, routingEngine as never, ); const client = { id: 'discord-client-new-session', data: { discordService: true }, emit: vi.fn(), }; await gateway.handleMessage( client as never, ingressEnvelope('start configured session', 'configured-session-001', { conversationId: 'Nova:discord:channel-001', }), ); await gateway.handleMessage( client as never, ingressEnvelope('start second configured session', 'configured-session-002', { channelId: 'channel-002', conversationId: 'Orion:discord:channel-002', }), ); expect(createSession).toHaveBeenCalledWith( 'Nova:discord:channel-001', expect.objectContaining({ agentConfigId: 'agent-config-nova', userId: 'discord-service', tenantId: 'tenant-discord', }), ); expect(createSession).toHaveBeenCalledWith( 'Orion:discord:channel-002', expect.objectContaining({ agentConfigId: 'agent-config-orion' }), ); expect(routingEngine.resolve).not.toHaveBeenCalled(); // Even though the pi-rpc router resolved the harness as `active`, verified Discord ingress must // never touch it — the create path stays on the embedded runtime. expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('dispatches a verified Discord SEND once and drops a byte-identical replay with zero additional dispatch/persist/ack (Task 5 G4)', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); const session = { provider: 'configured-provider', modelId: 'configured-model', agentConfigId: 'agent-config-nova', agentName: 'Nova', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const createSession = vi.fn().mockResolvedValue(session); const prompt = vi.fn().mockResolvedValue(undefined); const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), removeChannel: vi.fn(), prompt, }; const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); const brain = { agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, { resolve: vi.fn() } as never, ); const client = { id: 'discord-client-replay', data: { discordService: true }, emit: vi.fn(), }; const ackCount = (): number => client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length; // One fully-valid signed envelope; the replay reuses the SAME object (same messageId). const envelope = ingressEnvelope('verified once', 'discord-replay-001', { conversationId: 'Nova:discord:channel-001', }); // First delivery: the verified-Discord SEND runs the full embedded dispatch exactly once. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); // Byte-identical replay: the messageId is already claimed, so resolveDiscordIngress returns // null and the SEND handler bails before dispatch/persist/ack. Every effect stays at exactly one. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); // The harness runtime is never touched on either delivery. expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('a verified SEND that fails the configured service identity consumes no replay claim, so a corrected byte-identical retry dispatches/persists/acks exactly once and a later duplicate stays fail-closed (Task 5 item 4 — claim ordering)', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); const session = { provider: 'configured-provider', modelId: 'configured-model', agentConfigId: 'agent-config-nova', agentName: 'Nova', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const createSession = vi.fn().mockResolvedValue(session); const prompt = vi.fn().mockResolvedValue(undefined); const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), removeChannel: vi.fn(), prompt, }; const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); const brain = { agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, { resolve: vi.fn() } as never, ); const client = { id: 'discord-client-claim-ordering', data: { discordService: true }, emit: vi.fn(), }; const ackCount = (): number => client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length; // A single fully-valid signed envelope, reused byte-for-byte across all three deliveries. const envelope = ingressEnvelope('verified once with late identity', 'discord-order-001', { conversationId: 'Nova:discord:channel-001', }); // (1) Configured service identity is MISSING. The envelope is validly signed and passes the // binding + route checks, but the SEND must refuse at the identity gate BEFORE any claim // or effect. If the claim fires ahead of that gate, this delivery silently burns the // replay claim for `discord-order-001` even though nothing dispatched. delete process.env['DISCORD_SERVICE_USER_ID']; await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(0); expect(prompt).toHaveBeenCalledTimes(0); expect(addMessage).toHaveBeenCalledTimes(0); expect(ackCount()).toBe(0); // (2) Identity is now configured; the operator resends the SAME envelope byte-for-byte. Because // step (1) consumed no claim, this corrected retry claims once and runs the full embedded // dispatch exactly once. (Under the pre-fix ordering the claim was already spent in step (1), // so this retry is dropped as a replay and never dispatches — the RED this test drives.) process.env['DISCORD_SERVICE_USER_ID'] = 'discord-service'; await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); // (3) A genuine duplicate after a committed turn stays fail-closed: the claim taken in step (2) // blocks it, so every effect remains at exactly one. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('a verified SEND whose configured agent record fails reconciliation consumes no replay claim, so a corrected byte-identical retry dispatches/persists/acks exactly once (Task 5 finding 3)', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); const session = { provider: 'configured-provider', modelId: 'configured-model', agentConfigId: 'agent-config-nova', agentName: 'Nova', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const createSession = vi.fn().mockResolvedValue(session); const prompt = vi.fn().mockResolvedValue(undefined); const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), removeChannel: vi.fn(), prompt, }; const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); // The durable agent record does not reconcile on the first delivery (its name no longer matches // the verified binding's instance id), then reconciles cleanly on the corrected retry. const findAgent = vi .fn() .mockResolvedValueOnce({ id: 'agent-config-nova', name: 'Renamed-Away' }) .mockResolvedValue({ id: 'agent-config-nova', name: 'Nova' }); const brain = { agents: { findById: findAgent }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, { resolve: vi.fn() } as never, ); const client = { id: 'discord-client-reconcile', data: { discordService: true }, emit: vi.fn(), }; const ackCount = (): number => client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length; const envelope = ingressEnvelope( 'verified once with stale agent record', 'discord-reconcile-001', { conversationId: 'Nova:discord:channel-001', }, ); // (1) The configured-agent reconcile runs BEFORE the replay claim. A mismatch refuses the turn // and, crucially, consumes no claim for discord-reconcile-001 — nothing dispatches. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(0); expect(prompt).toHaveBeenCalledTimes(0); expect(addMessage).toHaveBeenCalledTimes(0); expect(ackCount()).toBe(0); // (2) The record now reconciles; because step (1) took no claim, this byte-identical retry claims // once and runs the full embedded dispatch exactly once. (Pre-fix, the claim was spent ahead // of the reconcile in step (1), so this retry was dropped as a replay — the RED this drives.) await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); // (3) A genuine duplicate after the committed turn stays fail-closed. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('a verified SEND refuses to reuse a same-scope embedded session minted under a different configured identity, with zero prompt/persist/ack (Task 5 finding 3)', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); // A live session already exists for this conversation/scope, but it was minted under a DIFFERENT // configured agent (Orion). The verified binding reconciles to Nova, so reusing this session would // execute one agent's turn under another agent's verified label — the reuse guard must refuse it. const foreignIdentitySession = { provider: 'configured-provider', modelId: 'configured-model', agentConfigId: 'agent-config-orion', agentName: 'Orion', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const prompt = vi.fn().mockResolvedValue(undefined); const createSession = vi.fn().mockResolvedValue(foreignIdentitySession); const agentService = { getSession: vi.fn().mockReturnValue(foreignIdentitySession), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), removeChannel: vi.fn(), prompt, }; const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); const brain = { agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, { resolve: vi.fn() } as never, ); const client = { id: 'discord-client-identity-swap', data: { discordService: true }, emit: vi.fn(), }; await gateway.handleMessage( client as never, ingressEnvelope('reuse under a different identity', 'discord-identity-swap-001', { conversationId: 'Nova:discord:channel-001', }), ); // Refused at the embedded reuse guard: no prompt, no persist, no ack — only a typed refusal. expect(prompt).not.toHaveBeenCalled(); expect(addMessage).not.toHaveBeenCalled(); expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything()); expect(client.emit).toHaveBeenCalledWith( 'error', expect.objectContaining({ conversationId: 'Nova:discord:channel-001' }), ); expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('a verified SEND whose configured agent record resolves under a different id fails reconciliation, consumes no replay claim, and a corrected byte-identical retry dispatches/persists/acks exactly once (Task 5 finding 3 — id axis)', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); const session = { provider: 'configured-provider', modelId: 'configured-model', agentConfigId: 'agent-config-nova', agentName: 'Nova', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const createSession = vi.fn().mockResolvedValue(session); const prompt = vi.fn().mockResolvedValue(undefined); const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), removeChannel: vi.fn(), prompt, }; const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); // The name matches the verified binding, but the record's own id is a DIFFERENT agent config — // an aliased/substituted lookup. Exact-id reconciliation must refuse it on the first delivery, // then admit the corrected record whose id matches the binding. const findAgent = vi .fn() .mockResolvedValueOnce({ id: 'agent-config-elsewhere', name: 'Nova' }) .mockResolvedValue({ id: 'agent-config-nova', name: 'Nova' }); const brain = { agents: { findById: findAgent }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, { resolve: vi.fn() } as never, ); const client = { id: 'discord-client-reconcile-id', data: { discordService: true }, emit: vi.fn(), }; const ackCount = (): number => client.emit.mock.calls.filter((call) => call[0] === 'message:ack').length; const envelope = ingressEnvelope( 'verified once with aliased agent id', 'discord-reconcile-id-001', { conversationId: 'Nova:discord:channel-001', }, ); // (1) The record's id differs from the binding's agentConfigId. Exact-id reconcile refuses the // turn BEFORE the replay claim, so nothing dispatches and the claim stays available. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(0); expect(prompt).toHaveBeenCalledTimes(0); expect(addMessage).toHaveBeenCalledTimes(0); expect(ackCount()).toBe(0); // (2) The record now reconciles on both id and name; because step (1) took no claim, this // byte-identical retry claims once and runs the full embedded dispatch exactly once. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); // (3) A genuine duplicate after the committed turn stays fail-closed. await gateway.handleMessage(client as never, envelope); expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledTimes(1); expect(addMessage).toHaveBeenCalledTimes(1); expect(ackCount()).toBe(1); expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('a verified SEND refuses a freshly minted same-scope session whose identity differs from the reconciled configured agent, with zero prompt/persist/ack (Task 5 finding 3 — post-create)', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' }, }, }, ]); // No live session exists for this scope, so the runtime MINTS one — but createSession returns a // session carrying a DIFFERENT configured identity (Orion) than the reconciled binding (Nova). // The post-create identity recheck must refuse it rather than dispatch one agent's turn under // another agent's verified label. (The existing reuse test covers the getSession path; this // covers the createSession path scrappy flagged as unvalidated.) const mintedForeignSession = { provider: 'configured-provider', modelId: 'configured-model', agentConfigId: 'agent-config-orion', agentName: 'Orion', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const prompt = vi.fn().mockResolvedValue(undefined); const createSession = vi.fn().mockResolvedValue(mintedForeignSession); const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession, recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), removeChannel: vi.fn(), prompt, }; const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); const brain = { agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), findMessages: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, { resolve: vi.fn() } as never, ); const client = { id: 'discord-client-postcreate-mismatch', data: { discordService: true }, emit: vi.fn(), }; await gateway.handleMessage( client as never, ingressEnvelope('mint under a different identity', 'discord-postcreate-001', { conversationId: 'Nova:discord:channel-001', }), ); // The freshly minted session failed the post-create identity recheck: refused with a typed // error, no prompt, no persist, no ack. expect(createSession).toHaveBeenCalledTimes(1); expect(prompt).not.toHaveBeenCalled(); expect(addMessage).not.toHaveBeenCalled(); expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything()); expect(client.emit).toHaveBeenCalledWith( 'error', expect.objectContaining({ conversationId: 'Nova:discord:channel-001' }), ); expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('retains validated persisted attachments in resumed conversation history', async () => { const attachment = { id: 'attachment-history', name: 'diagram.png', url: 'https://cdn.example.test/diagram.png', mimeType: 'image/png', sizeBytes: 4_096, }; const gateway = new ChatGateway( {} as never, {} as never, { conversations: { findMessages: vi.fn().mockResolvedValue([ { role: 'user', content: '', createdAt: new Date('2026-07-14T12:00:00.000Z'), metadata: { channelAttachments: [attachment] }, }, ]), }, } as never, {} as never, {} as never, {} as never, ) as unknown as { loadConversationHistory( conversationId: string, userId: string, ): Promise>; }; await expect( gateway.loadConversationHistory('Nova:discord:channel-001', 'discord-service'), ).resolves.toEqual([expect.objectContaining({ attachments: [attachment] })]); }); it('rejects malformed signed attachment payloads before gateway dispatch', async () => { configureDiscordEnv(); const { gateway, client } = discordGateway('admin'); const malformedPayload: Record = { ...createPayload({ messageId: 'malformed-attachments-001', conversationId: 'Nova:discord:channel-001', }), attachments: { id: 'not-an-array' }, }; const envelope = createDiscordIngressEnvelope( malformedPayload as unknown as DiscordIngressPayload, SERVICE_TOKEN, ); await gateway.handleMessage(client as never, envelope); expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything()); }); it('preserves authenticated attachment metadata through persistence and agent dispatch', async () => { configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; const prompt = vi.fn().mockResolvedValue(undefined); const addMessage = vi.fn().mockResolvedValue({ id: 'discord-persisted-message' }); const session = { provider: 'test-provider', modelId: 'test-model', // The reused embedded session carries the SAME reconciled identity as the verified binding, // so the finding-3 session-reuse guard admits it rather than refusing an identity swap. agentConfigId: 'agent-config-nova', agentName: 'Nova', piSession: { thinkingLevel: 'medium', getAvailableThinkingLevels: (): string[] => ['medium'], }, }; const agentService = { getSession: vi.fn().mockReturnValue(session), recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), prompt, }; const brain = { agents: { findById: vi.fn((id: string) => Promise.resolve({ id, name: 'Nova' })) }, conversations: { findById: vi.fn().mockResolvedValue({ id: 'Nova:discord:channel-001' }), create: vi.fn().mockResolvedValue(undefined), update: vi.fn().mockResolvedValue(undefined), addMessage, }, }; const harnessConversations = { append: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, brain as never, {} as never, {} as never, {} as never, ); const client = { id: 'discord-client-001', data: { discordService: true }, emit: vi.fn(), }; const attachment = { id: 'attachment-001', name: 'diagram.png', url: 'https://cdn.example.test/diagram.png', contentType: 'image/png', sizeBytes: 4_096, }; await gateway.handleMessage( client as never, ingressEnvelope('', 'attachment-message-001', { conversationId: 'Nova:discord:channel-001', attachments: [attachment], }), ); const expectedAttachment = { id: attachment.id, name: attachment.name, url: attachment.url, mimeType: attachment.contentType, sizeBytes: attachment.sizeBytes, }; expect(prompt).toHaveBeenCalledWith( 'Nova:discord:channel-001', '', { userId: 'discord-service', tenantId: 'tenant-discord' }, [expectedAttachment], ); expect(addMessage).toHaveBeenCalledWith( expect.objectContaining({ conversationId: 'Nova:discord:channel-001', metadata: expect.objectContaining({ channelAttachments: [expectedAttachment] }), }), 'discord-service', ); // The verified Discord prompt dispatch stays on the embedded runtime; the pi-rpc harness that // the router resolved as `active` is never reached. expect(harnessConversations.append).not.toHaveBeenCalled(); }); it('refuses a browser-forged Discord ingress envelope in pi-rpc with a fixed typed refusal and zero dispatch', async () => { // Correction #2 + #4 (behavioural). A browser socket is never `discordService` (that flag is // set only on a valid service-token handshake), so it cannot forge the trusted Discord path by // emitting an envelope-shaped payload. In pi-rpc it must receive a FIXED TYPED refusal // (`runtime_unsupported`, the same typed code the sibling harness-fence uses) and reach neither // the forced Discord service scope, the verified Discord operation, the embedded runtime, nor // the harness. There is no dedicated socket event for verified ingress — the only ingress // surface is the generic `message` handler, and a non-service client is refused there. // // RED today: a non-service client emitting an envelope-shaped payload falls to the browser // branch, fails the chat-message shape check, and is dropped SILENTLY (a warn + return) with no // typed refusal emitted — so the refusal assertion fails. Collection and construction succeed; // the gap is behavioural. GREEN emits the fixed typed refusal before any dispatch. configureDiscordEnv(); process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc'; const agentService = { getSession: vi.fn().mockReturnValue(undefined), createSession: vi.fn(), recordMessage: vi.fn(), onEvent: vi.fn().mockReturnValue((): void => undefined), addChannel: vi.fn(), prompt: vi.fn().mockResolvedValue(undefined), }; const harnessConversations = { append: vi.fn() }; const routingEngine = { resolve: vi.fn() }; const gateway = new ChatGateway( piRpcRouterFronting(agentService, harnessConversations) as never, {} as never, { conversations: { addMessage: vi.fn().mockResolvedValue(undefined) } } as never, {} as never, {} as never, routingEngine as never, ); const client = { id: 'browser-forging-discord', data: { discordService: false }, emit: vi.fn(), }; await gateway.handleMessage( client as never, ingressEnvelope('forged from a browser', 'browser-forgery-001', { conversationId: 'Nova:discord:channel-001', }), ); const refusal = client.emit.mock.calls.find( ([, payload]) => (payload as { code?: string } | undefined)?.code === 'runtime_unsupported', ); expect(refusal).toBeDefined(); expect(client.emit).not.toHaveBeenCalledWith('message:ack', expect.anything()); expect(agentService.createSession).not.toHaveBeenCalled(); expect(agentService.prompt).not.toHaveBeenCalled(); expect(harnessConversations.append).not.toHaveBeenCalled(); expect(routingEngine.resolve).not.toHaveBeenCalled(); }); it('accepts a thread message through its allowed bound parent channel', () => { const emitted = vi.fn(); const plugin = new DiscordPlugin({ token: 'unused', gatewayUrl: 'http://unused', serviceToken: SERVICE_TOKEN, allowedGuildIds: ['guild-001'], allowedChannelIds: ['channel-001'], allowedUserIds: ['user-001'], interactionBindings: [ { instanceId: 'Nova', agentConfigId: 'agent-config-nova', guildId: 'guild-001', channelId: 'channel-001', pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' } }, }, ], }); const internals = plugin as unknown as { client: { user: { id: string } }; socket: { connected: boolean; emit: ReturnType }; handleDiscordMessage(message: unknown): void; }; internals.client = { user: { id: 'bot-001' } }; internals.socket = { connected: true, emit: emitted }; internals.handleDiscordMessage({ id: 'thread-message', guildId: 'guild-001', channelId: 'thread-001', author: { id: 'user-001', bot: false }, mentions: { has: () => true }, content: '<@bot-001> hello from thread', channel: { parentId: 'channel-001' }, attachments: new Map(), }); const [, envelope] = emitted.mock.calls[0] as [ string, ReturnType, ]; expect(verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN)?.channelId).toBe('channel-001'); }); });