Files
lcbp3/backend/tests/integration/cross-spec/qdrant-isolation.spec.ts
T
admin 1a162bf320
CI / CD Pipeline / build (push) Successful in 4m54s
CI / CD Pipeline / deploy (push) Failing after 12m9s
feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
2026-05-16 10:59:53 +07:00

173 lines
5.8 KiB
TypeScript

// File: backend/tests/integration/cross-spec/qdrant-isolation.spec.ts
// Change Log:
// - 2026-05-16: Cross-spec integration test for QdrantService projectPublicId isolation
// - 2026-05-16: Fixed mocking strategy to use factory pattern with proper method exposure
// Define types for Qdrant mock responses
interface QdrantSearchResult {
id: string;
payload: Record<string, unknown>;
score: number;
}
// Create mock functions that can be spied on
const mockSearch = jest.fn();
const mockGetCollections = jest.fn().mockResolvedValue({ collections: [] });
const mockCreateCollection = jest.fn().mockResolvedValue(true);
const mockCreatePayloadIndex = jest.fn().mockResolvedValue(true);
// Mock QdrantClient before importing the service
jest.mock('@qdrant/js-client-rest', () => ({
QdrantClient: jest.fn().mockImplementation(() => ({
getCollections: mockGetCollections,
createCollection: mockCreateCollection,
createPayloadIndex: mockCreatePayloadIndex,
search: mockSearch,
delete: jest.fn().mockResolvedValue(true),
upsert: jest.fn().mockResolvedValue(true),
})),
}));
import { Test, TestingModule } from '@nestjs/testing';
import { AiQdrantService } from '../../../src/modules/ai/qdrant.service';
import { ConfigService } from '@nestjs/config';
describe('Cross-Spec: QdrantService Isolation', () => {
let service: AiQdrantService;
beforeEach(async () => {
// Reset mocks before each test
mockSearch.mockReset();
mockGetCollections.mockResolvedValue({ collections: [] });
const module: TestingModule = await Test.createTestingModule({
providers: [
AiQdrantService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
const config: Record<string, string> = {
AI_QDRANT_URL: 'http://192.168.10.100:6333',
QDRANT_URL: 'http://192.168.10.100:6333',
};
return config[key];
}),
},
},
],
}).compile();
service = module.get<AiQdrantService>(AiQdrantService);
});
it('should enforce projectPublicId as required parameter in search', async () => {
// Test that search() signature requires projectPublicId
const searchMethod = service.search;
// Get parameter names from function signature
const fnStr = searchMethod.toString();
// Assert: projectPublicId must be first parameter
expect(fnStr).toContain('projectPublicId');
// Act: Verify search calls Qdrant with projectPublicId filter
const mockResponse = [
{
id: 'doc-1',
payload: { document_public_id: 'doc-1', project_public_id: 'proj-a' },
score: 0.95,
},
];
mockSearch.mockResolvedValue(mockResponse as QdrantSearchResult[]);
await service.search('proj-a', [0.1, 0.2, 0.3], 5);
// Assert: Qdrant client call includes project_public_id filter
expect(mockSearch).toHaveBeenCalledWith(
'lcbp3_vectors',
expect.objectContaining({
filter: {
must: [{ key: 'project_public_id', match: { value: 'proj-a' } }],
},
})
);
});
it('should isolate results between different projects', async () => {
// Arrange: Mock Qdrant responses for two projects
const projectAResponse = [
{ id: 'doc-a1', payload: { project_public_id: 'proj-a' }, score: 0.9 },
{ id: 'doc-a2', payload: { project_public_id: 'proj-a' }, score: 0.85 },
];
const projectBResponse = [
{ id: 'doc-b1', payload: { project_public_id: 'proj-b' }, score: 0.92 },
];
// Act: Query Project A
mockSearch.mockResolvedValueOnce(projectAResponse as QdrantSearchResult[]);
const resultA = await service.search('proj-a', [0.1, 0.2], 5);
// Act: Query Project B
mockSearch.mockResolvedValueOnce(projectBResponse as QdrantSearchResult[]);
const resultB = await service.search('proj-b', [0.1, 0.2], 5);
// Assert: Results are isolated by project
expect(resultA.every((r) => r.payload.project_public_id === 'proj-a')).toBe(
true
);
expect(resultB.every((r) => r.payload.project_public_id === 'proj-b')).toBe(
true
);
// Assert: Different filters used for each project
const call1 = mockSearch.mock.calls[0] as unknown[];
const call2 = mockSearch.mock.calls[1] as unknown[];
type FilterArg = { filter: { must: Array<{ match: { value: string } }> } };
expect((call1[1] as FilterArg).filter.must[0].match.value).toBe('proj-a');
expect((call2[1] as FilterArg).filter.must[0].match.value).toBe('proj-b');
});
it('should verify no rawSearch method exists (security)', () => {
// Assert: No rawSearch method that bypasses projectPublicId filtering
expect((service as Record<string, unknown>).rawSearch).toBeUndefined();
});
it('should handle RFA cross-spec usage correctly', async () => {
// Simulate RFA feature using QdrantService for document context
const mockEmbedding: number[] = new Array(768).fill(0.1);
const mockResponse = [
{
id: 'related-doc-1',
payload: {
document_public_id: 'rel-1',
project_public_id: 'shared-proj',
content_preview: 'Related document content',
},
score: 0.88,
},
];
mockSearch.mockResolvedValue(mockResponse as QdrantSearchResult[]);
// RFA feature queries for related documents
const result = await service.search('shared-proj', mockEmbedding, 5);
// Assert: Results are scoped to project
expect(result[0].payload.project_public_id).toBe('shared-proj');
// Assert: Filter was applied
expect(mockSearch).toHaveBeenCalledWith(
'lcbp3_vectors',
expect.objectContaining({
filter: {
must: [{ key: 'project_public_id', match: { value: 'shared-proj' } }],
},
})
);
});
});