import 'reflect-metadata'; import { type CanActivate, type ExecutionContext, type INestApplication, ValidationPipe, } from '@nestjs/common'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { AuthGuard } from '../auth/auth.guard.js'; import { HarnessRegistry } from './harness.registry.js'; import { HARNESS_REGISTRY } from './harness.tokens.js'; import { FakeHarnessAdapter } from './testing/fake-harness.adapter.js'; // The real module under test — importing it (not a hand-listed controllers/mocks // list) is what makes an unresolved provider fail loudly at app.init() (#1145 guard). import { HarnessModule } from './harness.module.js'; // Fields that must NEVER surface on a browser-facing catalog/list response. const FORBIDDEN_KEYS = [ 'executable', 'executablePath', 'home', 'homeDir', 'cwd', 'workingDir', 'workingDirectory', 'nativeSessionPath', 'sessionPath', 'env', 'secret', 'secrets', 'token', 'apiKey', ]; function assertNoForbiddenLeak(payload: unknown): void { const serialized = JSON.stringify(payload).toLowerCase(); for (const key of FORBIDDEN_KEYS) { expect(serialized).not.toContain(key.toLowerCase()); } } const authGuard: CanActivate = { canActivate(context: ExecutionContext): boolean { const requestContext = context.switchToHttp().getRequest<{ user?: { id: string } }>(); requestContext.user = { id: 'user-1' }; return true; }, }; function registryWithFake(): HarnessRegistry { const registry = new HarnessRegistry(); registry.register(new FakeHarnessAdapter({ id: 'fake' })); return registry; } describe('Harness catalog HTTP surface', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [HarnessModule], }) .overrideGuard(AuthGuard) .useValue(authGuard) .overrideProvider(HARNESS_REGISTRY) .useValue(registryWithFake()) .compile(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), ); await app.init(); await app.getHttpAdapter().getInstance().ready(); }); afterAll(async () => { await app.close(); }); it('boots the real HarnessModule so all providers resolve at app.init()', () => { // If HarnessModule failed to resolve a provider, beforeAll's app.init() would // have thrown and this suite would never reach here. expect(app).toBeDefined(); }); it('GET /api/harnesses returns 200 with safe fields only', async () => { const response = await request(app.getHttpServer()).get('/api/harnesses'); expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBeGreaterThan(0); const summary = response.body[0]; expect(Object.keys(summary).sort()).toEqual(['capabilities', 'displayName', 'id']); expect(summary.id).toBe('fake'); expect(typeof summary.displayName).toBe('string'); expect(Array.isArray(summary.capabilities)).toBe(true); assertNoForbiddenLeak(response.body); }); it('GET /api/harnesses/:harnessId/catalog returns 200 with safe catalog fields only', async () => { const response = await request(app.getHttpServer()).get('/api/harnesses/fake/catalog'); expect(response.status).toBe(200); expect(response.body.harnessId).toBe('fake'); expect(typeof response.body.version).toBe('string'); expect(typeof response.body.fingerprint).toBe('string'); expect(Array.isArray(response.body.models)).toBe(true); expect(response.body.models.length).toBeGreaterThan(0); const entry = response.body.models[0]; // Whitelisted catalog-entry fields only (no executables/paths/secrets). expect(Object.keys(entry).sort()).toEqual( [ 'authState', 'availability', 'displayName', 'harnessId', 'inputTypes', 'modelId', 'providerId', 'reasoningCapability', ].sort(), ); assertNoForbiddenLeak(response.body); }); it('GET catalog for an unknown harnessId returns a typed adapter_unavailable error, never a fallback catalog', async () => { const response = await request(app.getHttpServer()).get('/api/harnesses/ghost-harness/catalog'); expect(response.status).toBe(404); expect(response.body.code).toBe('adapter_unavailable'); // A fallback catalog would carry a models array; a typed error must not. expect(response.body.models).toBeUndefined(); }); });