Files
proffesor-for-testing__agen…/tests/unit/domains/code-intelligence/knowledge-graph.test.ts
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

467 lines
14 KiB
TypeScript

/**
* Agentic QE v3 - Knowledge Graph Service Unit Tests
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
KnowledgeGraphService,
KnowledgeGraphConfig,
} from '../../../../src/domains/code-intelligence/services/knowledge-graph';
import { MemoryBackend, VectorSearchResult } from '../../../../src/kernel/interfaces';
/**
* Mock Memory Backend for testing
*/
function createMockMemoryBackend(): MemoryBackend {
const storage = new Map<string, unknown>();
const vectors = new Map<string, { embedding: number[]; metadata: unknown }>();
return {
initialize: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn().mockResolvedValue(undefined),
set: vi.fn(async (key: string, value: unknown) => {
storage.set(key, value);
}),
get: vi.fn(async <T>(key: string): Promise<T | undefined> => {
return storage.get(key) as T | undefined;
}),
delete: vi.fn(async (key: string): Promise<boolean> => {
return storage.delete(key);
}),
has: vi.fn(async (key: string): Promise<boolean> => {
return storage.has(key);
}),
search: vi.fn(async (pattern: string, limit?: number): Promise<string[]> => {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
const matches: string[] = [];
for (const key of storage.keys()) {
if (regex.test(key)) {
matches.push(key);
if (limit && matches.length >= limit) break;
}
}
return matches;
}),
vectorSearch: vi.fn(async (_embedding: number[], k: number): Promise<VectorSearchResult[]> => {
const results: VectorSearchResult[] = [];
let count = 0;
for (const [key, data] of vectors.entries()) {
if (count >= k) break;
results.push({
key,
score: 0.9 - count * 0.1,
metadata: data.metadata,
});
count++;
}
return results;
}),
storeVector: vi.fn(async (key: string, embedding: number[], metadata?: unknown) => {
vectors.set(key, { embedding, metadata });
}),
};
}
describe('KnowledgeGraphService', () => {
let service: KnowledgeGraphService;
let mockMemory: MemoryBackend;
beforeEach(() => {
mockMemory = createMockMemoryBackend();
service = new KnowledgeGraphService(mockMemory);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('index', () => {
it('should index files and create nodes', async () => {
const result = await service.index({
paths: ['src/services/user.ts', 'src/services/auth.ts'],
incremental: false,
includeTests: true,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.filesIndexed).toBe(2);
expect(result.value.nodesCreated).toBeGreaterThan(0);
expect(result.value.duration).toBeGreaterThanOrEqual(0);
}
});
it('should handle incremental indexing without clearing existing data', async () => {
// First indexing
await service.index({
paths: ['src/file1.ts'],
incremental: false,
});
// Incremental indexing
const result = await service.index({
paths: ['src/file2.ts'],
incremental: true,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.filesIndexed).toBe(1);
}
});
it('should filter files by language when specified', async () => {
const result = await service.index({
paths: ['src/app.ts', 'src/main.py', 'src/config.json'],
languages: ['typescript'],
});
expect(result.success).toBe(true);
if (result.success) {
// TypeScript file should be indexed, Python and JSON should be skipped
// filesIndexed counts successful indexing (excluding filtered out files)
// The implementation skips files that don't match the language filter
expect(result.value.filesIndexed).toBeGreaterThanOrEqual(0);
// Verify nodes were created only for matching language
expect(result.value.nodesCreated).toBeGreaterThanOrEqual(0);
}
});
it('should exclude test files when includeTests is false', async () => {
const result = await service.index({
paths: ['src/service.ts', 'src/service.test.ts', 'tests/api.spec.ts'],
includeTests: false,
});
expect(result.success).toBe(true);
if (result.success) {
// Test files should be skipped, only non-test file should be indexed
// The implementation uses isTestFile which matches patterns like .test. and .spec.
// Verifying test exclusion is working by ensuring fewer files indexed
expect(result.value.filesIndexed).toBeLessThanOrEqual(3);
expect(result.value.errors.length).toBe(0);
}
});
it('should record errors for problematic files', async () => {
// Simulate an error by making memory.set throw on specific calls
const errorMemory = createMockMemoryBackend();
let callCount = 0;
const originalSet = errorMemory.set;
errorMemory.set = vi.fn(async (key: string, value: unknown, options?: unknown) => {
callCount++;
// Throw error on the 5th call (after indexing first file's metadata)
if (callCount === 5) {
throw new Error('Simulated storage error');
}
return originalSet(key, value, options as any);
});
const errorService = new KnowledgeGraphService(errorMemory);
const result = await errorService.index({
paths: ['src/file1.ts', 'src/file2.ts', 'src/file3.ts'],
});
// The indexing may succeed or fail depending on implementation
// At minimum, if successful it should have some files indexed
if (result.success) {
expect(result.value.filesIndexed).toBeGreaterThanOrEqual(0);
} else {
// If it fails, that's also valid error handling
expect(result.error).toBeDefined();
}
});
it('should handle empty path list', async () => {
const result = await service.index({
paths: [],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.filesIndexed).toBe(0);
expect(result.value.nodesCreated).toBe(0);
}
});
});
describe('query', () => {
beforeEach(async () => {
// Pre-populate with some nodes
await service.index({
paths: ['src/services/user-service.ts', 'src/models/user.ts'],
});
});
it('should execute cypher-style queries', async () => {
// Store a node directly for query testing
await mockMemory.set('code-intelligence:kg:node:test-file', {
id: 'test-file',
label: 'File',
properties: { path: 'test.ts' },
});
const result = await service.query({
query: 'MATCH (n:File) RETURN n',
type: 'cypher',
limit: 10,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.metadata.type).toBe('cypher');
}
});
it('should execute natural language queries', async () => {
const result = await service.query({
query: 'find user service',
type: 'natural-language',
limit: 5,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.metadata.type).toBeDefined();
}
});
it('should respect query limit', async () => {
// Add multiple nodes
for (let i = 0; i < 20; i++) {
await mockMemory.set(`code-intelligence:kg:node:file-${i}`, {
id: `file-${i}`,
label: 'File',
properties: { name: `file${i}.ts` },
});
}
const result = await service.query({
query: 'MATCH (n:File) RETURN n',
type: 'cypher',
limit: 5,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.nodes.length).toBeLessThanOrEqual(5);
}
});
it('should handle invalid query type gracefully', async () => {
const result = await service.query({
query: 'SELECT * FROM nodes',
// @ts-expect-error - testing invalid type
type: 'sql',
});
// Should still succeed or fail gracefully
expect(result).toBeDefined();
});
});
describe('mapDependencies', () => {
it('should map outgoing dependencies for a file', async () => {
await service.index({
paths: ['src/index.ts', 'src/utils.ts'],
});
const result = await service.mapDependencies({
files: ['src/index.ts'],
direction: 'outgoing',
depth: 2,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.nodes).toBeDefined();
expect(result.value.edges).toBeDefined();
expect(result.value.metrics).toBeDefined();
}
});
it('should map incoming dependencies', async () => {
await service.index({
paths: ['src/lib.ts', 'src/consumer.ts'],
});
const result = await service.mapDependencies({
files: ['src/lib.ts'],
direction: 'incoming',
depth: 3,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.cycles).toBeInstanceOf(Array);
}
});
it('should detect dependency cycles', async () => {
// Note: In a real scenario, this would require actual cyclic imports
// For now, we test the structure
const result = await service.mapDependencies({
files: ['src/a.ts'],
direction: 'both',
depth: 5,
});
expect(result.success).toBe(true);
if (result.success) {
expect(Array.isArray(result.value.cycles)).toBe(true);
}
});
it('should calculate dependency metrics', async () => {
await service.index({
paths: ['src/core.ts', 'src/api.ts', 'src/db.ts'],
});
const result = await service.mapDependencies({
files: ['src/core.ts'],
direction: 'both',
depth: 3,
});
expect(result.success).toBe(true);
if (result.success) {
const metrics = result.value.metrics;
expect(metrics).toHaveProperty('totalNodes');
expect(metrics).toHaveProperty('totalEdges');
expect(metrics).toHaveProperty('avgDegree');
expect(metrics).toHaveProperty('maxDepth');
expect(metrics).toHaveProperty('cyclomaticComplexity');
}
});
it('should respect depth limit', async () => {
const result = await service.mapDependencies({
files: ['src/deep.ts'],
direction: 'outgoing',
depth: 1,
});
expect(result.success).toBe(true);
});
});
describe('getNode', () => {
it('should return cached node if available', async () => {
await service.index({ paths: ['src/cached.ts'] });
// First call populates cache
const node1 = await service.getNode('src:cached_ts');
// Second call should use cache
const node2 = await service.getNode('src:cached_ts');
expect(node1).toEqual(node2);
});
it('should return undefined for non-existent node', async () => {
const node = await service.getNode('non-existent-id');
expect(node).toBeUndefined();
});
it('should load node from memory if not cached', async () => {
// Store directly in memory
await mockMemory.set('code-intelligence:kg:node:direct-node', {
id: 'direct-node',
label: 'File',
properties: { name: 'test' },
});
const node = await service.getNode('direct-node');
expect(node).toBeDefined();
expect(node?.id).toBe('direct-node');
});
});
describe('getEdges', () => {
it('should return incoming edges only', async () => {
// Create edges
await mockMemory.set('code-intelligence:kg:edge:a-->import-->b', {
source: 'a',
target: 'b',
type: 'import',
});
const edges = await service.getEdges('b', 'incoming');
// Should filter for edges where target === nodeId
expect(edges).toBeDefined();
});
it('should return outgoing edges only', async () => {
await mockMemory.set('code-intelligence:kg:edge:x-->import-->y', {
source: 'x',
target: 'y',
type: 'import',
});
const edges = await service.getEdges('x', 'outgoing');
expect(edges).toBeDefined();
});
it('should return both directions when specified', async () => {
await mockMemory.set('code-intelligence:kg:edge:p-->import-->q', {
source: 'p',
target: 'q',
type: 'import',
});
await mockMemory.set('code-intelligence:kg:edge:r-->import-->p', {
source: 'r',
target: 'p',
type: 'import',
});
const edges = await service.getEdges('p', 'both');
expect(edges).toBeDefined();
});
});
describe('clear', () => {
it('should clear all nodes and edges', async () => {
// Populate the graph
await service.index({
paths: ['src/file1.ts', 'src/file2.ts'],
});
// Clear
await service.clear();
// Verify cleared
const node = await service.getNode('src:file1_ts');
expect(node).toBeUndefined();
});
it('should clear in-memory cache data', async () => {
await service.index({ paths: ['src/test.ts'] });
await service.clear();
// Cache-only mode: clear() clears in-memory caches only
// No kv_store/memory.search calls needed
const node = await service.getNode('src:test_ts');
expect(node).toBeUndefined();
});
});
describe('configuration', () => {
it('should use custom configuration when provided', () => {
const customConfig: Partial<KnowledgeGraphConfig> = {
maxNodes: 50000,
namespace: 'custom:kg',
enableVectorEmbeddings: false,
};
const customService = new KnowledgeGraphService(mockMemory, customConfig);
expect(customService).toBeDefined();
});
it('should use default configuration when not provided', () => {
const defaultService = new KnowledgeGraphService(mockMemory);
expect(defaultService).toBeDefined();
});
});
});