Phase 7A+7C: Subsystem aggregator + Flow tracer (post-review fixes)

This commit is contained in:
Jarvis Prime
2026-03-09 06:51:32 +00:00
parent 4221ab4d76
commit 4c212740a2
28 changed files with 2476 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
# Mock Architecture Documentation
## Purpose
This is a test fixture simulating a real repo's architecture.md file.
## Layered Architecture
The system follows a layered architecture:
- **Channels** receive inbound messages from external platforms
- **Gateway** manages sessions and routes requests
- **Agents** execute AI model interactions
- **Config** provides centralized configuration
- **Utils** provides shared infrastructure (logging, crypto, filesystem)
## Design Decisions
- Gateway and Agents have a deliberate circular dependency for session refresh workflows
- Config is loaded lazily and cached in memory
- Utils are stateless pure functions with no domain logic

View File

@@ -0,0 +1,104 @@
{
"contracts": [
{
"id": "gateway/types.ts:GatewayConfig",
"type": "Interface",
"name": "GatewayConfig",
"extends": ["BaseConfig"],
"fields": [
{ "name": "sessionKey", "type": "string" },
{ "name": "timeout", "type": "number" }
]
},
{
"id": "gateway/types.ts:SessionEntry",
"type": "Interface",
"name": "SessionEntry",
"fields": [
{ "name": "key", "type": "string" },
{ "name": "agentId", "type": "string" },
{ "name": "model", "type": "string" }
]
},
{
"id": "agents/scope.ts:AgentScope",
"type": "Interface",
"name": "AgentScope",
"fields": [
{ "name": "agentId", "type": "string" },
{ "name": "tools", "type": "string[]" }
]
},
{
"id": "agents/types.ts:AgentConfig",
"type": "Interface",
"name": "AgentConfig",
"fields": [
{ "name": "agentId", "type": "string" },
{ "name": "model", "type": "string" },
{ "name": "maxTokens", "type": "number" }
]
},
{
"id": "agents/types.ts:AgentResult",
"type": "Interface",
"name": "AgentResult",
"fields": [
{ "name": "output", "type": "string" },
{ "name": "tokensUsed", "type": "number" },
{ "name": "duration", "type": "number" }
]
},
{
"id": "agents/types.ts:AgentStatus",
"type": "TypeAlias",
"name": "AgentStatus"
},
{
"id": "config/types.ts:BaseConfig",
"type": "Interface",
"name": "BaseConfig",
"fields": [
{ "name": "defaultAgent", "type": "string" },
{ "name": "model", "type": "string" },
{ "name": "debug", "type": "boolean" }
]
},
{
"id": "config/types.ts:ProviderConfig",
"type": "Interface",
"name": "ProviderConfig",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "apiKey", "type": "string" },
{ "name": "baseUrl", "type": "string" }
]
},
{
"id": "config/types.ts:LogLevel",
"type": "Enum",
"name": "LogLevel",
"members": ["Debug", "Info", "Warn", "Error"]
},
{
"id": "config/schema.ts:SchemaDefinition",
"type": "Interface",
"name": "SchemaDefinition",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "fields", "type": "SchemaField[]" }
]
},
{
"id": "config/schema.ts:SchemaField",
"type": "Interface",
"name": "SchemaField",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "type", "type": "string" },
{ "name": "required", "type": "boolean" }
]
}
]
}

View File

