Files
Dragan Spiridonov 9fd18ed00a feat(code-intelligence): first-class C4 architecture diagrams (ADR-112)
Consolidate C4 diagram generation onto a single engine and expose it to
users via CLI and MCP, with a deterministic confidence gate and real
Knowledge-Graph-derived relationships.

- Consolidation (C1/C6): C4ModelService is the single render/analyze/store
  engine; the bridge delegates and FAILS LOUD on a render error instead of
  silently degrading. Duplicate inline generators removed.
- C2: real component relationships from the Knowledge Graph (AST import/call
  edges) replace the naming heuristic; project-scoped via a new KG basePath
  so repos outside cwd don't trip the path-traversal guard.
- C3: deterministic confidence gate (high/medium/low + reasons) on every
  diagram — surfaces the detector's known limits instead of hiding them.
- C4/C5: `aqe code c4` CLI + `qe/code/c4` MCP tool (generate/search),
  verified at MCP-CLI parity and through the protocol-server bridge.
- Search: generate persists embeddings (opt-in `enableC4Embeddings`, on for
  MCP) so `qe/code/c4 search` returns hits in the standard flow.
- Fixed pre-existing lint in touched files (require->import, unused catches).

Docs: ADR-112 + docs/guides/c4-architecture-diagrams.md. tsc + lint clean;
C4 surface fully green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:27:25 +00:00

111 lines
4.8 KiB
TypeScript

/**
* ADR-112 — qe/code/c4 MCP tool tests, incl. MCP-CLI parity.
*
* The MCP tool and the CLI both drive the SAME ProductFactorsBridgeService
* pipeline, so a generate over a fixture must yield the same diagrams +
* confidence the CLI's coordinator path produces (CLAUDE.md MCP-CLI parity rule).
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { createMockMemory } from '../../../mocks';
import { CodeC4Tool } from '../../../../src/mcp/tools/code-intelligence/c4';
import { ProductFactorsBridgeService } from '../../../../src/domains/code-intelligence/services/product-factors-bridge';
import { InMemoryEventBus } from '../../../../src/kernel/event-bus';
import type { MCPToolContext } from '../../../../src/mcp/tools/base';
let tmp: string;
function makeFixture(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'c4-mcp-'));
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
name: 'billing-svc', description: 'Billing service', dependencies: { pg: '^8.0.0' },
}));
for (const sub of ['controllers', 'services']) {
fs.mkdirSync(path.join(dir, 'src', sub), { recursive: true });
fs.writeFileSync(path.join(dir, 'src', sub, 'billing.ts'), `export class Billing_${sub} {}\n`);
}
return dir;
}
const ctx = (memory: ReturnType<typeof createMockMemory>): MCPToolContext => ({
requestId: 'test', startTime: Date.now(), memory,
});
beforeEach(() => { tmp = makeFixture(); });
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
describe('qe/code/c4 MCP tool', () => {
it('should_be_named_and_domain_scoped', () => {
const tool = new CodeC4Tool();
expect(tool.config.name).toBe('qe/code/c4');
expect(tool.config.domain).toBe('code-intelligence');
});
it('should_generate_C4_diagrams_with_a_confidence_assessment', async () => {
const tool = new CodeC4Tool();
const res = await tool.execute({ action: 'generate', projectPath: tmp, level: 'all' }, ctx(createMockMemory()));
expect(res.success).toBe(true);
const gen = res.data?.generateResult;
expect(gen?.diagrams.context).toContain('C4Context');
expect(gen?.diagrams.component).toContain('C4Component');
expect(gen?.confidence).toBeDefined();
expect(['high', 'medium', 'low']).toContain(gen!.confidence!.level);
});
it('should_respect_the_level_parameter', async () => {
const tool = new CodeC4Tool();
const res = await tool.execute({ action: 'generate', projectPath: tmp, level: 'context' }, ctx(createMockMemory()));
expect(res.success).toBe(true);
const gen = res.data?.generateResult;
expect(gen?.diagrams.context).toBeDefined();
expect(gen?.diagrams.component).toBeUndefined();
});
it('should_require_a_query_for_search', async () => {
const tool = new CodeC4Tool();
const res = await tool.execute({ action: 'search' }, ctx(createMockMemory()));
expect(res.success).toBe(false);
expect(res.error).toMatch(/query/i);
});
it('should_return_hits_for_search_after_generate_persists_embeddings', async () => {
// ADR-112: generate embeds + persists diagrams; search finds them via the
// shared memory + namespace. Same tool instance + same context memory.
const tool = new CodeC4Tool();
const context = ctx(createMockMemory());
const gen = await tool.execute({ action: 'generate', projectPath: tmp, level: 'all' }, context);
expect(gen.success).toBe(true);
const found = await tool.execute({ action: 'search', query: 'billing service architecture', limit: 5 }, context);
expect(found.success).toBe(true);
expect(found.data?.searchResult?.total).toBeGreaterThan(0);
});
it('should_produce_diagrams_matching_the_bridge_pipeline_the_CLI_uses', async () => {
// MCP path
const tool = new CodeC4Tool();
const mcp = await tool.execute({ action: 'generate', projectPath: tmp, level: 'all' }, ctx(createMockMemory()));
expect(mcp.success).toBe(true);
// CLI/coordinator path = the same bridge pipeline
const bridge = new ProductFactorsBridgeService(new InMemoryEventBus(), createMockMemory(), { publishEvents: false });
const cli = await bridge.requestC4Diagrams({
projectPath: tmp,
includeContext: true, includeContainer: true, includeComponent: true, includeDependency: true,
analyzeComponents: true, detectExternalSystems: true, analyzeCoupling: true,
});
expect(cli.success).toBe(true);
if (!cli.success) return;
const gen = mcp.data!.generateResult!;
expect(gen.componentsDetected).toBe(cli.value.components.length);
expect(gen.externalSystemsDetected).toBe(cli.value.externalSystems.length);
expect(gen.diagrams.context).toBe(cli.value.diagrams.context);
expect(gen.diagrams.component).toBe(cli.value.diagrams.component);
});
});