Assessor_2.0/tests/regmap/regulation-retrieval-servic...

183 lines
8.3 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
RegulationMappingUnavailableError,
readRegulationMappingConfig,
type RegulationMappingConfig
} from '../../server/regmap/regulation-mapping';
import { RegulationRetrievalService, groupHitsByUnit } from '../../server/regmap/regulation-retrieval-service';
import type { EmbeddingClient } from '../../server/regmap/embedding-client';
import type { QdrantCollectionInfo, QdrantSearchHit } from '../../server/regmap/qdrant-client';
const baseConfig: RegulationMappingConfig = {
qdrantUrl: 'http://qdrant:6333',
collection: 'regulation_catalog_v1',
embeddingBaseUrl: 'http://embed:8080/v1',
embeddingModel: 'test-embed',
embeddingDimensions: 3,
catalogVersion: 'TEST/2024',
requestTimeoutMs: 1000,
retrievalChunkLimit: 20,
candidateUnitLimit: 5,
maximumQueryCharacters: 4000
};
function fakeEmbedding(vector: number[] = [1, 0, 0]): EmbeddingClient {
return { embed: vi.fn(async (texts: string[]) => texts.map(() => vector)) };
}
function fakeQdrant(info: QdrantCollectionInfo, hits: QdrantSearchHit[] = []) {
return {
collectionInfo: vi.fn(async () => info),
search: vi.fn(async () => hits)
};
}
const hit = (unitId: string, score: number, extra: Record<string, unknown> = {}): QdrantSearchHit => ({
id: `${unitId}-point`,
score,
payload: { unit_id: unitId, control_id: unitId, control_title: `Control ${unitId}`, chunk_id: `${unitId}#1`, ...extra }
});
describe('readRegulationMappingConfig', () => {
it('is disabled without an embedding endpoint', () => {
expect(readRegulationMappingConfig({})).toEqual({ state: 'disabled' });
expect(readRegulationMappingConfig({ ASSESSOR_QDRANT_URL: 'http://qdrant:6333' })).toEqual({ state: 'disabled' });
});
it('is misconfigured when the embedding endpoint is set but required values are missing', () => {
const result = readRegulationMappingConfig({ ASSESSOR_EMBEDDING_BASE_URL: 'http://embed:8080/v1' });
expect(result.state).toBe('error');
if (result.state === 'error') expect(result.detailCode).toBe('config_incomplete');
});
it('parses a complete configuration and applies defaults', () => {
const result = readRegulationMappingConfig({
ASSESSOR_QDRANT_URL: 'http://qdrant:6333/',
ASSESSOR_EMBEDDING_BASE_URL: 'http://embed:8080/v1/',
ASSESSOR_EMBEDDING_MODEL: 'bge-m3',
ASSESSOR_EMBEDDING_DIMENSIONS: '1024',
ASSESSOR_REGMAP_CATALOG_VERSION: 'DIN EN ISO/IEC 27002:2024-01'
});
expect(result.state).toBe('ready');
if (result.state === 'ready') {
expect(result.config.qdrantUrl).toBe('http://qdrant:6333');
expect(result.config.embeddingBaseUrl).toBe('http://embed:8080/v1');
expect(result.config.collection).toBe('regulation_catalog_v1');
expect(result.config.embeddingDimensions).toBe(1024);
expect(result.config.candidateUnitLimit).toBe(5);
}
});
it('rejects an invalid embedding dimension', () => {
const result = readRegulationMappingConfig({
ASSESSOR_QDRANT_URL: 'http://qdrant:6333',
ASSESSOR_EMBEDDING_BASE_URL: 'http://embed:8080/v1',
ASSESSOR_EMBEDDING_MODEL: 'bge-m3',
ASSESSOR_EMBEDDING_DIMENSIONS: 'not-a-number',
ASSESSOR_REGMAP_CATALOG_VERSION: 'X'
});
expect(result.state).toBe('error');
});
});
describe('RegulationRetrievalService capability', () => {
it('reports collection_missing when the collection does not exist', async () => {
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: fakeEmbedding(),
qdrantClient: fakeQdrant({ exists: false })
});
const report = await service.refreshCapability();
expect(report).toMatchObject({ capability: 'misconfigured', detailCode: 'collection_missing' });
});
it('reports dimension_mismatch when the stored vector size differs', async () => {
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: fakeEmbedding(),
qdrantClient: fakeQdrant({ exists: true, vectorSize: 768, distance: 'Cosine' })
});
expect(await service.refreshCapability()).toMatchObject({ capability: 'misconfigured', detailCode: 'dimension_mismatch' });
});
it('reports distance_mismatch when the collection is not cosine', async () => {
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: fakeEmbedding(),
qdrantClient: fakeQdrant({ exists: true, vectorSize: 3, distance: 'Dot' })
});
expect(await service.refreshCapability()).toMatchObject({ capability: 'misconfigured', detailCode: 'distance_mismatch' });
});
it('is ready when the collection matches and the embedding probe succeeds', async () => {
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: fakeEmbedding(),
qdrantClient: fakeQdrant({ exists: true, vectorSize: 3, distance: 'Cosine' })
});
expect(await service.refreshCapability()).toMatchObject({ capability: 'ready', catalogVersion: 'TEST/2024', embeddingModel: 'test-embed' });
});
it('surfaces an embedding failure as misconfigured', async () => {
const embedding: EmbeddingClient = { embed: vi.fn(async () => { throw Object.assign(new Error('down'), { code: 'embedding_unreachable' }); }) };
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: embedding,
qdrantClient: fakeQdrant({ exists: true, vectorSize: 3, distance: 'Cosine' })
});
expect(await service.refreshCapability()).toMatchObject({ capability: 'misconfigured', detailCode: 'embedding_unreachable' });
});
});
describe('groupHitsByUnit', () => {
it('groups chunks to their unit, scoring each unit by its best chunk', () => {
const grouped = groupHitsByUnit([hit('A', 0.5), hit('B', 0.9), hit('A', 0.7)], 5);
expect(grouped.map(c => [c.unitId, c.score])).toEqual([['B', 0.9], ['A', 0.7]]);
});
it('is deterministic on ties and honours the unit limit', () => {
const grouped = groupHitsByUnit([hit('B', 0.8), hit('A', 0.8), hit('C', 0.4)], 2);
expect(grouped.map(c => c.unitId)).toEqual(['A', 'B']);
});
it('ignores hits without a unit id', () => {
const grouped = groupHitsByUnit([{ id: 'x', score: 0.9, payload: {} }, hit('A', 0.3)], 5);
expect(grouped.map(c => c.unitId)).toEqual(['A']);
});
});
describe('RegulationRetrievalService retrieve', () => {
it('embeds the query and returns grouped candidate units', async () => {
const embedding = fakeEmbedding([0, 1, 0]);
const qdrant = fakeQdrant({ exists: true, vectorSize: 3, distance: 'Cosine' }, [hit('8.24', 0.91), hit('5.1', 0.42)]);
const service = new RegulationRetrievalService(baseConfig, { embeddingClient: embedding, qdrantClient: qdrant });
await service.refreshCapability();
const result = await service.retrieve({ title: 'RC4 enabled on DC', description: 'Weak cipher permitted' });
expect(result.catalogVersion).toBe('TEST/2024');
expect(result.candidates.map(c => c.unitId)).toEqual(['8.24', '5.1']);
expect(result.candidates[0]).toMatchObject({ controlId: '8.24', controlTitle: 'Control 8.24', bestChunkId: '8.24#1' });
expect(embedding.embed).toHaveBeenCalledWith(expect.arrayContaining([expect.stringContaining('RC4 enabled on DC')]), 'query');
});
it('throws RegulationMappingUnavailableError when not ready', async () => {
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: fakeEmbedding(),
qdrantClient: fakeQdrant({ exists: false })
});
await expect(service.retrieve({ title: 'anything' })).rejects.toBeInstanceOf(RegulationMappingUnavailableError);
});
it('does not log the query text', async () => {
const logs: unknown[] = [];
const spies = [
vi.spyOn(console, 'log').mockImplementation((...args) => { logs.push(...args); }),
vi.spyOn(console, 'error').mockImplementation((...args) => { logs.push(...args); }),
vi.spyOn(console, 'warn').mockImplementation((...args) => { logs.push(...args); })
];
const service = new RegulationRetrievalService(baseConfig, {
embeddingClient: fakeEmbedding([0, 1, 0]),
qdrantClient: fakeQdrant({ exists: true, vectorSize: 3, distance: 'Cosine' }, [hit('8.24', 0.91)])
});
await service.refreshCapability();
await service.retrieve({ title: 'SECRET-MARKER-TITLE', description: 'SECRET-MARKER-DESC' });
spies.forEach(spy => spy.mockRestore());
expect(logs.join(' ')).not.toContain('SECRET-MARKER');
});
});