@@ -0,0 +1,17 @@
{
"edges": [
{ "from": "gateway", "to": "agents", "type": "CALLS", "via": ["gateway/session.ts:refreshSession→agents/runner.ts:runAgent"] },
{ "from": "gateway", "to": "config", "type": "IMPORTS", "via": ["gateway/session.ts→config/config.ts", "gateway/types.ts→config/types.ts"] },
{ "from": "gateway", "to": "utils", "type": "CALLS", "via": ["gateway/middleware.ts:applyMiddleware→utils/logger.ts:log", "gateway/utils.ts:formatSessionKey→utils/logger.ts:log"] },
{ "from": "agents", "to": "gateway", "type": "CALLS", "via": ["agents/runner.ts:reloadAgent→gateway/session.ts:loadSession"] },
{ "from": "agents", "to": "config", "type": "IMPORTS", "via": ["agents/scope.ts→config/config.ts"] },
{ "from": "agents", "to": "utils", "type": "CALLS", "via": ["agents/runner.ts:runAgent→utils/logger.ts:log", "agents/scope.ts:createAgentScope→utils/logger.ts:log", "agents/tools.ts→utils/logger.ts:log"] },
{ "from": "channels", "to": "gateway", "type": "CALLS", "via": ["channels/telegram.ts:onTelegramMessage→gateway/server.ts:handleRequest", "channels/discord.ts:onDiscordMessage→gateway/server.ts:handleRequest"] },
{ "from": "channels", "to": "utils", "type": "CALLS", "via": ["channels/telegram.ts→utils/logger.ts:log", "channels/discord.ts→utils/logger.ts:log"] },
{ "from": "config", "to": "utils", "type": "CALLS", "via": ["config/config.ts:loadConfigFromDisk→utils/logger.ts:log"] },
{ "from": "utils", "to": "utils", "type": "CALLS", "via": ["utils/fs-helpers.ts→utils/logger.ts:log"] }
],
"cycles": [
{ "subsystems": ["gateway", "agents"], "via": "gateway/session.ts↔agents/runner.ts" }
]
}

View File

@@ -0,0 +1,26 @@
{
"flows": [
{
"entryPoint": "channels/telegram.ts:onTelegramMessage",
"subsystemSequence": ["channels", "gateway", "agents"],
"flow": [
{ "subsystem": "channels", "entity": "channels/telegram.ts:onTelegramMessage", "depth": 0 },
{ "subsystem": "gateway", "entity": "gateway/server.ts:handleRequest", "depth": 1, "crossedVia": "CALLS" },
{ "subsystem": "gateway", "entity": "gateway/session.ts:loadSession", "depth": 1.5, "crossedVia": "CALLS" }
],
"cyclesDetected": []
},
{
"entryPoint": "gateway/session.ts:refreshSession",
"subsystemSequence": ["gateway", "agents"],
"flow": [
{ "subsystem": "gateway", "entity": "gateway/session.ts:refreshSession", "depth": 0 },
{ "subsystem": "gateway", "entity": "gateway/session.ts:loadSession", "depth": 0.5, "crossedVia": "CALLS" },
{ "subsystem": "agents", "entity": "agents/runner.ts:runAgent", "depth": 1, "crossedVia": "CALLS" }
],
"cyclesDetected": [
{ "at": "agents/runner.ts:reloadAgent", "backEdgeTo": "gateway/session.ts:loadSession" }
]
}
]
}

View File

@@ -0,0 +1,52 @@
{
"subsystems": [
{
"name": "gateway",
"kind": "domain",
"files": [
"gateway/server.ts",
"gateway/session.ts",
"gateway/middleware.ts",
"gateway/types.ts",
"gateway/utils.ts"
]
},
{
"name": "agents",
"kind": "domain",
"files": [
"agents/runner.ts",
"agents/scope.ts",
"agents/tools.ts",
"agents/types.ts",
"agents/defaults.ts"
]
},
{
"name": "channels",
"kind": "domain",
"files": [
"channels/telegram.ts",
"channels/discord.ts"
]
},
{
"name": "config",
"kind": "cross-cutting",
"files": [
"config/config.ts",
"config/types.ts",
"config/schema.ts"
]
},
{
"name": "utils",
"kind": "cross-cutting",
"files": [
"utils/logger.ts",
"utils/crypto.ts",
"utils/fs-helpers.ts"
]
}
]
}

1367
test/fixtures/system-docs/snapshot.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
// Fixture: agents/defaults.ts — default agent configuration
import { AgentConfig } from './types';
export const DEFAULT_AGENT: AgentConfig = {
agentId: 'default',
model: 'claude-sonnet-4',
maxTokens: 4096,
};
export function getDefaultAgent(): AgentConfig {
return { ...DEFAULT_AGENT };
}

View File

