177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
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 { DurableSessionService } from './durable-session.service.js';
|
|
import { DurableSessionRepository } from './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<void> => {
|
|
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(DurableSessionService)
|
|
.useValue({})
|
|
.overrideProvider(DurableSessionRepository)
|
|
.useValue({})
|
|
.overrideProvider(AgentService)
|
|
.useValue({})
|
|
.overrideProvider(ProviderService)
|
|
.useValue({})
|
|
.overrideProvider(ProviderCredentialsService)
|
|
.useValue({})
|
|
.overrideProvider(RoutingService)
|
|
.useValue({})
|
|
.overrideProvider(RoutingEngineService)
|
|
.useValue({})
|
|
.overrideProvider(SkillLoaderService)
|
|
.useValue({})
|
|
.compile();
|
|
|
|
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
|
|
await app.init();
|
|
await app.getHttpAdapter().getInstance().ready();
|
|
});
|
|
|
|
afterAll(async (): Promise<void> => {
|
|
await app?.close();
|
|
});
|
|
|
|
it('returns gateway denial responses from the actual guarded interaction routes', async (): Promise<void> => {
|
|
if (!app) throw new Error('Nest application did not initialize');
|
|
|
|
const attachDenied = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/interaction/Nova/sessions/session-1/attach',
|
|
headers: { 'x-correlation-id': 'correlation-1' },
|
|
payload: { mode: 'read' },
|
|
});
|
|
expect(attachDenied.statusCode).toBe(401);
|
|
|
|
const sendDenied = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/interaction/Nova/sessions/session-1/send',
|
|
headers: { cookie: 'session=trusted', 'x-correlation-id': 'correlation-1' },
|
|
payload: {},
|
|
});
|
|
expect(sendDenied.statusCode).toBe(403);
|
|
expect(sendDenied.json()).toMatchObject({
|
|
message: 'Content and idempotency key are required',
|
|
});
|
|
|
|
const stopDenied = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/interaction/Nova/sessions/session-1/stop',
|
|
headers: { cookie: 'session=trusted', 'x-correlation-id': 'correlation-1' },
|
|
payload: {},
|
|
});
|
|
expect(stopDenied.statusCode).toBe(403);
|
|
expect(stopDenied.json()).toMatchObject({ message: 'Exact-action approval is required' });
|
|
});
|
|
|
|
it('requires authentication and reaches the Hermes provider registered by AgentModule', async (): Promise<void> => {
|
|
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' },
|
|
]);
|
|
});
|
|
});
|