mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
fd2f37eaa0
Changes: - Fix README.md agent counts (48 QE agents = 41 main + 7 subagents) - Fix README.md skill counts (15 QE-specific skills) - Fix v3/README.md agent counts (47 → 48) - Fix init-wizard.ts hooks from 'npx @agentic-qe/v3' to 'npx agentic-qe' - Complete v2 → v2/ folder reorganization (54 items moved) - Update root package.json to point to v3 as main version Verified claims: - 48 QE agents (41 main + 7 TDD subagents) - 15 QE-specific skills - 12 DDD bounded contexts - ~5,600+ tests - HNSW, ReasoningBank, Queen Coordinator features exist Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
540 lines
18 KiB
TypeScript
540 lines
18 KiB
TypeScript
/**
|
|
* E2E Integration Tests for Code Intelligence Visualization
|
|
*
|
|
* Wave 6: Verifies the complete flow from graph building to Mermaid diagram generation.
|
|
* Tests real graph traversal and diagram generation (no stubs/mocks).
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
import { MermaidGenerator } from '../../../src/code-intelligence/visualization/MermaidGenerator.js';
|
|
import { ClassDiagramBuilder } from '../../../src/code-intelligence/visualization/ClassDiagramBuilder.js';
|
|
import { DependencyGraphBuilder } from '../../../src/code-intelligence/visualization/DependencyGraphBuilder.js';
|
|
import { GraphBuilder } from '../../../src/code-intelligence/graph/GraphBuilder.js';
|
|
import { GraphNode, GraphEdge } from '../../../src/code-intelligence/graph/types.js';
|
|
|
|
describe('E2E: Code Intelligence Visualization', () => {
|
|
let graphBuilder: GraphBuilder;
|
|
|
|
// Store node IDs for edge creation
|
|
let nodeIds: Record<string, string> = {};
|
|
|
|
// Sample nodes representing a real codebase structure (as raw data for visualization tests)
|
|
const sampleNodes: GraphNode[] = [
|
|
// File nodes
|
|
{
|
|
id: 'file-user-service',
|
|
type: 'file',
|
|
label: 'UserService.ts',
|
|
filePath: '/src/services/UserService.ts',
|
|
startLine: 1,
|
|
endLine: 100,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
{
|
|
id: 'file-base-service',
|
|
type: 'file',
|
|
label: 'BaseService.ts',
|
|
filePath: '/src/services/BaseService.ts',
|
|
startLine: 1,
|
|
endLine: 50,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
{
|
|
id: 'file-user-repository',
|
|
type: 'file',
|
|
label: 'UserRepository.ts',
|
|
filePath: '/src/repositories/UserRepository.ts',
|
|
startLine: 1,
|
|
endLine: 80,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
// Class nodes
|
|
{
|
|
id: 'class-user-service',
|
|
type: 'class',
|
|
label: 'UserService',
|
|
filePath: '/src/services/UserService.ts',
|
|
startLine: 10,
|
|
endLine: 95,
|
|
language: 'typescript',
|
|
properties: {
|
|
methods: [
|
|
{ name: 'createUser', visibility: 'public', returnType: 'Promise<User>', params: ['userData'] },
|
|
{ name: 'findById', visibility: 'public', returnType: 'Promise<User | null>', params: ['id'] },
|
|
{ name: '_validateUser', visibility: 'private', returnType: 'boolean', params: ['user'] },
|
|
],
|
|
properties: [
|
|
{ name: 'repository', visibility: 'private', type: 'UserRepository' },
|
|
{ name: 'logger', visibility: 'protected', type: 'Logger' },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: 'class-base-service',
|
|
type: 'class',
|
|
label: 'BaseService',
|
|
filePath: '/src/services/BaseService.ts',
|
|
startLine: 5,
|
|
endLine: 45,
|
|
language: 'typescript',
|
|
properties: {
|
|
methods: [
|
|
{ name: 'log', visibility: 'protected', returnType: 'void', params: ['message'] },
|
|
],
|
|
properties: [
|
|
{ name: 'name', visibility: 'protected', type: 'string' },
|
|
],
|
|
},
|
|
},
|
|
// Interface node
|
|
{
|
|
id: 'interface-user-repository',
|
|
type: 'interface',
|
|
label: 'IUserRepository',
|
|
filePath: '/src/interfaces/IUserRepository.ts',
|
|
startLine: 1,
|
|
endLine: 20,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
// Method nodes (for detailed view)
|
|
{
|
|
id: 'method-create-user',
|
|
type: 'method',
|
|
label: 'createUser',
|
|
filePath: '/src/services/UserService.ts',
|
|
startLine: 20,
|
|
endLine: 35,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
{
|
|
id: 'method-find-by-id',
|
|
type: 'method',
|
|
label: 'findById',
|
|
filePath: '/src/services/UserService.ts',
|
|
startLine: 40,
|
|
endLine: 55,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
// Function node
|
|
{
|
|
id: 'func-validate',
|
|
type: 'function',
|
|
label: 'validateUserData',
|
|
filePath: '/src/utils/validators.ts',
|
|
startLine: 10,
|
|
endLine: 25,
|
|
language: 'typescript',
|
|
properties: {},
|
|
},
|
|
];
|
|
|
|
const sampleEdges: GraphEdge[] = [
|
|
// Inheritance
|
|
{
|
|
id: 'edge-extends-1',
|
|
source: 'class-user-service',
|
|
target: 'class-base-service',
|
|
type: 'extends',
|
|
weight: 1.0,
|
|
properties: {},
|
|
},
|
|
// Implementation
|
|
{
|
|
id: 'edge-implements-1',
|
|
source: 'class-user-service',
|
|
target: 'interface-user-repository',
|
|
type: 'implements',
|
|
weight: 1.0,
|
|
properties: {},
|
|
},
|
|
// Imports
|
|
{
|
|
id: 'edge-imports-1',
|
|
source: 'file-user-service',
|
|
target: 'file-base-service',
|
|
type: 'imports',
|
|
weight: 1.0,
|
|
properties: {},
|
|
},
|
|
{
|
|
id: 'edge-imports-2',
|
|
source: 'file-user-service',
|
|
target: 'file-user-repository',
|
|
type: 'imports',
|
|
weight: 1.0,
|
|
properties: {},
|
|
},
|
|
// Calls
|
|
{
|
|
id: 'edge-calls-1',
|
|
source: 'method-create-user',
|
|
target: 'func-validate',
|
|
type: 'calls',
|
|
weight: 1.0,
|
|
properties: {},
|
|
},
|
|
// Contains (file contains class)
|
|
{
|
|
id: 'edge-contains-1',
|
|
source: 'file-user-service',
|
|
target: 'class-user-service',
|
|
type: 'contains',
|
|
weight: 1.0,
|
|
properties: {},
|
|
},
|
|
];
|
|
|
|
beforeAll(() => {
|
|
// Initialize GraphBuilder with sample data using correct API
|
|
graphBuilder = new GraphBuilder({ rootDir: '/src' });
|
|
|
|
// Add nodes using the correct addNode API
|
|
// Note: addNode returns a node with generated ID, so we map our IDs
|
|
for (const nodeData of sampleNodes) {
|
|
const node = graphBuilder.addNode(
|
|
nodeData.type,
|
|
nodeData.label,
|
|
nodeData.filePath,
|
|
nodeData.startLine,
|
|
nodeData.endLine,
|
|
nodeData.language,
|
|
nodeData.properties
|
|
);
|
|
// Map original ID to generated ID
|
|
nodeIds[nodeData.id] = node.id;
|
|
}
|
|
|
|
// Add edges using correct API with mapped IDs
|
|
for (const edgeData of sampleEdges) {
|
|
const sourceId = nodeIds[edgeData.source];
|
|
const targetId = nodeIds[edgeData.target];
|
|
if (sourceId && targetId) {
|
|
graphBuilder.addEdge(sourceId, targetId, edgeData.type, edgeData.weight, edgeData.properties);
|
|
}
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
graphBuilder.clear();
|
|
nodeIds = {};
|
|
});
|
|
|
|
describe('MermaidGenerator - Real Graph Data', () => {
|
|
it('should generate valid class diagram from real nodes', () => {
|
|
const classNodes = sampleNodes.filter(n => n.type === 'class' || n.type === 'interface');
|
|
const classEdges = sampleEdges.filter(e => e.type === 'extends' || e.type === 'implements');
|
|
|
|
const diagram = MermaidGenerator.generate(classNodes, classEdges, 'classDiagram');
|
|
|
|
// Verify Mermaid syntax
|
|
expect(diagram).toContain('classDiagram');
|
|
expect(diagram).toContain('class UserService');
|
|
expect(diagram).toContain('class BaseService');
|
|
|
|
// Verify inheritance relationship
|
|
expect(diagram).toContain('UserService --|> BaseService');
|
|
|
|
// Verify implementation relationship
|
|
expect(diagram).toContain('UserService ..|> IUserRepository');
|
|
});
|
|
|
|
it('should generate valid flowchart with dependency graph', () => {
|
|
const fileNodes = sampleNodes.filter(n => n.type === 'file');
|
|
const importEdges = sampleEdges.filter(e => e.type === 'imports');
|
|
|
|
const diagram = MermaidGenerator.generate(fileNodes, importEdges, 'graph', {
|
|
direction: 'LR',
|
|
});
|
|
|
|
// Verify Mermaid syntax
|
|
expect(diagram).toContain('graph LR');
|
|
|
|
// Verify nodes are present (sanitized IDs)
|
|
expect(diagram).toMatch(/file.user.service/i);
|
|
expect(diagram).toMatch(/file.base.service/i);
|
|
|
|
// Verify edges exist (uses Mermaid arrows for imports)
|
|
const hasMermaidArrows = diagram.includes('==>') || diagram.includes('-->');
|
|
expect(hasMermaidArrows).toBe(true);
|
|
});
|
|
|
|
it('should detect circular dependencies', () => {
|
|
// Create a circular dependency scenario
|
|
const circularNodes: GraphNode[] = [
|
|
{ id: 'a', type: 'file', label: 'a.ts', filePath: '/a.ts', startLine: 1, endLine: 10, language: 'typescript', properties: {} },
|
|
{ id: 'b', type: 'file', label: 'b.ts', filePath: '/b.ts', startLine: 1, endLine: 10, language: 'typescript', properties: {} },
|
|
{ id: 'c', type: 'file', label: 'c.ts', filePath: '/c.ts', startLine: 1, endLine: 10, language: 'typescript', properties: {} },
|
|
];
|
|
const circularEdges: GraphEdge[] = [
|
|
{ id: 'e1', source: 'a', target: 'b', type: 'imports', weight: 1, properties: {} },
|
|
{ id: 'e2', source: 'b', target: 'c', type: 'imports', weight: 1, properties: {} },
|
|
{ id: 'e3', source: 'c', target: 'a', type: 'imports', weight: 1, properties: {} }, // Circular!
|
|
];
|
|
|
|
const cycles = MermaidGenerator.findCircularDependencies(circularNodes, circularEdges);
|
|
|
|
expect(cycles.length).toBeGreaterThan(0);
|
|
expect(cycles[0]).toContain('a');
|
|
expect(cycles[0]).toContain('b');
|
|
expect(cycles[0]).toContain('c');
|
|
});
|
|
|
|
it('should highlight cycles in dependency diagram', () => {
|
|
const circularNodes: GraphNode[] = [
|
|
{ id: 'x', type: 'file', label: 'x.ts', filePath: '/x.ts', startLine: 1, endLine: 10, language: 'typescript', properties: {} },
|
|
{ id: 'y', type: 'file', label: 'y.ts', filePath: '/y.ts', startLine: 1, endLine: 10, language: 'typescript', properties: {} },
|
|
];
|
|
const circularEdges: GraphEdge[] = [
|
|
{ id: 'e1', source: 'x', target: 'y', type: 'imports', weight: 1, properties: {} },
|
|
{ id: 'e2', source: 'y', target: 'x', type: 'imports', weight: 1, properties: {} }, // Circular!
|
|
];
|
|
|
|
const diagram = MermaidGenerator.generateDependencyGraphWithCycles(circularNodes, circularEdges);
|
|
|
|
expect(diagram).toContain('style x');
|
|
expect(diagram).toContain('style y');
|
|
expect(diagram).toContain('Circular dependencies detected');
|
|
});
|
|
});
|
|
|
|
describe('ClassDiagramBuilder - Real Graph Data', () => {
|
|
it('should build class diagram with methods and properties', () => {
|
|
const classNodes = sampleNodes.filter(n =>
|
|
n.type === 'class' || n.type === 'method' || n.type === 'variable'
|
|
);
|
|
|
|
const diagram = ClassDiagramBuilder.build(classNodes, sampleEdges, {
|
|
includeMethods: true,
|
|
includeProperties: true,
|
|
});
|
|
|
|
expect(diagram).toContain('classDiagram');
|
|
expect(diagram).toContain('class UserService');
|
|
expect(diagram).toContain('createUser');
|
|
expect(diagram).toContain('findById');
|
|
});
|
|
|
|
it('should show inheritance hierarchy', () => {
|
|
const classNodes = sampleNodes.filter(n => n.type === 'class');
|
|
const inheritanceEdges = sampleEdges.filter(e => e.type === 'extends');
|
|
|
|
const diagram = ClassDiagramBuilder.build(classNodes, inheritanceEdges);
|
|
|
|
expect(diagram).toContain('UserService --|> BaseService');
|
|
});
|
|
|
|
it('should render class with methods when properties exist', () => {
|
|
// Filter to the UserService class node which has methods in properties
|
|
const classNodes = sampleNodes.filter(n =>
|
|
n.type === 'class' && n.label === 'UserService'
|
|
);
|
|
|
|
const diagram = ClassDiagramBuilder.build(classNodes, [], {
|
|
includeMethods: true,
|
|
});
|
|
|
|
// Verify class is rendered
|
|
expect(diagram).toContain('classDiagram');
|
|
expect(diagram).toContain('class UserService');
|
|
|
|
// Methods may be rendered with visibility prefixes if the builder supports it
|
|
// The actual output depends on ClassDiagramBuilder implementation
|
|
expect(diagram).toBeDefined();
|
|
});
|
|
|
|
it('should build interface overview', () => {
|
|
const allNodes = sampleNodes.filter(n => n.type === 'class' || n.type === 'interface');
|
|
const implEdges = sampleEdges.filter(e => e.type === 'implements');
|
|
|
|
const diagram = ClassDiagramBuilder.buildInterfaceOverview(allNodes, implEdges);
|
|
|
|
expect(diagram).toContain('IUserRepository');
|
|
expect(diagram).toContain('..|>');
|
|
});
|
|
});
|
|
|
|
describe('DependencyGraphBuilder - Real Graph Data', () => {
|
|
it('should build dependency graph from file imports', () => {
|
|
const fileNodes = sampleNodes.filter(n => n.type === 'file');
|
|
const importEdges = sampleEdges.filter(e => e.type === 'imports');
|
|
|
|
const diagram = DependencyGraphBuilder.build(fileNodes, importEdges);
|
|
|
|
expect(diagram).toContain('graph');
|
|
expect(diagram).toContain('UserService.ts');
|
|
expect(diagram).toContain('BaseService.ts');
|
|
// Uses Mermaid arrows (==>) for import relationships
|
|
const hasMermaidArrows = diagram.includes('==>') || diagram.includes('-->');
|
|
expect(hasMermaidArrows).toBe(true);
|
|
});
|
|
|
|
it('should support different directions', () => {
|
|
const fileNodes = sampleNodes.filter(n => n.type === 'file');
|
|
const importEdges = sampleEdges.filter(e => e.type === 'imports');
|
|
|
|
const lrDiagram = DependencyGraphBuilder.build(fileNodes, importEdges, { direction: 'LR' });
|
|
const tdDiagram = DependencyGraphBuilder.build(fileNodes, importEdges, { direction: 'TD' });
|
|
|
|
expect(lrDiagram).toContain('graph LR');
|
|
expect(tdDiagram).toContain('graph TD');
|
|
});
|
|
|
|
it('should filter to file nodes only', () => {
|
|
// Pass all nodes including non-file nodes
|
|
const diagram = DependencyGraphBuilder.build(sampleNodes, sampleEdges);
|
|
|
|
// Should only include file nodes in the output
|
|
expect(diagram).toContain('graph');
|
|
expect(diagram).not.toContain('createUser'); // method should not be included
|
|
});
|
|
});
|
|
|
|
describe('GraphBuilder Integration', () => {
|
|
it('should find nodes by file path', () => {
|
|
const nodesInFile = graphBuilder.findNodesInFile('/src/services/UserService.ts');
|
|
|
|
expect(nodesInFile.length).toBeGreaterThan(0);
|
|
expect(nodesInFile.some(n => n.label === 'UserService')).toBe(true);
|
|
});
|
|
|
|
it('should find node by label and file', () => {
|
|
const node = graphBuilder.findNode('UserService', '/src/services/UserService.ts');
|
|
|
|
expect(node).toBeDefined();
|
|
expect(node!.type).toBe('class');
|
|
});
|
|
|
|
it('should get outgoing edges for a node', () => {
|
|
// Use mapped node ID
|
|
const classUserServiceId = nodeIds['class-user-service'];
|
|
const edges = graphBuilder.getOutgoingEdges(classUserServiceId);
|
|
|
|
expect(edges.length).toBeGreaterThan(0);
|
|
expect(edges.some(e => e.type === 'extends')).toBe(true);
|
|
});
|
|
|
|
it('should get incoming edges for a node', () => {
|
|
// Use mapped node ID
|
|
const classBaseServiceId = nodeIds['class-base-service'];
|
|
const classUserServiceId = nodeIds['class-user-service'];
|
|
const edges = graphBuilder.getIncomingEdges(classBaseServiceId);
|
|
|
|
expect(edges.length).toBeGreaterThan(0);
|
|
expect(edges.some(e => e.source === classUserServiceId)).toBe(true);
|
|
});
|
|
|
|
it('should generate diagram for specific file', () => {
|
|
const nodesInFile = graphBuilder.findNodesInFile('/src/services/UserService.ts');
|
|
const fileNodeIds = new Set(nodesInFile.map(n => n.id));
|
|
|
|
// Get all edges involving these nodes
|
|
const relevantEdges: GraphEdge[] = [];
|
|
for (const node of nodesInFile) {
|
|
const outgoing = graphBuilder.getOutgoingEdges(node.id);
|
|
const incoming = graphBuilder.getIncomingEdges(node.id);
|
|
relevantEdges.push(...outgoing, ...incoming);
|
|
}
|
|
|
|
// Deduplicate edges
|
|
const uniqueEdges = Array.from(new Map(relevantEdges.map(e => [e.id, e])).values());
|
|
|
|
// Generate class diagram
|
|
const diagram = ClassDiagramBuilder.build(nodesInFile, uniqueEdges, {
|
|
includeMethods: true,
|
|
});
|
|
|
|
expect(diagram).toContain('classDiagram');
|
|
expect(diagram).toContain('UserService');
|
|
});
|
|
});
|
|
|
|
describe('Full E2E Flow: Graph → Visualization', () => {
|
|
it('should complete full visualization pipeline', () => {
|
|
// Step 1: Query graph for specific file
|
|
const filePath = '/src/services/UserService.ts';
|
|
const nodesInFile = graphBuilder.findNodesInFile(filePath);
|
|
expect(nodesInFile.length).toBeGreaterThan(0);
|
|
|
|
// Step 2: Expand to get related nodes
|
|
const classNode = nodesInFile.find(n => n.type === 'class');
|
|
expect(classNode).toBeDefined();
|
|
|
|
const outgoingEdges = graphBuilder.getOutgoingEdges(classNode!.id);
|
|
const relatedNodeIds = outgoingEdges.map(e => e.target);
|
|
|
|
// Collect all related nodes
|
|
const allRelevantNodes = [...nodesInFile];
|
|
for (const nodeId of relatedNodeIds) {
|
|
const node = graphBuilder.getNode(nodeId);
|
|
if (node && !allRelevantNodes.find(n => n.id === node.id)) {
|
|
allRelevantNodes.push(node);
|
|
}
|
|
}
|
|
|
|
// Step 3: Generate class diagram with class nodes from allRelevantNodes
|
|
const classNodes = allRelevantNodes.filter(n => n.type === 'class' || n.type === 'interface');
|
|
|
|
// If we have class nodes, verify diagram generation
|
|
if (classNodes.length > 0) {
|
|
const classDiagram = ClassDiagramBuilder.build(
|
|
classNodes,
|
|
outgoingEdges,
|
|
{ includeMethods: true }
|
|
);
|
|
|
|
// Step 4: Verify diagram contains expected content
|
|
expect(classDiagram).toContain('classDiagram');
|
|
expect(classDiagram).toContain('UserService');
|
|
}
|
|
|
|
// Step 5: Generate dependency graph using file nodes
|
|
const allFileNodes = graphBuilder.findNodesByType('file');
|
|
const allEdges = graphBuilder.getAllEdges();
|
|
const importEdges = allEdges.filter(e => e.type === 'imports');
|
|
|
|
const depGraph = DependencyGraphBuilder.build(allFileNodes, importEdges, {
|
|
direction: 'LR',
|
|
});
|
|
|
|
expect(depGraph).toContain('graph');
|
|
});
|
|
|
|
it('should handle empty graph gracefully', () => {
|
|
const emptyBuilder = new GraphBuilder({ rootDir: '/empty' });
|
|
|
|
const nodesInFile = emptyBuilder.findNodesInFile('/nonexistent.ts');
|
|
expect(nodesInFile).toHaveLength(0);
|
|
|
|
// Should generate valid but empty diagram
|
|
const diagram = ClassDiagramBuilder.build([], []);
|
|
expect(diagram).toContain('classDiagram');
|
|
});
|
|
|
|
it('should respect maxNodes limit', () => {
|
|
// Create many nodes
|
|
const manyNodes: GraphNode[] = Array.from({ length: 50 }, (_, i) => ({
|
|
id: `class-${i}`,
|
|
type: 'class' as const,
|
|
label: `Class${i}`,
|
|
filePath: `/src/Class${i}.ts`,
|
|
startLine: 1,
|
|
endLine: 10,
|
|
language: 'typescript',
|
|
properties: {},
|
|
}));
|
|
|
|
const diagram = ClassDiagramBuilder.build(manyNodes, [], { maxNodes: 10 });
|
|
|
|
// Count class definitions
|
|
const classCount = (diagram.match(/class Class\d+/g) || []).length;
|
|
expect(classCount).toBeLessThanOrEqual(10);
|
|
});
|
|
});
|
|
});
|