@@ -0,0 +1,23 @@
// Fixture: agents/runner.ts — agent execution (circular dep with gateway/session.ts)
import { loadSession } from '../gateway/session';
import { AgentScope } from './scope';
import { log } from '../utils/logger';
export function runAgent(session: { key: string; agentId: string; model: string }): string {
log('running agent ' + session.agentId);
const scope = createScope(session.agentId);
return executeAgent(scope, session.model);
}
function createScope(agentId: string): AgentScope {
return { agentId, tools: getDefaultTools() };
}
function executeAgent(scope: AgentScope, model: string): string {
return `result from ${scope.agentId} on ${model}`;
}
export function reloadAgent(key: string): string {
const session = loadSession(key); // circular: agents calls gateway
return runAgent(session);
}

View File

@@ -0,0 +1,18 @@
// Fixture: agents/scope.ts — agent scope management
import { getConfig } from '../config/config';
import { log } from '../utils/logger';
export interface AgentScope {
agentId: string;
tools: string[];
}
export function createAgentScope(agentId: string): AgentScope {
log('creating scope for ' + agentId);
const config = getConfig();
return { agentId, tools: getDefaultTools() };
}
export function getDefaultTools(): string[] {
return ['read', 'write', 'exec', 'web_search'];
}

View File

@@ -0,0 +1,15 @@
// Fixture: agents/tools.ts — tool registry
import { log } from '../utils/logger';
export function registerTool(name: string, handler: Function): void {
log('registering tool: ' + name);
}
export function executeTool(name: string, args: Record<string, unknown>): unknown {
log('executing tool: ' + name);
return null;
}
export function listTools(): string[] {
return ['read', 'write', 'exec', 'web_search', 'browser'];
}

View File

@@ -0,0 +1,14 @@
// Fixture: agents/types.ts — agent type definitions
export interface AgentConfig {
agentId: string;
model: string;
maxTokens: number;
}
export interface AgentResult {
output: string;
tokensUsed: number;
duration: number;
}
export type AgentStatus = 'idle' | 'running' | 'done' | 'error';

View File

@@ -0,0 +1,12 @@
// Fixture: channels/discord.ts — Discord channel adapter
import { handleRequest } from '../gateway/server';
import { log } from '../utils/logger';
export function onDiscordMessage(guildId: string, channelId: string, text: string): void {
log('discord message in ' + guildId);
const session = handleRequest('discord:' + guildId + ':' + channelId);
}
export function sendDiscordReply(channelId: string, message: string): void {
log('sending discord reply to ' + channelId);
}

View File

@@ -0,0 +1,12 @@
// Fixture: channels/telegram.ts — Telegram channel adapter
import { handleRequest } from '../gateway/server';
import { log } from '../utils/logger';
export function onTelegramMessage(chatId: string, text: string): void {
log('telegram message from ' + chatId);
const session = handleRequest('telegram:' + chatId);
}
export function sendTelegramReply(chatId: string, message: string): void {
log('sending reply to ' + chatId);
}

View File

@@ -0,0 +1,22 @@
// Fixture: config/config.ts — central configuration
import { BaseConfig } from './types';
import { log } from '../utils/logger';
let _config: BaseConfig | null = null;
export function getConfig(): BaseConfig {
if (!_config) {
_config = loadConfigFromDisk();
}
return _config;
}
function loadConfigFromDisk(): BaseConfig {
log('loading config from disk');
return { defaultAgent: 'default', model: 'claude-sonnet-4', debug: false };
}
export function reloadConfig(): void {
_config = null;
getConfig();
}

View File

@@ -0,0 +1,18 @@
// Fixture: config/schema.ts — ORPHAN FILE (no inbound edges, only exports)
// This file is imported by nobody — tests orphan handling
export interface SchemaDefinition {
name: string;
version: string;
fields: SchemaField[];
}
export interface SchemaField {
name: string;
type: 'string' | 'number' | 'boolean' | 'object';
required: boolean;
}
export function validateSchema(schema: SchemaDefinition): boolean {
return schema.fields.length > 0 && schema.version.length > 0;
}

View File

@@ -0,0 +1,19 @@
// Fixture: config/types.ts — base config types (imported by gateway/types.ts)
export interface BaseConfig {
defaultAgent: string;
model: string;
debug: boolean;
}
export interface ProviderConfig {
name: string;
apiKey: string;
baseUrl: string;
}
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Warn = 'warn',
Error = 'error',
}

View File

