Phase 7B, 7E, 7D: Contracts, Diagrams, Sysdoc
This commit is contained in:
104
test/test-contracts.js
Normal file
104
test/test-contracts.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { extractContracts, extractAllContracts, buildContractXref } = require('../contracts.js');
|
||||
const GraphStore = require('../graph.js');
|
||||
const { buildSubsystems, relPath } = require('../subsystem.js');
|
||||
|
||||
const FIXTURE_DIR = path.join(__dirname, 'fixtures/system-docs');
|
||||
const FIXTURE_SRC = path.join(FIXTURE_DIR, 'src');
|
||||
const SNAPSHOT = path.join(FIXTURE_DIR, 'snapshot.json');
|
||||
const EXPECTED = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'expected-contracts.json'), 'utf8'));
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, name) {
|
||||
if (condition) { passed++; console.log(` ✓ ${name}`); }
|
||||
else { failed++; console.log(` ✗ ${name}`); }
|
||||
}
|
||||
|
||||
console.log('=== 7B: Contract Extractor Tests ===\n');
|
||||
|
||||
// Test 1: Interface extraction with fields
|
||||
console.log('Test 1: Interface extraction');
|
||||
const gwContracts = extractContracts(path.join(FIXTURE_SRC, 'gateway/types.ts'), 'gateway/types.ts');
|
||||
const gwConfig = gwContracts.find(c => c.name === 'GatewayConfig');
|
||||
assert(gwConfig !== undefined, 'Found GatewayConfig');
|
||||
assert(gwConfig.type === 'Interface', 'GatewayConfig is Interface');
|
||||
assert(gwConfig.fields.length === 2, 'GatewayConfig has 2 fields');
|
||||
assert(gwConfig.fields.some(f => f.name === 'sessionKey' && f.type === 'string'), 'Has sessionKey: string');
|
||||
assert(gwConfig.fields.some(f => f.name === 'timeout' && f.type === 'number'), 'Has timeout: number');
|
||||
|
||||
// Test 2: Extends clause
|
||||
console.log('\nTest 2: Extends clause');
|
||||
assert(gwConfig.extends && gwConfig.extends.includes('BaseConfig'), 'GatewayConfig extends BaseConfig');
|
||||
|
||||
const sessionEntry = gwContracts.find(c => c.name === 'SessionEntry');
|
||||
assert(sessionEntry !== undefined, 'Found SessionEntry');
|
||||
assert(!sessionEntry.extends, 'SessionEntry has no extends');
|
||||
|
||||
// Test 3: Type alias
|
||||
console.log('\nTest 3: Type alias');
|
||||
const agentContracts = extractContracts(path.join(FIXTURE_SRC, 'agents/types.ts'), 'agents/types.ts');
|
||||
const agentStatus = agentContracts.find(c => c.name === 'AgentStatus');
|
||||
assert(agentStatus !== undefined, 'Found AgentStatus');
|
||||
assert(agentStatus.type === 'TypeAlias', 'AgentStatus is TypeAlias');
|
||||
|
||||
// Test 4: Enum with members
|
||||
console.log('\nTest 4: Enum extraction');
|
||||
const configContracts = extractContracts(path.join(FIXTURE_SRC, 'config/types.ts'), 'config/types.ts');
|
||||
const logLevel = configContracts.find(c => c.name === 'LogLevel');
|
||||
assert(logLevel !== undefined, 'Found LogLevel');
|
||||
assert(logLevel.type === 'Enum', 'LogLevel is Enum');
|
||||
assert(logLevel.members.length === 4, 'LogLevel has 4 members');
|
||||
assert(logLevel.members.includes('Debug'), 'Has Debug member');
|
||||
assert(logLevel.members.includes('Error'), 'Has Error member');
|
||||
|
||||
// Test 5: Visibility
|
||||
console.log('\nTest 5: Visibility');
|
||||
assert(gwConfig.visibility === 'public', 'Exported interface is public');
|
||||
|
||||
// Test 6: Match expected contracts count
|
||||
console.log('\nTest 6: Total contract count');
|
||||
const graph = GraphStore.loadSnapshot(SNAPSHOT);
|
||||
const subs = buildSubsystems(graph, { minTraffic: 3, crossCuttingThreshold: 0.6 });
|
||||
const allResult = extractAllContracts(subs, FIXTURE_SRC);
|
||||
assert(allResult.contracts.length === EXPECTED.contracts.length,
|
||||
`Total contracts: ${allResult.contracts.length} (expected ${EXPECTED.contracts.length})`);
|
||||
|
||||
// Test 7: By-subsystem grouping
|
||||
console.log('\nTest 7: By-subsystem grouping');
|
||||
assert(allResult.bySubsystem['gateway'].length === 2, 'gateway has 2 contracts');
|
||||
assert(allResult.bySubsystem['agents'].length === 4, 'agents has 4 contracts');
|
||||
assert(allResult.bySubsystem['config'].length === 5, 'config has 5 contracts');
|
||||
|
||||
// Test 8: Contract cross-reference
|
||||
console.log('\nTest 8: Contract cross-reference');
|
||||
const xref = buildContractXref(allResult.contracts, graph, relPath);
|
||||
assert(xref['BaseConfig'] !== undefined, 'BaseConfig in xref');
|
||||
assert(xref['BaseConfig'].definedIn === 'config', 'BaseConfig defined in config');
|
||||
assert(xref['BaseConfig'].usedBy.includes('gateway'), 'BaseConfig used by gateway');
|
||||
|
||||
// Test 9: Orphan file contracts still extracted
|
||||
console.log('\nTest 9: Orphan file contracts');
|
||||
const schemaContracts = allResult.bySubsystem['config'].filter(c => c.id.startsWith('config/schema.ts'));
|
||||
assert(schemaContracts.length === 2, 'Orphan schema.ts contracts extracted');
|
||||
|
||||
// Test 10: OpenClaw scale test
|
||||
console.log('\nTest 10: OpenClaw scale');
|
||||
const fullSnap = path.join(__dirname, '..', 'snapshots', 'openclaw-full.json');
|
||||
if (fs.existsSync(fullSnap)) {
|
||||
const fullGraph = GraphStore.loadSnapshot(fullSnap);
|
||||
const fullSubs = buildSubsystems(fullGraph);
|
||||
const start = Date.now();
|
||||
const fullResult = extractAllContracts(fullSubs, '/app/src');
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(` OpenClaw: ${fullResult.contracts.length} contracts in ${elapsed}ms`);
|
||||
assert(fullResult.contracts.length > 50, `Found >50 contracts (${fullResult.contracts.length})`);
|
||||
assert(elapsed < 30000, `Completed in <30s (${elapsed}ms)`);
|
||||
} else {
|
||||
console.log(' (skipped — openclaw-full.json not found)');
|
||||
}
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
66
test/test-diagrams.js
Normal file
66
test/test-diagrams.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const GraphStore = require('../graph.js');
|
||||
const { buildSubsystems } = require('../subsystem.js');
|
||||
const { buildFlowIndex, traceFlow } = require('../flow.js');
|
||||
const { generateDependencyDiagram, generateFlowDiagram, generateContractDiagram } = require('../diagrams.js');
|
||||
|
||||
const FIXTURE_DIR = path.join(__dirname, 'fixtures/system-docs');
|
||||
const SNAPSHOT = path.join(FIXTURE_DIR, 'snapshot.json');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, name) {
|
||||
if (condition) { passed++; console.log(` ✓ ${name}`); }
|
||||
else { failed++; console.log(` ✗ ${name}`); }
|
||||
}
|
||||
|
||||
const graph = GraphStore.loadSnapshot(SNAPSHOT);
|
||||
const subs = buildSubsystems(graph, { minTraffic: 3, crossCuttingThreshold: 0.6 });
|
||||
const index = buildFlowIndex(graph, subs);
|
||||
|
||||
console.log('=== 7E: Diagram Generator Tests ===\n');
|
||||
|
||||
// Test 1: Dependency diagram valid Mermaid
|
||||
console.log('Test 1: Dependency diagram');
|
||||
const depDiag = generateDependencyDiagram(subs);
|
||||
assert(depDiag.startsWith('graph TD'), 'Starts with graph TD');
|
||||
assert(depDiag.includes('gateway'), 'Contains gateway node');
|
||||
assert(depDiag.includes('-->'), 'Contains edges');
|
||||
assert(depDiag.includes('shared'), 'Contains shared class for cross-cutting');
|
||||
assert(!depDiag.includes('undefined'), 'No undefined values');
|
||||
|
||||
// Test 2: Flow sequence diagram
|
||||
console.log('\nTest 2: Flow sequence diagram');
|
||||
const flow = traceFlow('channels/telegram.ts:onTelegramMessage', index);
|
||||
const flowDiag = generateFlowDiagram(flow);
|
||||
assert(flowDiag.startsWith('sequenceDiagram'), 'Starts with sequenceDiagram');
|
||||
assert(flowDiag.includes('participant channels'), 'Has channels participant');
|
||||
assert(flowDiag.includes('->>'), 'Has message arrows');
|
||||
assert(!flowDiag.includes('undefined'), 'No undefined values');
|
||||
|
||||
// Test 3: Contract class diagram
|
||||
console.log('\nTest 3: Contract class diagram');
|
||||
const contracts = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'expected-contracts.json'), 'utf8'));
|
||||
const contractDiag = generateContractDiagram(contracts.contracts);
|
||||
assert(contractDiag.startsWith('classDiagram'), 'Starts with classDiagram');
|
||||
assert(contractDiag.includes('GatewayConfig'), 'Contains GatewayConfig');
|
||||
assert(contractDiag.includes('BaseConfig <|-- GatewayConfig'), 'Contains extends relationship');
|
||||
assert(contractDiag.includes('<<enumeration>>'), 'Contains enum');
|
||||
assert(contractDiag.includes('LogLevel'), 'Contains LogLevel enum');
|
||||
assert(!contractDiag.includes('undefined'), 'No undefined values');
|
||||
|
||||
// Test 4: Empty inputs
|
||||
console.log('\nTest 4: Edge cases');
|
||||
const emptyDeps = generateDependencyDiagram({ subsystems: [], crossCutting: [], dependencyMatrix: {} });
|
||||
assert(emptyDeps === 'graph TD\n classDef shared fill:#f9f,stroke:#333,stroke-dasharray: 5 5', 'Empty dep diagram is valid');
|
||||
|
||||
const emptyFlow = generateFlowDiagram({ flow: [], subsystemSequence: [], cyclesDetected: [] });
|
||||
assert(emptyFlow === 'sequenceDiagram', 'Empty flow diagram is valid');
|
||||
|
||||
const emptyContracts = generateContractDiagram([]);
|
||||
assert(emptyContracts === 'classDiagram', 'Empty contract diagram is valid');
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
74
test/test-sysdoc.js
Normal file
74
test/test-sysdoc.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { generateDocs } = require('../sysdoc.js');
|
||||
const GraphStore = require('../graph.js');
|
||||
|
||||
const FIXTURE_DIR = path.join(__dirname, 'fixtures/system-docs');
|
||||
const FIXTURE_SRC = path.join(FIXTURE_DIR, 'src');
|
||||
const SNAPSHOT = path.join(FIXTURE_DIR, 'snapshot.json');
|
||||
const OUT_DIR = path.join(__dirname, 'tmp-docs');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, name) {
|
||||
if (condition) { passed++; console.log(` ✓ ${name}`); }
|
||||
else { failed++; console.log(` ✗ ${name}`); }
|
||||
}
|
||||
|
||||
console.log('=== 7D: Hierarchical Doc Generator Tests ===\n');
|
||||
|
||||
// Clean and run
|
||||
if (fs.existsSync(OUT_DIR)) {
|
||||
fs.rmSync(OUT_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const graph = GraphStore.loadSnapshot(SNAPSHOT);
|
||||
const result = generateDocs(graph, FIXTURE_SRC, OUT_DIR, {
|
||||
entryPoints: ['channels/telegram.ts:onTelegramMessage', 'gateway/session.ts:refreshSession'],
|
||||
srcDir: '/src/',
|
||||
minTraffic: 3,
|
||||
crossCuttingThreshold: 0.6
|
||||
});
|
||||
|
||||
// Test 1: Output directory structure (Divio)
|
||||
console.log('Test 1: Output directory structure');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'reference/subsystems')), 'reference/subsystems exists');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'reference/contracts')), 'reference/contracts exists');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'explanation')), 'explanation exists');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'diagrams')), 'diagrams exists');
|
||||
|
||||
// Test 2: System Architecture overview
|
||||
console.log('\nTest 2: System Architecture document');
|
||||
const sysArch = fs.readFileSync(path.join(OUT_DIR, 'reference/system-architecture.md'), 'utf8');
|
||||
assert(sysArch.includes('# System Architecture'), 'Has title');
|
||||
assert(sysArch.includes('- **gateway**'), 'Lists gateway subsystem');
|
||||
assert(sysArch.includes('- **config**'), 'Lists config cross-cutting concern');
|
||||
assert(sysArch.includes('graph TD'), 'Embeds Mermaid dependency diagram');
|
||||
|
||||
// Test 3: Subsystem Docs
|
||||
console.log('\nTest 3: Per-Subsystem documents');
|
||||
const gatewayDoc = fs.readFileSync(path.join(OUT_DIR, 'reference/subsystems/gateway.md'), 'utf8');
|
||||
assert(gatewayDoc.includes('# Subsystem: gateway'), 'Gateway doc has title');
|
||||
assert(gatewayDoc.includes('- `handleRequest`'), 'Gateway doc lists public exports');
|
||||
assert(gatewayDoc.includes('classDiagram'), 'Gateway doc embeds contract diagram');
|
||||
assert(gatewayDoc.includes('GatewayConfig'), 'Gateway doc includes its contracts');
|
||||
|
||||
// Test 4: Data Flows
|
||||
console.log('\nTest 4: Data Flows document');
|
||||
const flowsDoc = fs.readFileSync(path.join(OUT_DIR, 'explanation/data-flows.md'), 'utf8');
|
||||
assert(flowsDoc.includes('# Data Flows'), 'Has title');
|
||||
assert(flowsDoc.includes('channels/telegram.ts:onTelegramMessage'), 'Has first flow entry point');
|
||||
assert(flowsDoc.includes('gateway/session.ts:refreshSession'), 'Has second flow entry point');
|
||||
assert(flowsDoc.includes('sequenceDiagram'), 'Embeds Mermaid sequence diagrams');
|
||||
assert(flowsDoc.includes('channels → utils → gateway → config'), 'Shows subsystem sequence');
|
||||
|
||||
// Test 5: Mermaid Artifacts
|
||||
console.log('\nTest 5: Diagram files generated');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'diagrams/system-deps.mmd')), 'system-deps.mmd generated');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'diagrams/gateway-contracts.mmd')), 'gateway-contracts.mmd generated');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'diagrams/all-contracts.mmd')), 'all-contracts.mmd generated');
|
||||
assert(fs.existsSync(path.join(OUT_DIR, 'diagrams/flow-0.mmd')), 'flow-0.mmd generated');
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user