mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
cc5220c5a0
Resolves the two remaining gaps from the audit.
Namespace divergence (CLI↔MCP parity):
KnowledgeGraphService persisted nodes/edges under the backend's DEFAULT
namespace — 'qe-kernel' for the CLI kernel, 'mcp-tools' for the MCP shared
backend — so a graph indexed by one tool was invisible to the other, and the
init existence check (`SELECT ... FROM kv_store WHERE namespace =
'code-intelligence:kg'`) always returned 0 because the data was elsewhere.
Now every KG kv op passes an explicit `{ namespace: 'code-intelligence:kg' }`
(via a single `nsOpts` getter so reads and writes can't drift apart — issue
#491 Bug 2). The two external readers that searched the default namespace by key
prefix (impact-analyzer test discovery, test-generator KG-context) now read that
namespace too. Verified: both `aqe code index` and the MCP `code_index` tool
land KG rows in `code-intelligence:kg`; deps/search still work; the init
`hasCodeIntelligenceIndex` check now finds the index.
FileReader stale content on incremental re-index:
indexFile() now evicts the file from the FileReader content cache before reading
it. The reader caches content for 5 min, so a re-index in a long-lived process
(the MCP server) could otherwise extract from pre-edit text. The two reads
within indexFile (entities + imports) still share one cache fill, so they stay
consistent. The incremental regression test now re-indexes on the SAME instance,
exercising both removeFileFromGraph and the cache eviction.
Adds a regression test asserting KG data is stored under 'code-intelligence:kg'
regardless of the backend's default namespace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
740 lines
27 KiB
TypeScript
740 lines
27 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Knowledge Graph Real I/O Integration Tests
|
|
* Tests that perform actual file system operations on the v3 codebase
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import path from 'path';
|
|
import fs from 'fs/promises';
|
|
import { KnowledgeGraphService } from '../../../src/domains/code-intelligence/services/knowledge-graph';
|
|
import type { MemoryBackend, VectorSearchResult } from '../../../src/kernel/interfaces';
|
|
|
|
// ============================================================================
|
|
// Mock Memory Backend with Persistence Simulation
|
|
// ============================================================================
|
|
|
|
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 });
|
|
}),
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// File System Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Recursively get all TypeScript files in a directory
|
|
*/
|
|
async function getTypeScriptFiles(dir: string): Promise<string[]> {
|
|
const files: string[] = [];
|
|
|
|
async function walk(currentDir: string) {
|
|
try {
|
|
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(currentDir, entry.name);
|
|
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
await walk(fullPath);
|
|
} else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
} catch {
|
|
// Directory doesn't exist or can't be read
|
|
}
|
|
}
|
|
|
|
await walk(dir);
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Extract entities from actual TypeScript file content
|
|
*/
|
|
async function extractEntitiesFromFile(filePath: string): Promise<ExtractedEntity[]> {
|
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
const entities: ExtractedEntity[] = [];
|
|
|
|
// Extract class declarations - check for export keyword properly
|
|
const classMatches = content.matchAll(/(export\s+)?class\s+(\w+)/g);
|
|
for (const match of classMatches) {
|
|
entities.push({
|
|
type: 'class',
|
|
name: match[2],
|
|
file: filePath,
|
|
exported: match[1] !== undefined,
|
|
});
|
|
}
|
|
|
|
// Extract interface declarations
|
|
const interfaceMatches = content.matchAll(/(export\s+)?interface\s+(\w+)/g);
|
|
for (const match of interfaceMatches) {
|
|
entities.push({
|
|
type: 'interface',
|
|
name: match[2],
|
|
file: filePath,
|
|
exported: match[1] !== undefined,
|
|
});
|
|
}
|
|
|
|
// Extract function declarations
|
|
const functionMatches = content.matchAll(/(export\s+)?(?:async\s+)?function\s+(\w+)/g);
|
|
for (const match of functionMatches) {
|
|
entities.push({
|
|
type: 'function',
|
|
name: match[2],
|
|
file: filePath,
|
|
exported: match[1] !== undefined,
|
|
});
|
|
}
|
|
|
|
// Extract type declarations
|
|
const typeMatches = content.matchAll(/(export\s+)?type\s+(\w+)/g);
|
|
for (const match of typeMatches) {
|
|
entities.push({
|
|
type: 'type',
|
|
name: match[2],
|
|
file: filePath,
|
|
exported: match[1] !== undefined,
|
|
});
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
interface ExtractedEntity {
|
|
type: 'class' | 'interface' | 'function' | 'type';
|
|
name: string;
|
|
file: string;
|
|
exported: boolean;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests
|
|
// ============================================================================
|
|
|
|
describe('Knowledge Graph Real I/O', () => {
|
|
let service: KnowledgeGraphService;
|
|
let memory: MemoryBackend;
|
|
const v3SrcPath = path.join(__dirname, '../../../src');
|
|
|
|
beforeEach(() => {
|
|
memory = createMockMemoryBackend();
|
|
service = new KnowledgeGraphService(memory, {
|
|
maxNodes: 10000,
|
|
enableVectorEmbeddings: false, // Disable for faster tests
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await service.clear();
|
|
});
|
|
|
|
describe('indexing actual v3 source files', () => {
|
|
it('should index kernel directory and create nodes', async () => {
|
|
const kernelPath = path.join(v3SrcPath, 'kernel');
|
|
const files = await getTypeScriptFiles(kernelPath);
|
|
|
|
expect(files.length).toBeGreaterThan(0);
|
|
|
|
const result = await service.index({
|
|
paths: files,
|
|
incremental: false,
|
|
includeTests: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.filesIndexed).toBeGreaterThan(0);
|
|
expect(result.value.nodesCreated).toBeGreaterThan(0);
|
|
expect(result.value.duration).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should index domains directory with multiple files', async () => {
|
|
const domainsPath = path.join(v3SrcPath, 'domains');
|
|
const files = await getTypeScriptFiles(domainsPath);
|
|
|
|
expect(files.length).toBeGreaterThan(10); // Should have many domain files
|
|
|
|
const result = await service.index({
|
|
paths: files,
|
|
incremental: false,
|
|
includeTests: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.filesIndexed).toBeGreaterThan(10);
|
|
expect(result.value.nodesCreated).toBeGreaterThan(10);
|
|
}
|
|
});
|
|
|
|
it('should index coordination files', async () => {
|
|
const coordPath = path.join(v3SrcPath, 'coordination');
|
|
const files = await getTypeScriptFiles(coordPath);
|
|
|
|
const result = await service.index({
|
|
paths: files,
|
|
incremental: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.filesIndexed).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should perform incremental indexing correctly', async () => {
|
|
const kernelPath = path.join(v3SrcPath, 'kernel');
|
|
const kernelFiles = await getTypeScriptFiles(kernelPath);
|
|
|
|
// First indexing
|
|
const firstResult = await service.index({
|
|
paths: kernelFiles.slice(0, 3),
|
|
incremental: false,
|
|
});
|
|
|
|
expect(firstResult.success).toBe(true);
|
|
|
|
// Incremental indexing
|
|
const secondResult = await service.index({
|
|
paths: kernelFiles.slice(3),
|
|
incremental: true,
|
|
});
|
|
|
|
expect(secondResult.success).toBe(true);
|
|
if (secondResult.success && firstResult.success) {
|
|
// Both should have created nodes
|
|
expect(firstResult.value.nodesCreated).toBeGreaterThan(0);
|
|
expect(secondResult.value.nodesCreated).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should handle non-existent files gracefully', async () => {
|
|
const result = await service.index({
|
|
paths: ['/non/existent/file.ts', '/another/missing/file.ts'],
|
|
incremental: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
// Should record errors for missing files
|
|
expect(result.value.errors.length).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('extracting real entities from domain services', () => {
|
|
it('should extract entities from test-generator.ts', async () => {
|
|
const filePath = path.join(v3SrcPath, 'domains/test-generation/services/test-generator.ts');
|
|
const entities = await extractEntitiesFromFile(filePath);
|
|
|
|
expect(entities.length).toBeGreaterThan(0);
|
|
|
|
// Should have TestGeneratorService class
|
|
const serviceClass = entities.find(e => e.type === 'class' && e.name === 'TestGeneratorService');
|
|
expect(serviceClass).toBeDefined();
|
|
|
|
// Should have ITestGenerationService interface
|
|
const serviceInterface = entities.find(e => e.type === 'interface' && e.name === 'ITestGenerationService');
|
|
expect(serviceInterface).toBeDefined();
|
|
});
|
|
|
|
it('should extract entities from security-scanner.ts', async () => {
|
|
// Note: SecurityScannerService was refactored into scanners/scanner-orchestrator.ts
|
|
// The original security-scanner.ts is now a facade that re-exports
|
|
const filePath = path.join(v3SrcPath, 'domains/security-compliance/services/scanners/scanner-orchestrator.ts');
|
|
const entities = await extractEntitiesFromFile(filePath);
|
|
|
|
expect(entities.length).toBeGreaterThan(0);
|
|
|
|
const serviceClass = entities.find(e => e.type === 'class' && e.name === 'SecurityScannerService');
|
|
expect(serviceClass).toBeDefined();
|
|
expect(serviceClass!.exported).toBe(true);
|
|
});
|
|
|
|
it('should extract entities from knowledge-graph.ts', async () => {
|
|
const filePath = path.join(v3SrcPath, 'domains/code-intelligence/services/knowledge-graph.ts');
|
|
const entities = await extractEntitiesFromFile(filePath);
|
|
|
|
expect(entities.length).toBeGreaterThan(0);
|
|
|
|
const serviceClass = entities.find(e => e.type === 'class' && e.name === 'KnowledgeGraphService');
|
|
expect(serviceClass).toBeDefined();
|
|
|
|
const serviceInterface = entities.find(e => e.type === 'interface' && e.name === 'IKnowledgeGraphService');
|
|
expect(serviceInterface).toBeDefined();
|
|
});
|
|
|
|
it('should extract all exported types from interfaces.ts', async () => {
|
|
const filePath = path.join(v3SrcPath, 'kernel/interfaces.ts');
|
|
const entities = await extractEntitiesFromFile(filePath);
|
|
|
|
// Should have many interfaces
|
|
const interfaces = entities.filter(e => e.type === 'interface');
|
|
expect(interfaces.length).toBeGreaterThan(5);
|
|
|
|
// Key interfaces should be present
|
|
const interfaceNames = interfaces.map(i => i.name);
|
|
expect(interfaceNames).toContain('DomainPlugin');
|
|
expect(interfaceNames).toContain('EventBus');
|
|
expect(interfaceNames).toContain('MemoryBackend');
|
|
});
|
|
});
|
|
|
|
describe('querying the knowledge graph', () => {
|
|
beforeEach(async () => {
|
|
// Index some real files first
|
|
const kernelPath = path.join(v3SrcPath, 'kernel');
|
|
const files = await getTypeScriptFiles(kernelPath);
|
|
await service.index({
|
|
paths: files.slice(0, 5),
|
|
incremental: false,
|
|
});
|
|
});
|
|
|
|
it('should query for File nodes using cypher-style query', async () => {
|
|
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 query using natural language', async () => {
|
|
const result = await service.query({
|
|
query: 'find event bus',
|
|
type: 'natural-language',
|
|
limit: 5,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.metadata).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('dependency mapping', () => {
|
|
beforeEach(async () => {
|
|
const coordPath = path.join(v3SrcPath, 'coordination');
|
|
const files = await getTypeScriptFiles(coordPath);
|
|
await service.index({
|
|
paths: files,
|
|
incremental: false,
|
|
});
|
|
});
|
|
|
|
it('should map dependencies for coordination files', async () => {
|
|
const result = await service.mapDependencies({
|
|
files: ['src/coordination/cross-domain-router.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();
|
|
expect(result.value.metrics.totalNodes).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
|
|
it('should calculate dependency metrics', async () => {
|
|
const result = await service.mapDependencies({
|
|
files: ['src/coordination/protocol-executor.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');
|
|
expect(metrics.cyclomaticComplexity).toBeGreaterThanOrEqual(1);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('file statistics', () => {
|
|
it('should count TypeScript files in v3 src directory', async () => {
|
|
const files = await getTypeScriptFiles(v3SrcPath);
|
|
|
|
// v3 should have a substantial number of source files
|
|
expect(files.length).toBeGreaterThan(50);
|
|
|
|
// All files should be .ts or .tsx
|
|
for (const file of files) {
|
|
expect(file).toMatch(/\.tsx?$/);
|
|
}
|
|
});
|
|
|
|
it('should have kernel files', async () => {
|
|
const kernelFiles = await getTypeScriptFiles(path.join(v3SrcPath, 'kernel'));
|
|
|
|
expect(kernelFiles.length).toBeGreaterThan(5);
|
|
expect(kernelFiles.some(f => f.includes('event-bus'))).toBe(true);
|
|
expect(kernelFiles.some(f => f.includes('interfaces'))).toBe(true);
|
|
});
|
|
|
|
it('should have domain files organized by domain', async () => {
|
|
const domainsPath = path.join(v3SrcPath, 'domains');
|
|
const domainFiles = await getTypeScriptFiles(domainsPath);
|
|
|
|
// Should have files from multiple domains
|
|
expect(domainFiles.some(f => f.includes('test-generation'))).toBe(true);
|
|
expect(domainFiles.some(f => f.includes('coverage-analysis'))).toBe(true);
|
|
expect(domainFiles.some(f => f.includes('security-compliance'))).toBe(true);
|
|
expect(domainFiles.some(f => f.includes('code-intelligence'))).toBe(true);
|
|
});
|
|
|
|
it('should have coordination files', async () => {
|
|
const coordFiles = await getTypeScriptFiles(path.join(v3SrcPath, 'coordination'));
|
|
|
|
expect(coordFiles.length).toBeGreaterThan(5);
|
|
expect(coordFiles.some(f => f.includes('cross-domain-router'))).toBe(true);
|
|
expect(coordFiles.some(f => f.includes('protocol-executor'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('entity extraction statistics', () => {
|
|
it('should extract significant number of entities from kernel', async () => {
|
|
const kernelFiles = await getTypeScriptFiles(path.join(v3SrcPath, 'kernel'));
|
|
let totalEntities = 0;
|
|
|
|
for (const file of kernelFiles) {
|
|
try {
|
|
const entities = await extractEntitiesFromFile(file);
|
|
totalEntities += entities.length;
|
|
} catch {
|
|
// Skip files that can't be read
|
|
}
|
|
}
|
|
|
|
expect(totalEntities).toBeGreaterThan(20);
|
|
});
|
|
|
|
it('should find exported classes across domains', async () => {
|
|
const domainFiles = await getTypeScriptFiles(path.join(v3SrcPath, 'domains'));
|
|
const exportedClasses: string[] = [];
|
|
|
|
for (const file of domainFiles) {
|
|
try {
|
|
const entities = await extractEntitiesFromFile(file);
|
|
const classes = entities.filter(e => e.type === 'class' && e.exported);
|
|
exportedClasses.push(...classes.map(c => c.name));
|
|
} catch {
|
|
// Skip files that can't be read
|
|
}
|
|
}
|
|
|
|
expect(exportedClasses.length).toBeGreaterThan(10);
|
|
|
|
// Should include key domain services
|
|
expect(exportedClasses).toContain('TestGeneratorService');
|
|
expect(exportedClasses).toContain('SecurityScannerService');
|
|
expect(exportedClasses).toContain('KnowledgeGraphService');
|
|
});
|
|
});
|
|
|
|
describe('codebase structure validation', () => {
|
|
it('should verify v3 follows expected directory structure', async () => {
|
|
const srcDir = v3SrcPath;
|
|
|
|
// Check expected directories exist
|
|
const expectedDirs = ['kernel', 'domains', 'coordination', 'shared'];
|
|
for (const dir of expectedDirs) {
|
|
const dirPath = path.join(srcDir, dir);
|
|
try {
|
|
const stat = await fs.stat(dirPath);
|
|
expect(stat.isDirectory()).toBe(true);
|
|
} catch {
|
|
throw new Error(`Expected directory ${dir} not found in v3/src`);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should verify domain directory structure', async () => {
|
|
const domainsDir = path.join(v3SrcPath, 'domains');
|
|
|
|
const expectedDomains = [
|
|
'test-generation',
|
|
'test-execution',
|
|
'coverage-analysis',
|
|
'quality-assessment',
|
|
'defect-intelligence',
|
|
'code-intelligence',
|
|
'security-compliance',
|
|
'chaos-resilience',
|
|
'contract-testing',
|
|
'learning-optimization',
|
|
'visual-accessibility',
|
|
'requirements-validation',
|
|
];
|
|
|
|
for (const domain of expectedDomains) {
|
|
const domainPath = path.join(domainsDir, domain);
|
|
try {
|
|
const stat = await fs.stat(domainPath);
|
|
expect(stat.isDirectory()).toBe(true);
|
|
} catch {
|
|
throw new Error(`Expected domain directory ${domain} not found`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Regression: #511 — TS/JS dependency edges survive across processes
|
|
//
|
|
// `aqe code index` and `aqe code deps` run as SEPARATE CLI processes, so the
|
|
// graph must be persisted: an index by one service instance must be visible
|
|
// to mapDependencies() on a fresh instance sharing the same backend. Before
|
|
// the fix, nodes/edges were cache-only and `deps` returned 0 edges on TS
|
|
// repos even though `index` reported edges created.
|
|
// ==========================================================================
|
|
describe('#511 cross-instance TS dependency edges', () => {
|
|
let tmpDir: string;
|
|
let sharedMemory: MemoryBackend;
|
|
|
|
beforeEach(async () => {
|
|
sharedMemory = createMockMemoryBackend();
|
|
// Fixture must live under the project root: FileReader (SEC-004) rejects
|
|
// absolute paths outside process.cwd(), so os.tmpdir() would be unreadable.
|
|
tmpDir = await fs.mkdtemp(path.join(__dirname, 'tmp-kg-511-'));
|
|
await fs.writeFile(
|
|
path.join(tmpDir, 'math.ts'),
|
|
'export function add(a: number, b: number): number { return a + b; }\n'
|
|
);
|
|
await fs.writeFile(
|
|
path.join(tmpDir, 'app.ts'),
|
|
"import { add } from './math';\nexport const total = add(1, 2);\n"
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('should expose import edges to a fresh service instance after indexing', async () => {
|
|
// Arrange: index with one service ("the index process")
|
|
const appPath = path.join(tmpDir, 'app.ts');
|
|
const mathPath = path.join(tmpDir, 'math.ts');
|
|
const indexer = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
const indexResult = await indexer.index({ paths: [appPath, mathPath] });
|
|
expect(indexResult.success).toBe(true);
|
|
|
|
// Act: map dependencies with a SEPARATE instance sharing only the backend
|
|
const reader = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
const depsResult = await reader.mapDependencies({
|
|
files: [appPath, mathPath],
|
|
direction: 'both',
|
|
depth: 3,
|
|
});
|
|
|
|
// Assert: the import edge survived persistence and connects the files
|
|
expect(depsResult.success).toBe(true);
|
|
const deps = depsResult.value!;
|
|
expect(deps.metrics.totalEdges).toBeGreaterThan(0);
|
|
|
|
const appNodeId = ':' + appPath.replace(/^[/\\]/, '').replace(/[/\\]/g, ':').replace(/\./g, '_');
|
|
const mathNodeId = ':' + mathPath.replace(/^[/\\]/, '').replace(/[/\\]/g, ':').replace(/\./g, '_');
|
|
const importEdge = deps.edges.find(
|
|
(e) => e.source === appNodeId && e.target === mathNodeId && e.type === 'import'
|
|
);
|
|
expect(importEdge).toBeDefined();
|
|
|
|
// math.ts is depended upon (inDegree > 0), confirming the graph connects
|
|
const mathNode = deps.nodes.find((n) => n.path === mathPath);
|
|
expect(mathNode?.inDegree).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should not report phantom cycles for a simple acyclic import chain', async () => {
|
|
const appPath = path.join(tmpDir, 'app.ts');
|
|
const mathPath = path.join(tmpDir, 'math.ts');
|
|
const indexer = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
await indexer.index({ paths: [appPath, mathPath] });
|
|
|
|
const reader = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
const depsResult = await reader.mapDependencies({
|
|
files: [appPath, mathPath],
|
|
direction: 'both',
|
|
depth: 3,
|
|
});
|
|
|
|
expect(depsResult.success).toBe(true);
|
|
// app → math is acyclic; 'both' traversal must not fabricate A→B→A
|
|
expect(depsResult.value!.cycles).toHaveLength(0);
|
|
});
|
|
|
|
it('should warn when a file has no dependency extractor (#511 req #2)', async () => {
|
|
// Arrange: a Python file alongside the TS files — no TS-Compiler extractor
|
|
const pyPath = path.join(tmpDir, 'util.py');
|
|
await fs.writeFile(pyPath, 'def hello():\n return 1\n');
|
|
const indexer = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
|
|
// Act
|
|
const result = await indexer.index({
|
|
paths: [path.join(tmpDir, 'app.ts'), pyPath],
|
|
});
|
|
|
|
// Assert: a loud warning naming the unsupported extension is surfaced
|
|
expect(result.success).toBe(true);
|
|
const warnings = result.value!.warnings ?? [];
|
|
expect(warnings.some((w) => w.includes(".py"))).toBe(true);
|
|
});
|
|
|
|
it('should drop stale outgoing edges on incremental re-index (#511)', async () => {
|
|
const appPath = path.join(tmpDir, 'app.ts');
|
|
const mathPath = path.join(tmpDir, 'math.ts');
|
|
const indexer = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
await indexer.index({ paths: [appPath, mathPath] });
|
|
|
|
// app.ts no longer imports ./math. The incremental re-index runs on the
|
|
// SAME long-lived instance — exercising both the persistence-layer cleanup
|
|
// (removeFileFromGraph) and the FileReader cache eviction that guarantees
|
|
// the new file content is read (#511).
|
|
await fs.writeFile(appPath, 'export const total = 42;\n');
|
|
await indexer.index({ paths: [appPath, mathPath], incremental: true });
|
|
|
|
// The stale app → math import edge must be gone
|
|
const reader = new KnowledgeGraphService(sharedMemory, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
const depsResult = await reader.mapDependencies({
|
|
files: [appPath, mathPath],
|
|
direction: 'both',
|
|
depth: 3,
|
|
});
|
|
expect(depsResult.success).toBe(true);
|
|
const mathNode = depsResult.value!.nodes.find((n) => n.path === mathPath);
|
|
expect(mathNode?.inDegree).toBe(0);
|
|
});
|
|
|
|
it('should persist under the code-intelligence:kg namespace regardless of backend default (#511)', async () => {
|
|
// A namespace-honoring backend whose DEFAULT namespace is something else
|
|
// entirely (mirrors CLI 'qe-kernel' / MCP 'mcp-tools'). The KG must still
|
|
// write/read under 'code-intelligence:kg' so init checks + CLI↔MCP agree.
|
|
const storage = new Map<string, unknown>();
|
|
const composite = (key: string, ns?: string) => `${ns ?? 'backend-default'}::${key}`;
|
|
const honoring = {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
set: vi.fn(async (k: string, v: unknown, o?: { namespace?: string }) => {
|
|
storage.set(composite(k, o?.namespace), v);
|
|
}),
|
|
get: vi.fn(async (k: string, o?: { namespace?: string }) => storage.get(composite(k, o?.namespace))),
|
|
delete: vi.fn(async (k: string, o?: { namespace?: string }) => storage.delete(composite(k, o?.namespace))),
|
|
has: vi.fn(async (k: string, o?: { namespace?: string }) => storage.has(composite(k, o?.namespace))),
|
|
search: vi.fn(async (pattern: string, limit?: number, o?: { namespace?: string }) => {
|
|
const prefix = `${o?.namespace ?? 'backend-default'}::`;
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
const out: string[] = [];
|
|
for (const ck of storage.keys()) {
|
|
if (!ck.startsWith(prefix)) continue;
|
|
const key = ck.slice(prefix.length);
|
|
if (regex.test(key)) {
|
|
out.push(key);
|
|
if (limit && out.length >= limit) break;
|
|
}
|
|
}
|
|
return out;
|
|
}),
|
|
vectorSearch: vi.fn(async () => []),
|
|
storeVector: vi.fn(async () => {}),
|
|
} as unknown as MemoryBackend;
|
|
|
|
const appPath = path.join(tmpDir, 'app.ts');
|
|
const indexer = new KnowledgeGraphService(honoring, {
|
|
enableVectorEmbeddings: false,
|
|
enableLLMExtraction: false,
|
|
});
|
|
await indexer.index({ paths: [appPath, path.join(tmpDir, 'math.ts')] });
|
|
|
|
// Nodes are found under 'code-intelligence:kg', NOT the backend default —
|
|
// this is exactly what `hasCodeIntelligenceIndex` / cross-tool reads need.
|
|
const inKgNs = await honoring.search('code-intelligence:kg:node:*', 100, {
|
|
namespace: 'code-intelligence:kg',
|
|
});
|
|
const inDefaultNs = await honoring.search('code-intelligence:kg:node:*', 100);
|
|
expect(inKgNs.length).toBeGreaterThan(0);
|
|
expect(inDefaultNs.length).toBe(0);
|
|
});
|
|
});
|
|
});
|