@@ -0,0 +1,12 @@
// Fixture: gateway/middleware.ts — request middleware
import { log } from '../utils/logger';
import { SessionEntry } from './types';
export function applyMiddleware(session: SessionEntry): void {
log('applying middleware for ' + session.key);
validateSession(session);
}
function validateSession(session: SessionEntry): boolean {
return session.key.length > 0;
}

View File

@@ -0,0 +1,18 @@
// Fixture: gateway/server.ts — main gateway entry point
import { loadSession, saveSession } from './session';
import { applyMiddleware } from './middleware';
import { GatewayConfig, SessionEntry } from './types';
export function startGateway(config: GatewayConfig): void {
const session = loadSession(config.sessionKey);
applyMiddleware(session);
}
export function handleRequest(sessionKey: string): SessionEntry {
const session = loadSession(sessionKey);
return session;
}
export function shutdownGateway(): void {
// cleanup
}

View File

@@ -0,0 +1,19 @@
// Fixture: gateway/session.ts — session management (has circular dep with agents/runner.ts)
import { getConfig } from '../config/config';
import { runAgent } from '../agents/runner';
import { SessionEntry } from './types';
export function loadSession(key: string): SessionEntry {
const config = getConfig();
return { key, agentId: config.defaultAgent, model: config.model };
}
export function saveSession(entry: SessionEntry): void {
// persist
}
export function refreshSession(key: string): SessionEntry {
const session = loadSession(key);
runAgent(session); // circular: gateway calls agents, agents call gateway
return session;
}

View File

@@ -0,0 +1,15 @@
// Fixture: gateway/types.ts — re-exports from config/types + gateway-specific types
import { BaseConfig } from '../config/types';
export interface GatewayConfig extends BaseConfig {
sessionKey: string;
timeout: number;
}
export interface SessionEntry {
key: string;
agentId: string;
model: string;
}
export { BaseConfig } from '../config/types';

View File

@@ -0,0 +1,7 @@
// Fixture: gateway/utils.ts — gateway-specific helpers
import { log } from '../utils/logger';
export function formatSessionKey(prefix: string, id: string): string {
log('formatting key');
return `${prefix}:${id}`;
}

View File

@@ -0,0 +1,13 @@
// Fixture: utils/crypto.ts — crypto helpers
export function hashString(input: string): string {
return Buffer.from(input).toString('base64');
}
export function generateToken(length: number): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}

View File

@@ -0,0 +1,15 @@
// Fixture: utils/fs-helpers.ts — file system helpers
import { log } from './logger';
export function readFileSync(path: string): string {
log('reading ' + path);
return '';
}
export function writeFileSync(path: string, content: string): void {
log('writing ' + path);
}
export function fileExists(path: string): boolean {
return false;
}

View File

@@ -0,0 +1,14 @@
// Fixture: utils/logger.ts — cross-cutting utility (high fan-in)
// Imported by almost every other module — tests cross-cutting detection
export function log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
export function warn(message: string): void {
console.warn(`[WARN] ${message}`);
}
export function error(message: string, err?: Error): void {
console.error(`[ERROR] ${message}`, err?.message || '');
}

93
test/test-flow.js Normal file
View File

@@ -0,0 +1,93 @@
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);

109
test/test-subsystem.js Normal file
View File

