diff --git a/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts b/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts new file mode 100644 index 0000000..625bd97 --- /dev/null +++ b/apps/gateway/src/agent/hermes-runtime-reachability.e2e.test.ts @@ -0,0 +1,144 @@ +import 'reflect-metadata'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { Global, Module } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; +import { HermesRuntimeProvider } from '@mosaicstack/agent'; +import { AgentModule } from './agent.module.js'; +import { AUTH } from '../auth/auth.tokens.js'; +import { AuthGuard } from '../auth/auth.guard.js'; +import { BRAIN } from '../brain/brain.tokens.js'; +import { DB } from '../database/database.module.js'; +import { CoordModule } from '../coord/coord.module.js'; +import { McpClientModule } from '../mcp-client/mcp-client.module.js'; +import { SkillsModule } from '../skills/skills.module.js'; +import { GCModule } from '../gc/gc.module.js'; +import { LogModule } from '../log/log.module.js'; +import { CommandsModule } from '../commands/commands.module.js'; +import { + AGENT_RUNTIME_PROVIDER_REGISTRY, + RUNTIME_APPROVAL_VERIFIER, + RUNTIME_PROVIDER_AUDIT_SINK, + RuntimeProviderAuditService, +} from './runtime-provider-registry.service.js'; +import { TessDurableSessionService } from './tess-durable-session.service.js'; +import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; +import { AgentService } from './agent.service.js'; +import { ProviderService } from './provider.service.js'; +import { ProviderCredentialsService } from './provider-credentials.service.js'; +import { RoutingService } from './routing.service.js'; +import { RoutingEngineService } from './routing/routing-engine.service.js'; +import { SkillLoaderService } from './skill-loader.service.js'; + +const authenticatedUser = { id: 'operator-1', tenantId: 'tenant-1' }; + +@Module({}) +class EmptyAgentDependencyModule {} + +@Global() +@Module({ + providers: [ + { + provide: AUTH, + useValue: { + api: { + getSession: vi.fn(async ({ headers }: { headers: Headers }) => + headers.get('cookie') === 'session=trusted' + ? { user: authenticatedUser, session: { id: 'session-1' } } + : null, + ), + }, + }, + }, + AuthGuard, + { provide: BRAIN, useValue: {} }, + { provide: DB, useValue: {} }, + ], + exports: [AUTH, AuthGuard, BRAIN, DB], +}) +class AuthenticatedRequestModule {} + +/** + * This is deliberately an HTTP test rather than a controller unit test: it + * exercises AgentModule's actual provider factory, Nest DI, and AuthGuard. + */ +describe('Hermes runtime provider reachability', (): void => { + let app: NestFastifyApplication | undefined; + + beforeAll(async (): Promise => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const moduleRef = await Test.createTestingModule({ + imports: [AuthenticatedRequestModule, AgentModule], + }) + .overrideModule(CoordModule) + .useModule(EmptyAgentDependencyModule) + .overrideModule(McpClientModule) + .useModule(EmptyAgentDependencyModule) + .overrideModule(SkillsModule) + .useModule(EmptyAgentDependencyModule) + .overrideModule(GCModule) + .useModule(EmptyAgentDependencyModule) + .overrideModule(LogModule) + .useModule(EmptyAgentDependencyModule) + .overrideModule(CommandsModule) + .useModule(EmptyAgentDependencyModule) + .overrideProvider(RuntimeProviderAuditService) + .useValue({ record: vi.fn().mockResolvedValue(undefined) }) + .overrideProvider(RUNTIME_PROVIDER_AUDIT_SINK) + .useValue({ record: vi.fn().mockResolvedValue(undefined) }) + .overrideProvider(RUNTIME_APPROVAL_VERIFIER) + .useValue({ consume: vi.fn().mockResolvedValue(false) }) + .overrideProvider(TessDurableSessionService) + .useValue({}) + .overrideProvider(TessDurableSessionRepository) + .useValue({}) + .overrideProvider(AgentService) + .useValue({}) + .overrideProvider(ProviderService) + .useValue({}) + .overrideProvider(ProviderCredentialsService) + .useValue({}) + .overrideProvider(RoutingService) + .useValue({}) + .overrideProvider(RoutingEngineService) + .useValue({}) + .overrideProvider(SkillLoaderService) + .useValue({}) + .compile(); + + app = moduleRef.createNestApplication(new FastifyAdapter()); + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + afterAll(async (): Promise => { + await app?.close(); + }); + + it('requires authentication and reaches the Hermes provider registered by AgentModule', async (): Promise => { + if (!app) throw new Error('Nest application did not initialize'); + const registry = app.get(AGENT_RUNTIME_PROVIDER_REGISTRY); + expect(registry.get('runtime.hermes')).toBeInstanceOf(HermesRuntimeProvider); + + const denied = await app.inject({ + method: 'GET', + url: '/api/interaction/Nova/transitional-capabilities?provider=runtime.hermes', + headers: { 'x-correlation-id': 'correlation-1' }, + }); + expect(denied.statusCode).toBe(401); + + const response = await app.inject({ + method: 'GET', + url: '/api/interaction/Nova/transitional-capabilities?provider=runtime.hermes', + headers: { cookie: 'session=trusted', 'x-correlation-id': 'correlation-1' }, + }); + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual([ + { capability: 'kanban', status: 'unsupported' }, + { capability: 'skills', status: 'unsupported' }, + { capability: 'memory', status: 'unsupported' }, + { capability: 'tools', status: 'unsupported' }, + { capability: 'cron', status: 'unsupported' }, + ]); + }); +});