import { randomUUID } from 'node:crypto'; import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { PrismaClient } from '@prisma/client'; import { resetDb } from '../test-utils/reset-db'; import { makeFakePorts, makeFakeInference } from '@insignia/iios-testkit'; import { PolicyDeniedError } from '@insignia/iios-contracts'; import { MessageService, type MessagePrincipal } from '../messaging/message.service'; import { ActorResolver } from '../identity/actor.resolver'; import { AiJobService } from './ai.service'; import { AiBudgetGuard } from './ai-budget.guard'; import type { PrismaService } from '../prisma/prisma.service'; import type { FakeInference } from '@insignia/iios-testkit'; import type { FakePorts } from '@insignia/iios-testkit'; const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public'; const prisma = new PrismaClient({ datasources: { db: { url } } }); const asService = prisma as unknown as PrismaService; const actors = new ActorResolver(asService); const user: MessagePrincipal = { userId: 'admin-1', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Admin' }; function svc(opts?: { ports?: FakePorts; inference?: FakeInference; quota?: number }) { const ports = opts?.ports ?? makeFakePorts(); const inference = opts?.inference ?? makeFakeInference(); const budget = new AiBudgetGuard(asService); if (opts?.quota !== undefined) budget.setQuota(opts.quota); return new AiJobService(asService, ports, inference, budget, actors); } async function makeInteraction(text: string): Promise { const m = new MessageService(asService, makeFakePorts(), actors); const { threadId } = await m.openThread(null, user); const msg = await m.send(threadId, user, { content: text }, `k-${randomUUID()}`); return msg.id; } beforeAll(async () => { await prisma.$connect(); }); afterAll(async () => { await prisma.$disconnect(); }); beforeEach(async () => { await resetDb(prisma); }); describe('AiJobService (P7 proposal layer)', () => { it('CLASSIFY "party at 9 PM" → CLASSIFICATION artifact + MODERATION_FLAG claim + AI moderation flag', async () => { const interactionId = await makeInteraction('There is a party at 9 PM tonight!'); const { job, artifact } = await svc().runJob({ interactionId, jobType: 'CLASSIFY', principal: user }); expect(job.status).toBe('DONE'); expect(artifact?.artifactType).toBe('CLASSIFICATION'); expect(artifact?.claims.some((c) => c.claimType === 'MODERATION_FLAG')).toBe(true); const flag = await prisma.iiosModerationFlag.findFirst({ where: { interactionId, proposedBy: 'AI' } }); expect(flag?.flagType).toBe('AFTER_HOURS_EVENT'); expect(flag?.confidence).toBeGreaterThan(0); expect(flag?.explanation).toBeTruthy(); }); it('SUMMARIZE → SUMMARY artifact with a cited evidence link', async () => { const interactionId = await makeInteraction('The committee met today. Budget discussed.'); const { artifact } = await svc().runJob({ interactionId, jobType: 'SUMMARIZE', principal: user }); expect(artifact?.artifactType).toBe('SUMMARY'); expect((artifact?.contentRef as { text?: string }).text).toContain('[ai]'); expect(artifact?.evidence.some((e) => e.sourceId === interactionId)).toBe(true); }); it('EXTRACT → EVENT claim stays certainty=DISCUSSION (Scenario 7)', async () => { const interactionId = await makeInteraction('Maybe a party at 9 PM?'); const { artifact } = await svc().runJob({ interactionId, jobType: 'EXTRACT', principal: user }); const event = artifact?.claims.find((c) => c.claimType === 'EVENT'); expect((event?.claimJson as { certainty?: string }).certainty).toBe('DISCUSSION'); }); it('fails closed: OPA deny → FAILED job, PolicyDeniedError, 0 artifacts (KG-03)', async () => { const interactionId = await makeInteraction('hello'); const ports = makeFakePorts(); ports.opa.deny('no ai purpose'); await expect(svc({ ports }).runJob({ interactionId, jobType: 'CLASSIFY', principal: user })).rejects.toBeInstanceOf(PolicyDeniedError); expect(await prisma.iiosAiArtifact.count()).toBe(0); expect((await prisma.iiosAiJob.findFirstOrThrow({ where: { interactionId } })).status).toBe('FAILED'); }); it('budget governor: quota 0 → ABSTAINED job, 0 artifacts (KG-12 degrade)', async () => { const interactionId = await makeInteraction('The committee met today.'); const { job, artifact } = await svc({ quota: 0 }).runJob({ interactionId, jobType: 'SUMMARIZE', principal: user }); expect(job.status).toBe('ABSTAINED'); expect(job.abstentionReason).toBe('BUDGET_EXCEEDED'); expect(artifact).toBeNull(); expect(await prisma.iiosAiArtifact.count()).toBe(0); }); it('is idempotent: replaying the same job key twice → exactly 1 artifact', async () => { const interactionId = await makeInteraction('There is a party at 9 PM tonight!'); const s = svc(); const key = `ai:CLASSIFY:${interactionId}`; await s.runJob({ interactionId, jobType: 'CLASSIFY', principal: user, idempotencyKey: key }); await s.runJob({ interactionId, jobType: 'CLASSIFY', principal: user, idempotencyKey: key }); expect(await prisma.iiosAiJob.count({ where: { interactionId } })).toBe(1); expect(await prisma.iiosAiArtifact.count()).toBe(1); }); it('accept records human feedback (ACCEPTED + claims validated) and emits an event', async () => { const interactionId = await makeInteraction('There is a party at 9 PM tonight!'); const { artifact } = await svc().runJob({ interactionId, jobType: 'CLASSIFY', principal: user }); const updated = await svc().accept(artifact!.id, user); expect(updated.status).toBe('ACCEPTED'); expect(updated.acceptedByActorId).toBeTruthy(); const claim = await prisma.iiosAiClaim.findFirst({ where: { artifactId: artifact!.id } }); expect(claim?.validationStatus).toBe('ACCEPTED'); const evt = await prisma.iiosOutboxEvent.findFirst({ where: { eventType: 'com.insignia.iios.ai.artifact.accepted.v1', aggregateId: artifact!.id } }); expect(evt).toBeTruthy(); }); });