@@ -0,0 +1,109 @@
const fs = require('fs');
const path = require('path');
const GraphStore = require('../graph.js');
const { buildSubsystems } = require('../subsystem.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}`); }
}
// Load graph and run subsystem aggregator
const graph = GraphStore.loadSnapshot(SNAPSHOT);
const result = buildSubsystems(graph, { minTraffic: 3, crossCuttingThreshold: 0.6 });
const expected = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'expected-subsystems.json'), 'utf8'));
console.log('=== 7A: Subsystem Aggregator Tests ===\n');
// Test 1: Directory clustering — correct number of subsystems
console.log('Test 1: Directory clustering');
assert(result.subsystems.length === 5, `Found 5 subsystems (got ${result.subsystems.length})`);
const names = result.subsystems.map(s => s.name).sort();
assert(JSON.stringify(names) === JSON.stringify(['agents', 'channels', 'config', 'gateway', 'utils']),
`Subsystem names match: ${names.join(', ')}`);
// Test 2: File assignment accuracy
console.log('\nTest 2: File assignment accuracy');
let correctFiles = 0;
let totalFiles = 0;
for (const expSub of expected.subsystems) {
const actualSub = result.subsystems.find(s => s.name === expSub.name);
assert(!!actualSub, `Subsystem "${expSub.name}" exists`);
if (actualSub) {
for (const f of expSub.files) {
totalFiles++;
if (actualSub.files.includes(f)) correctFiles++;
else console.log(` MISSING: ${f} in ${expSub.name}`);
}
}
}
const accuracy = totalFiles > 0 ? (correctFiles / totalFiles * 100).toFixed(1) : 0;
assert(correctFiles / totalFiles >= 0.9, `File assignment accuracy ≥90% (${accuracy}%, ${correctFiles}/${totalFiles})`);
// Test 3: Cross-cutting detection
console.log('\nTest 3: Cross-cutting detection');
assert(result.crossCutting.includes('utils'), 'utils detected as cross-cutting');
assert(result.crossCutting.includes('config'), 'config detected as cross-cutting');
assert(!result.crossCutting.includes('gateway'), 'gateway is NOT cross-cutting');
assert(!result.crossCutting.includes('agents'), 'agents is NOT cross-cutting');
assert(!result.crossCutting.includes('channels'), 'channels is NOT cross-cutting');
// Verify kind field matches
for (const expSub of expected.subsystems) {
const actualSub = result.subsystems.find(s => s.name === expSub.name);
if (actualSub) {
assert(actualSub.kind === expSub.kind, `${expSub.name} kind="${actualSub.kind}" (expected "${expSub.kind}")`);
}
}
// Test 4: Dependency matrix — key edges exist
console.log('\nTest 4: Dependency matrix');
const dm = result.dependencyMatrix;
assert('gateway→agents' in dm, 'gateway→agents edge exists');
assert('agents→gateway' in dm, 'agents→gateway edge exists (circular dep)');
assert('channels→gateway' in dm, 'channels→gateway edge exists');
assert('gateway→config' in dm, 'gateway→config edge exists');
assert('agents→utils' in dm, 'agents→utils edge exists');
assert('channels→utils' in dm, 'channels→utils edge exists');
// Test 5: Empty subsystem handling (channels has only 2 files, no internal CALLS)
console.log('\nTest 5: Edge cases');
const channelsSub = result.subsystems.find(s => s.name === 'channels');
assert(channelsSub && channelsSub.files.length === 2, `channels has 2 files (got ${channelsSub?.files.length})`);
// Orphan file: config/schema.ts should still be in config subsystem
const configSub = result.subsystems.find(s => s.name === 'config');
assert(configSub && configSub.files.includes('config/schema.ts'), 'Orphan config/schema.ts assigned to config');
// Test 6: Public exports populated
console.log('\nTest 6: Public exports');
const gatewaySub = result.subsystems.find(s => s.name === 'gateway');
assert(gatewaySub.publicExports.includes('handleRequest'), 'gateway exports handleRequest');
assert(gatewaySub.publicExports.includes('loadSession'), 'gateway exports loadSession');
const agentsSub = result.subsystems.find(s => s.name === 'agents');
assert(agentsSub.publicExports.includes('runAgent'), 'agents exports runAgent');
// Test 7: Run on OpenClaw full snapshot (performance)
console.log('\nTest 7: OpenClaw full snapshot');
const fullSnap = path.join(__dirname, '..', 'snapshots', 'openclaw-full.json');
if (fs.existsSync(fullSnap)) {
const start = Date.now();
const fullGraph = GraphStore.loadSnapshot(fullSnap);
const fullResult = buildSubsystems(fullGraph);
const elapsed = Date.now() - start;
assert(fullResult.subsystems.length > 10, `OpenClaw has >10 subsystems (got ${fullResult.subsystems.length})`);
assert(elapsed < 5000, `Completed in <5s (${elapsed}ms)`);
assert(fullResult.crossCutting.length > 0, `Detected cross-cutting subsystems: ${fullResult.crossCutting.join(', ')}`);
console.log(` OpenClaw: ${fullResult.subsystems.length} subsystems, ${Object.keys(fullResult.dependencyMatrix).length} dep edges, ${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);