import { randomUUID } from 'node:crypto'; import { Pool } from 'pg'; import { describe, expect, it } from 'vitest'; import { createLiteLlmClientFromEnvironment } from '../../server/ai/litellm-client'; import { runDatabaseMigrations } from '../../server/postgres/migration-runner'; import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository'; import { createRegulatoryRagRuntime } from '../../server/regulatory/regulatory-rag-runtime'; import { RegulatoryMappingService } from '../../server/regulatory/regulatory-mapping-service'; import type { Assessment, AssessmentProject, ScanRun } from '../../src/domain/canonical-model'; import { RepositoryConflictError } from '../../src/persistence/repository-errors'; const enabled = process.env.ASSESSOR_REGULATORY_MAPPING_INTEGRATION === '1' && Boolean(process.env.ASSESSOR_TEST_DATABASE_URL); const describeIntegration = enabled ? describe : describe.skip; describeIntegration('regulatory mapping end-to-end integration', () => { it('maps one synthetic finding through BGE-M3, Qdrant, LiteLLM, and PostgreSQL', async () => { const stamp = new Date().toISOString(); const suffix = randomUUID(); const project: AssessmentProject = { id: `synthetic-regulatory-${suffix}`, name: 'Synthetic regulatory integration', createdAt: stamp, updatedAt: stamp, attributes: {} }; const assessment: Assessment = { id: `synthetic-assessment-${suffix}`, projectId: project.id, name: 'Synthetic', createdAt: stamp, updatedAt: stamp }; const scanRun: ScanRun = { id: `synthetic-run-${suffix}`, assessmentId: assessment.id, sequence: 1, label: 'Synthetic', tool: { id: 'purple-knight', name: 'Purple Knight', version: 'synthetic' }, importedAt: stamp }; const occurrenceId = `synthetic-occurrence-${suffix}`; const pool = new Pool({ connectionString: process.env.ASSESSOR_TEST_DATABASE_URL, max: 2, connectionTimeoutMillis: 5_000 }); await runDatabaseMigrations(pool); const repository = await PostgresAssessorRepository.create(pool); const rag = createRegulatoryRagRuntime(); const client = createLiteLlmClientFromEnvironment(); if (!client) throw new Error('Approved LiteLLM configuration is required.'); const service = new RegulatoryMappingService(repository, { findingRetrieval: rag.findingRetrieval, index: rag.index, embeddingModel: rag.embeddingModel, client, corpusId: process.env.ASSESSOR_RAG_ACTIVE_CORPUS_ID, corpusVersion: process.env.ASSESSOR_RAG_ACTIVE_CORPUS_VERSION }); let deleted = false; try { await repository.saveProject(project); await repository.saveAssessment(assessment); const session = await repository.beginImport({ sessionId: `synthetic-session-${suffix}`, projectId: project.id, assessmentId: assessment.id, scanRun }); await session.stage({ importFiles: [{ id: `synthetic-file-${suffix}`, scanRunId: scanRun.id, fileName: 'synthetic.csv', format: 'CSV', sizeBytes: 100, importedAt: stamp, detectedTool: scanRun.tool, detection: { level: 'high', score: 1, reasons: ['synthetic'] }, processedWith: { parserId: 'csv', parserVersion: 'synthetic', profileId: 'purple-knight', profileVersion: 'synthetic', mappingVersion: 'synthetic' }, sourceArtifact: { kind: 'not-retained', reason: 'Synthetic integration fixture' } }], sourceTables: [{ id: `synthetic-table-${suffix}`, importFileId: `synthetic-file-${suffix}`, name: 'Synthetic', index: 0, rowCount: 1, columnCount: 2, attributes: {} }], findingDefinitions: [{ id: `synthetic-definition-${suffix}`, canonicalKey: `purple-knight:synthetic-${suffix}`, title: 'SMB signing disabled', description: 'Synthetic public-topic fixture', category: 'Synthetic', frameworkReferences: [], createdAt: stamp, updatedAt: stamp }], findingOccurrences: [{ id: occurrenceId, definitionId: `synthetic-definition-${suffix}`, scanRunId: scanRun.id, importFileId: `synthetic-file-${suffix}`, sourceTableId: `synthetic-table-${suffix}`, sourceIdentity: { tool: scanRun.tool, originalRuleId: 'SYNTHETIC-SMB', originalTitle: 'SMB signing disabled' }, sourcePosition: { rowIndex: 1 }, originalFields: [{ key: 'title', label: 'Title', value: 'SMB signing disabled', columnIndex: 0 }], fieldMappings: [], severity: { originalValue: 'high', normalizedValue: 'high', provenance: { decisionSource: 'source-native', confidence: { level: 'high', score: 1, reasons: ['synthetic'] }, mappingVersion: 'synthetic' } }, status: { originalValue: 'Open', normalizedValue: 'open', provenance: { decisionSource: 'source-native', confidence: { level: 'high', score: 1, reasons: ['synthetic'] }, mappingVersion: 'synthetic' } } }] }); await session.commit(); const resultProjectionId = `scanner-finding:${occurrenceId}`; await service.start(project.id, { resultProjectionId }); await service.close(); const result = await service.get(project.id, resultProjectionId); expect(result.run?.status, result.run?.errorCode).toBe('succeeded'); expect(result.run?.candidates).toHaveLength(8); expect(result.run?.candidates.every(candidate => candidate.evidence.some(item => item.sourceStandard === 'ISO/IEC 27001'))).toBe(true); expect(result.run?.result?.mappings.length).toBeLessThanOrEqual(5); expect(result.run?.result?.mappings.every(mapping => { const candidate = result.run?.candidates.find(item => item.controlId === mapping.controlId); return Boolean(candidate && mapping.evidenceChunkIds.every(id => candidate.evidence.some(item => item.chunkId === id))); })).toBe(true); const candidate = result.run!.candidates[0]; const ruleKey = result.run!.ruleKey; const reviewed = await service.decide(project.id, { resultProjectionId, controlId: candidate.controlId, decision: 'accepted', comment: 'Synthetic review.' }); expect(reviewed.decisions[0]).toMatchObject({ controlId: candidate.controlId, decision: 'accepted' }); // Omitting expectedLatestDecisionId conflicts with the existing global decision for this rule+control. await expect(repository.createRegulatoryMappingDecision({ version: 1, id: `synthetic-conflict-${suffix}`, ruleKey, sourceTool: 'purple-knight', sourceRuleId: 'SYNTHETIC-SMB', mappingId: result.run!.id, controlId: candidate.controlId, decision: 'rejected', createdAt: new Date().toISOString() })).rejects.toBeInstanceOf(RepositoryConflictError); await repository.deleteProject(project.id, { projectId: project.id, cascade: true }); deleted = true; // Regulatory mappings and decisions are global: they survive project deletion. await expect(repository.getRegulatoryMappingRun(result.run!.id)).resolves.not.toBeNull(); expect((await repository.listRegulatoryMappingDecisions(ruleKey)).length).toBeGreaterThan(0); } finally { await service.close(); if (!deleted) await repository.deleteProject(project.id, { projectId: project.id, cascade: true }).catch(() => false); await pool.end(); } }, 120_000); });