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, beforeEach, 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 { HarnessSelectionRepository } from './harness-selection.repository.js'; import { FakeHarnessAdapter } from './testing/fake-harness.adapter.js'; // Import the REAL module (not a hand-listed controllers+mocks list) so an // unresolved provider fails at app.init() — the #1145-class DI-boot guard. import { HarnessModule } from './harness.module.js'; // A known-available tuple from the fake adapter's default catalog. const VALID = { harnessId: 'fake', providerId: 'fake-openai', modelId: 'fake-mini' }; // A tuple whose provider/model are not in any catalog. const UNKNOWN = { harnessId: 'fake', providerId: 'ghost-provider', modelId: 'ghost-model' }; // A tuple that is known in the catalog but flagged unavailable. const UNAVAILABLE = { harnessId: 'fake', providerId: 'fake-openai', modelId: 'fake-legacy' }; 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 selection HTTP surface', () => { let app: INestApplication; let repository: HarnessSelectionRepository; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [HarnessModule], }) .overrideGuard(AuthGuard) .useValue(authGuard) .overrideProvider(HARNESS_REGISTRY) .useValue(registryWithFake()) .compile(); // Real in-memory repository from the module graph — proves the module wired it. repository = moduleRef.get(HarnessSelectionRepository); app = moduleRef.createNestApplication(new FastifyAdapter()); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), ); await app.init(); await app.getHttpAdapter().getInstance().ready(); }); beforeEach(() => { // Reset owner-scoped state between tests via the public API surface. repository.set({ userId: 'user-1', tenantId: 'user-1' }, VALID); }); afterAll(async () => { await app.close(); }); it('GET selection is server-scoped and ignores caller-supplied scope in the query', async () => { const response = await request(app.getHttpServer()) .get('/api/chat/preferences/selection') .query({ userId: 'attacker', tenantId: 'attacker-tenant', seatId: 'attacker-seat' }); expect(response.status).toBe(200); // The returned selection is user-1's (guard-derived scope), not the query's. expect(response.body.selection).toEqual(VALID); }); it('PUT with a valid structured tuple persists and round-trips via GET', async () => { const next = { harnessId: 'fake', providerId: 'fake-openai', modelId: 'fake-pro' }; const put = await request(app.getHttpServer()) .put('/api/chat/preferences/selection') .send(next) .set('Content-Type', 'application/json'); expect(put.status).toBe(200); expect(put.body.selection).toEqual(next); const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection'); expect(get.status).toBe(200); expect(get.body.selection).toEqual(next); }); it('PUT with FREE TEXT is rejected 400 and does not mutate the stored selection', async () => { const response = await request(app.getHttpServer()) .put('/api/chat/preferences/selection') .send({ selection: 'gpt-4o' }) .set('Content-Type', 'application/json'); expect(response.status).toBe(400); const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection'); expect(get.body.selection).toEqual(VALID); }); it.each([ ['seatId', { ...VALID, seatId: 'attacker-seat' }], ['tenantId', { ...VALID, tenantId: 'attacker-tenant' }], ['userId', { ...VALID, userId: 'attacker' }], ['nativeSessionPath', { ...VALID, nativeSessionPath: '/var/native/x.jsonl' }], ['executable', { ...VALID, executable: '/usr/bin/evil' }], ['home', { ...VALID, home: '/home/attacker' }], ['cwd', { ...VALID, cwd: '/tmp/attacker' }], ])( 'PUT with an extra authority-bearing field (%s) is rejected 400 and does not mutate stored selection', async (_name, body) => { const response = await request(app.getHttpServer()) .put('/api/chat/preferences/selection') .send(body) .set('Content-Type', 'application/json'); expect(response.status).toBe(400); const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection'); expect(get.body.selection).toEqual(VALID); }, ); it('PUT with an UNKNOWN tuple returns selection_invalid, unchanged and echoed unchanged (no fallback)', async () => { const response = await request(app.getHttpServer()) .put('/api/chat/preferences/selection') .send(UNKNOWN) .set('Content-Type', 'application/json'); expect(response.status).toBe(422); expect(response.body.code).toBe('selection_invalid'); // Echoed back unchanged: no first-row / first-provider substitution. expect(response.body.selection).toEqual(UNKNOWN); const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection'); expect(get.body.selection).toEqual(VALID); }); it('PUT with a KNOWN-but-UNAVAILABLE tuple returns model_unavailable, unchanged (distinct from selection_invalid)', async () => { const response = await request(app.getHttpServer()) .put('/api/chat/preferences/selection') .send(UNAVAILABLE) .set('Content-Type', 'application/json'); expect(response.status).toBe(422); expect(response.body.code).toBe('model_unavailable'); expect(response.body.selection).toEqual(UNAVAILABLE); const get = await request(app.getHttpServer()).get('/api/chat/preferences/selection'); expect(get.body.selection).toEqual(VALID); }); });