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 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 subsystemMap = buildSubsystems(graph, { minTraffic: 3, crossCuttingThreshold: 0.6 }); const index = buildFlowIndex(graph, subsystemMap); console.log('=== 7C: Flow Tracer Tests ===\n'); // Test 1: Simple linear flow across subsystems console.log('Test 1: Linear flow'); const t1 = traceFlow('channels/telegram.ts:onTelegramMessage', index); assert(t1.subsystemSequence.includes('channels'), 'Started in channels'); assert(t1.subsystemSequence.includes('gateway'), 'Crossed to gateway'); assert(t1.flow.some(f => f.entity === 'gateway/server.ts:handleRequest' && f.crossedVia === 'CALLS'), 'Recorded CALLS crossing'); // Test 2: Cycle detection (gateway <-> agents) console.log('\nTest 2: Cycle detection'); const t2 = traceFlow('gateway/session.ts:refreshSession', index); assert(t2.subsystemSequence.includes('gateway'), 'Started in gateway'); assert(t2.subsystemSequence.includes('agents'), 'Crossed to agents'); assert(t2.flow.length < 20, 'Trace terminated without infinite loop'); // Test 3: God object exclusion console.log('\nTest 3: God object exclusion'); const indexLowThreshold = buildFlowIndex(graph, subsystemMap, { godThreshold: 2 }); const t3 = traceFlow('channels/telegram.ts:onTelegramMessage', indexLowThreshold); assert(t3.excludedNodes.includes('utils/logger.ts:log'), 'logger.ts:log was excluded as god object'); assert(!t3.flow.some(f => f.entity === 'utils/logger.ts:log'), 'logger does not appear in flow'); // Test 4: Depth limit console.log('\nTest 4: Depth limit'); const t4 = traceFlow('channels/telegram.ts:onTelegramMessage', index, { maxDepth: 1.2 }); assert(t4.flow.some(f => f.entity === 'gateway/server.ts:handleRequest'), 'Included depth 1 node'); assert(!t4.flow.some(f => f.entity === 'gateway/session.ts:loadSession'), 'Excluded depth 1.5 node'); // Test 5: Empty trace console.log('\nTest 5: Empty trace'); const t5 = traceFlow('utils/crypto.ts:hashString', index); assert(t5.flow.length === 1, 'Only entry point in flow'); assert(t5.subsystemSequence.length === 1, 'Only one subsystem'); // Test 6: Invalid entry point console.log('\nTest 6: Invalid entry point'); const t6 = traceFlow('nonexistent/file.ts:foo', index); assert(t6.error !== undefined, 'Returns error for invalid entry point'); assert(t6.flow.length === 0, 'Empty flow for invalid entry'); // Test 7: Multiple traces reuse same index (performance) console.log('\nTest 7: Index reuse + OpenClaw performance'); const fullSnap = path.join(__dirname, '..', 'snapshots', 'openclaw-full.json'); if (fs.existsSync(fullSnap)) { const fullGraph = GraphStore.loadSnapshot(fullSnap); const fullSubs = buildSubsystems(fullGraph); const indexStart = Date.now(); const fullIndex = buildFlowIndex(fullGraph, fullSubs, { godThreshold: 50 }); const indexTime = Date.now() - indexStart; console.log(` Index built in ${indexTime}ms`); // Run 3 traces on the same index const entries = ['cli/route.ts:tryRouteCli', 'gateway/session-utils.ts', 'pairing/pairing-store.ts']; let totalTraceTime = 0; for (const ep of entries) { const start = Date.now(); const t = traceFlow(ep, fullIndex, { maxDepth: 8 }); const elapsed = Date.now() - start; totalTraceTime += elapsed; console.log(` Trace "${ep}": ${t.flow.length} steps, ${t.subsystemSequence.length} subsystems, ${elapsed}ms`); } assert(indexTime < 3000, `Index built in <3s (${indexTime}ms)`); assert(totalTraceTime < 5000, `All 3 traces completed in <5s (${totalTraceTime}ms)`); } else { console.log(' (skipped — openclaw-full.json not found)'); } console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); process.exit(failed > 0 ? 1 : 0);