import { createGatewayRuntimeProviderRegistry } from './agent.module.js'; import { firstValueFrom } from 'rxjs'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { RuntimeApprovalDeniedError, RuntimeProviderService, } from './runtime-provider-registry.service.js'; import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js'; import { InteractionController } from './interaction.controller.js'; describe('InteractionController', (): void => { afterEach(() => vi.restoreAllMocks()); it('maps a denied runtime approval to Fastify HTTP 403', () => { const send = vi.fn(); const status = vi.fn().mockReturnValue({ send }); const response = { status }; const host = { switchToHttp: () => ({ getResponse: () => response }) }; new RuntimeApprovalDeniedFilter().catch(new RuntimeApprovalDeniedError(), host as never); expect(status).toHaveBeenCalledWith(403); expect(send).toHaveBeenCalledWith({ statusCode: 403, message: 'Runtime termination approval denied', }); }); it('honors a differently named configured instance without a code change', async () => { const prior = process.env['MOSAIC_AGENT_NAME']; process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const runtime = { listSessions: vi.fn().mockResolvedValue([]) }; const controller = new InteractionController(runtime as never, {} as never); await expect( controller.sessions('Nova', 'fleet', { id: 'owner', tenantId: 'team' }, 'corr-1'), ).resolves.toEqual([]); await expect( controller.sessions('Other', 'fleet', { id: 'owner', tenantId: 'team' }, 'corr-1'), ).rejects.toThrow('Interaction agent is not configured'); if (prior === undefined) delete process.env['MOSAIC_AGENT_NAME']; else process.env['MOSAIC_AGENT_NAME'] = prior; }); it('reaches the registered Hermes provider through the authenticated transitional matrix route', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const registry = createGatewayRuntimeProviderRegistry(); const runtime = new RuntimeProviderService( registry, { record: vi.fn().mockResolvedValue(undefined) }, { consume: vi.fn().mockResolvedValue(false) }, ); const controller = new InteractionController(runtime, {} as never); await expect( controller.transitionalCapabilities( 'Nova', 'runtime.hermes', { id: 'owner', tenantId: 'team' }, 'corr-1', ), ).resolves.toEqual([ { capability: 'kanban', status: 'unsupported' }, { capability: 'skills', status: 'unsupported' }, { capability: 'memory', status: 'unsupported' }, { capability: 'tools', status: 'unsupported' }, { capability: 'cron', status: 'unsupported' }, ]); }); it('rejects a request without the non-simple correlation header', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const controller = new InteractionController({ listSessions: vi.fn() } as never, {} as never); await expect(controller.sessions('Nova', 'fleet', { id: 'owner' })).rejects.toThrow( 'X-Correlation-Id is required', ); }); it('enrolls a visible runtime session under the cross-surface conversation handle', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const runtime = { listSessions: vi.fn().mockResolvedValue([{ id: 'runtime-1' }]), }; const durable = { enroll: vi.fn().mockResolvedValue(undefined) }; const controller = new InteractionController(runtime as never, durable as never); await expect( controller.enroll( 'Nova', 'conversation-1', { providerId: 'fleet', runtimeSessionId: 'runtime-1' }, { id: 'owner', tenantId: 'team' }, 'corr-1', ), ).resolves.toEqual({ status: 'enrolled', sessionId: 'conversation-1' }); expect(durable.enroll).toHaveBeenCalledWith( { agentName: 'Nova', sessionId: 'conversation-1', tenantId: 'team', ownerId: 'owner', providerId: 'fleet', runtimeSessionId: 'runtime-1', }, expect.objectContaining({ correlationId: 'corr-1' }), ); }); it('rejects an invalid attach mode before invoking a provider', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const controller = new InteractionController({ attach: vi.fn() } as never, {} as never); await expect( controller.attach('Nova', 'durable-1', { mode: 'write' as never }, { id: 'owner' }, 'corr-1'), ).rejects.toThrow('Interaction attach mode is invalid'); }); it('resumes a durable session by attaching and streaming its runtime events', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const runtimeEvent = { type: 'message.delta' as const, sessionId: 'runtime-1', cursor: 'cursor-1', occurredAt: '2026-07-13T00:00:00.000Z', content: 'resumed', }; const runtime = { attach: vi.fn().mockResolvedValue({ attachmentId: 'attach-1', sessionId: 'runtime-1' }), streamSession: vi.fn(async function* () { yield runtimeEvent; }), }; const durable = { getSnapshot: vi.fn().mockResolvedValue({ identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }), }; const controller = new InteractionController(runtime as never, durable as never); await controller.attach('Nova', 'conversation-1', { mode: 'read' }, { id: 'owner' }, 'corr-1'); await expect( firstValueFrom( controller.stream('Nova', 'conversation-1', undefined, { id: 'owner' }, 'corr-1'), ), ).resolves.toEqual({ data: runtimeEvent }); expect(runtime.attach).toHaveBeenCalledWith( 'fleet', 'runtime-1', 'read', expect.objectContaining({ correlationId: 'corr-1' }), ); expect(runtime.streamSession).toHaveBeenCalledWith( 'fleet', 'runtime-1', undefined, expect.objectContaining({ correlationId: 'corr-1' }), ); }); it('does not create a runtime stream after the SSE subscriber disconnects during snapshot lookup', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; let resolveSnapshot!: (value: { identity: Record }) => void; const snapshot = new Promise<{ identity: Record }>((resolve) => { resolveSnapshot = resolve; }); const runtime = { streamSession: vi.fn() }; const durable = { getSnapshot: vi.fn().mockReturnValue(snapshot) }; const controller = new InteractionController(runtime as never, durable as never); const subscription = controller .stream('Nova', 'conversation-1', undefined, { id: 'owner' }, 'corr-1') .subscribe(); subscription.unsubscribe(); resolveSnapshot({ identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }); await new Promise((resolve) => setTimeout(resolve, 0)); expect(runtime.streamSession).not.toHaveBeenCalled(); }); it.each([ ['wrong actor', { getSnapshot: vi.fn().mockRejectedValue(new Error('scope mismatch')) }], [ 'session-agent mismatch', { getSnapshot: vi.fn().mockResolvedValue({ identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }), }, ], ])('denies a CLI stop for %s', async (_reason, durable) => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const runtime = { terminate: vi.fn().mockResolvedValue(undefined) }; const controller = new InteractionController(runtime as never, durable as never); await expect( controller.stop( 'Nova', 'durable-1', { approvalRef: 'approval-1' }, { id: 'owner' }, 'corr-1', ), ).rejects.toBeDefined(); expect(runtime.terminate).not.toHaveBeenCalled(); }); it('surfaces a denied runtime approval to the CLI interaction surface', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const runtime = { terminate: vi.fn().mockRejectedValue(new Error('Runtime termination approval denied')), }; const durable = { getSnapshot: vi.fn().mockResolvedValue({ identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }), }; const controller = new InteractionController(runtime as never, durable as never); await expect( controller.stop( 'Nova', 'durable-1', { approvalRef: 'approval-1' }, { id: 'owner' }, 'corr-1', ), ).rejects.toThrow('Runtime termination approval denied'); }); it('uses the durable session identity and runtime registry for an approved stop', async () => { process.env['MOSAIC_AGENT_NAME'] = 'Nova'; const runtime = { terminate: vi.fn().mockResolvedValue(undefined) }; const durable = { getSnapshot: vi.fn().mockResolvedValue({ identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, }), }; const controller = new InteractionController(runtime as never, durable as never); await controller.stop( 'Nova', 'durable-1', { approvalRef: 'approval-1' }, { id: 'owner' }, 'corr-1', ); expect(runtime.terminate).toHaveBeenCalledWith( 'fleet', 'runtime-1', 'approval-1', expect.objectContaining({ correlationId: 'corr-1', actorScope: { userId: 'owner', tenantId: 'owner' }, }), ); }); });