Files
Profa 0b128530c1 feat: wire 26 MCP tools via bridge, add 4 new tools with CLI, cleanup dead code
- Remove 9 Istanbul static assets from src/coverage/, add gitignore patterns
- Create qe-tool-bridge.ts: registers 26 QE tools missing from protocol-server
  (GOAP, embeddings, coherence, mincut, learning, coverage/gaps, QX, etc.)
- Add 4 new MCP tools from previously unwired source modules:
  - qe/tests/schedule: phase-based test pipeline with git-aware selection
  - qe/tests/load: agent fleet load testing with mock/real mode
  - qe/security/url-validate: URL threat detection + PII exposure scanning
  - qe/workflows/browser-load: YAML workflow loading/validation
- Add CLI subcommands for all 4 new tools:
  - aqe test schedule/load with full option support
  - aqe security --url-validate
  - aqe workflow browser-list/browser-load
- Enrich fleet_health with structural health (mincut-lambda) from real agents
- Delete dead src/cli/commands/qe-tools.ts (935 lines, zero imports)
- Archive neural-optimizer to src/_archived/ (exclude from tsconfig)
- Add docs/integration-roadmap.md with full MCP tool inventory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:55:56 +00:00

596 lines
18 KiB
TypeScript

/**
* Agentic QE v3 - Neural Topology Optimizer Tests
* ADR-034: Neural Topology Optimizer
*
* Tests for the NeuralTopologyOptimizer implementation.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
NeuralTopologyOptimizer,
createNeuralTopologyOptimizer,
} from '../../../src/neural-optimizer/topology-optimizer';
import {
MutableSwarmTopology,
createTopology,
createAgent,
buildMeshTopology,
buildRingTopology,
} from '../../../src/neural-optimizer/swarm-topology';
import {
DEFAULT_OPTIMIZER_CONFIG,
actionToIndex,
indexToActionType,
ACTION_TYPES,
} from '../../../src/neural-optimizer/types';
import type { TopologyAgent } from '../../../src/neural-optimizer/types';
function createTestTopology(numAgents: number = 5): MutableSwarmTopology {
const topology = createTopology('custom');
for (let i = 0; i < numAgents; i++) {
topology.addAgent(
createAgent(`agent-${i}`, 'worker', {
metrics: {
tasksCompleted: Math.floor(Math.random() * 100),
avgTaskDurationMs: Math.random() * 1000,
successRate: 0.8 + Math.random() * 0.2,
currentLoad: Math.random() * 0.6,
},
})
);
}
// Add some initial connections
for (let i = 0; i < numAgents - 1; i++) {
topology.addConnection(`agent-${i}`, `agent-${i + 1}`, Math.random());
}
return topology;
}
describe('NeuralTopologyOptimizer', () => {
let topology: MutableSwarmTopology;
let optimizer: NeuralTopologyOptimizer;
beforeEach(() => {
topology = createTestTopology(5);
optimizer = new NeuralTopologyOptimizer(topology);
});
describe('initialization', () => {
it('should create optimizer with default config', () => {
const stats = optimizer.getStats();
expect(stats.totalSteps).toBe(0);
expect(stats.episodes).toBe(0);
expect(stats.currentEpsilon).toBe(DEFAULT_OPTIMIZER_CONFIG.epsilon);
});
it('should accept custom config', () => {
const customOptimizer = new NeuralTopologyOptimizer(topology, {
epsilon: 0.5,
learningRate: 0.01,
hiddenSize: 128,
});
const stats = customOptimizer.getStats();
expect(stats.currentEpsilon).toBe(0.5);
});
it('should handle empty topology', () => {
const emptyTopology = createTopology();
const emptyOptimizer = new NeuralTopologyOptimizer(emptyTopology);
// Should not throw
const result = emptyOptimizer.optimizeStep();
expect(result).toBeDefined();
});
});
describe('optimizeStep', () => {
it('should return optimization result', () => {
const result = optimizer.optimizeStep();
expect(result.action).toBeDefined();
expect(typeof result.reward).toBe('number');
expect(typeof result.newMinCut).toBe('number');
expect(typeof result.communicationEfficiency).toBe('number');
expect(typeof result.loadBalance).toBe('number');
expect(typeof result.tdError).toBe('number');
expect(typeof result.epsilon).toBe('number');
});
it('should increment total steps', () => {
const statsBefore = optimizer.getStats();
optimizer.optimizeStep();
const statsAfter = optimizer.getStats();
expect(statsAfter.totalSteps).toBe(statsBefore.totalSteps + 1);
});
it('should decay epsilon over time', () => {
const initialEpsilon = optimizer.getStats().currentEpsilon;
for (let i = 0; i < 10; i++) {
optimizer.optimizeStep();
}
const finalEpsilon = optimizer.getStats().currentEpsilon;
expect(finalEpsilon).toBeLessThan(initialEpsilon);
});
it('should not decay epsilon below minimum', () => {
const minEpsilon = 0.01;
const fastDecayOptimizer = new NeuralTopologyOptimizer(topology, {
epsilon: 0.1,
epsilonDecay: 0.5,
minEpsilon,
});
for (let i = 0; i < 100; i++) {
fastDecayOptimizer.optimizeStep();
}
expect(fastDecayOptimizer.getStats().currentEpsilon).toBeGreaterThanOrEqual(
minEpsilon
);
});
it('should update action counts', () => {
const statsBefore = optimizer.getStats();
const totalActionsBefore = Object.values(statsBefore.actionCounts).reduce(
(sum, count) => sum + count,
0
);
optimizer.optimizeStep();
const statsAfter = optimizer.getStats();
const totalActionsAfter = Object.values(statsAfter.actionCounts).reduce(
(sum, count) => sum + count,
0
);
expect(totalActionsAfter).toBe(totalActionsBefore + 1);
});
it('should track rewards', () => {
optimizer.optimizeStep();
const stats = optimizer.getStats();
expect(stats.rewardHistory).toHaveLength(1);
});
it('should track min-cut history', () => {
optimizer.optimizeStep();
const stats = optimizer.getStats();
expect(stats.minCutHistory).toHaveLength(1);
});
});
describe('optimize (multiple steps)', () => {
it('should run multiple optimization steps', () => {
const results = optimizer.optimize(10);
expect(results).toHaveLength(10);
expect(optimizer.getStats().totalSteps).toBe(10);
});
it('should increment episode count', () => {
optimizer.optimize(10);
expect(optimizer.getStats().episodes).toBe(1);
optimizer.optimize(10);
expect(optimizer.getStats().episodes).toBe(2);
});
it('should accumulate cumulative reward', () => {
const results = optimizer.optimize(10);
const totalReward = results.reduce((sum, r) => sum + r.reward, 0);
const stats = optimizer.getStats();
expect(Math.abs(stats.cumulativeReward - totalReward)).toBeLessThan(0.001);
});
});
describe('provideFeedback', () => {
it('should accept external feedback', () => {
optimizer.optimizeStep();
// Should not throw
optimizer.provideFeedback(1.0);
});
it('should update learning based on feedback', () => {
// Run some steps first
for (let i = 0; i < 5; i++) {
optimizer.optimizeStep();
}
// Provide strong positive feedback
optimizer.provideFeedback(1.0);
// Stats should be updated
const stats = optimizer.getStats();
expect(stats.totalSteps).toBeGreaterThan(0);
});
});
describe('getSkipRegions', () => {
it('should return low connectivity agents', () => {
// Create topology with isolated agent
const sparseTopology = createTopology();
sparseTopology.addAgent(createAgent('connected-1', 'worker'));
sparseTopology.addAgent(createAgent('connected-2', 'worker'));
sparseTopology.addAgent(createAgent('isolated', 'worker'));
sparseTopology.addConnection('connected-1', 'connected-2');
const sparseOptimizer = new NeuralTopologyOptimizer(sparseTopology);
const skipRegions = sparseOptimizer.getSkipRegions();
// Isolated agent should be in skip regions (degree < 2)
expect(skipRegions).toContain('isolated');
});
it('should return empty array for well-connected topology', () => {
const meshTopology = buildMeshTopology([
createAgent('agent-1', 'worker'),
createAgent('agent-2', 'worker'),
createAgent('agent-3', 'worker'),
]);
const meshOptimizer = new NeuralTopologyOptimizer(meshTopology);
const skipRegions = meshOptimizer.getSkipRegions();
expect(skipRegions).toHaveLength(0);
});
});
describe('getStats', () => {
it('should return valid statistics', () => {
optimizer.optimize(10);
const stats = optimizer.getStats();
expect(stats.totalSteps).toBe(10);
expect(stats.episodes).toBe(1);
expect(typeof stats.cumulativeReward).toBe('number');
expect(typeof stats.avgReward).toBe('number');
expect(typeof stats.avgTdError).toBe('number');
expect(stats.minCutHistory).toHaveLength(10);
expect(stats.rewardHistory).toHaveLength(10);
});
it('should calculate average reward correctly', () => {
const results = optimizer.optimize(10);
const expectedAvg =
results.reduce((sum, r) => sum + r.reward, 0) / results.length;
const stats = optimizer.getStats();
expect(Math.abs(stats.avgReward - expectedAvg)).toBeLessThan(0.001);
});
it('should track all action types', () => {
optimizer.optimize(100);
const stats = optimizer.getStats();
for (const actionType of ACTION_TYPES) {
expect(actionType in stats.actionCounts).toBe(true);
}
});
});
describe('reset', () => {
it('should reset optimizer state', () => {
optimizer.optimize(10);
optimizer.reset();
const stats = optimizer.getStats();
expect(stats.currentEpsilon).toBe(DEFAULT_OPTIMIZER_CONFIG.epsilon);
});
it('should preserve learned weights', () => {
optimizer.optimize(50);
const modelBefore = optimizer.exportModel();
optimizer.reset();
const modelAfter = optimizer.exportModel();
expect(modelAfter.valueNetwork.wHidden[0][0]).toBe(
modelBefore.valueNetwork.wHidden[0][0]
);
});
});
describe('hardReset', () => {
it('should clear all learning', () => {
optimizer.optimize(10);
optimizer.hardReset();
const stats = optimizer.getStats();
expect(stats.totalSteps).toBe(0);
expect(stats.episodes).toBe(0);
expect(stats.cumulativeReward).toBe(0);
expect(stats.minCutHistory).toHaveLength(0);
});
it('should reinitialize networks', () => {
optimizer.optimize(50);
const modelBefore = optimizer.exportModel();
optimizer.hardReset();
const modelAfter = optimizer.exportModel();
// Weights should be different after reinitialization
expect(modelAfter.valueNetwork.wHidden[0][0]).not.toBe(
modelBefore.valueNetwork.wHidden[0][0]
);
});
});
describe('exportModel', () => {
it('should export valid model', () => {
optimizer.optimize(10);
const model = optimizer.exportModel();
expect(model.type).toBe('neural-topology-optimizer');
expect(model.version).toBeDefined();
expect(model.config).toBeDefined();
expect(model.valueNetwork).toBeDefined();
expect(model.targetNetwork).toBeDefined();
expect(model.stats).toBeDefined();
expect(model.exportedAt).toBeDefined();
});
it('should include training stats', () => {
optimizer.optimize(10);
const model = optimizer.exportModel();
expect(model.stats.totalSteps).toBe(10);
expect(model.stats.episodes).toBe(1);
});
it('should include network weights', () => {
optimizer.optimize(10);
const model = optimizer.exportModel();
expect(model.valueNetwork.wHidden).toBeDefined();
expect(model.valueNetwork.bHidden).toBeDefined();
expect(model.valueNetwork.wOutput).toBeDefined();
expect(typeof model.valueNetwork.bOutput).toBe('number');
});
});
describe('importModel', () => {
it('should import exported model', () => {
optimizer.optimize(50);
const model = optimizer.exportModel();
const newTopology = createTestTopology(5);
const newOptimizer = new NeuralTopologyOptimizer(newTopology);
newOptimizer.importModel(model);
const newStats = newOptimizer.getStats();
expect(newStats.totalSteps).toBe(50);
expect(newStats.currentEpsilon).toBe(model.stats.currentEpsilon);
});
it('should throw on invalid model type', () => {
const invalidModel = {
type: 'invalid-type',
version: '1.0.0',
config: DEFAULT_OPTIMIZER_CONFIG,
valueNetwork: { wHidden: [[0]], bHidden: [0], wOutput: [0], bOutput: 0 },
stats: optimizer.getStats(),
exportedAt: new Date().toISOString(),
};
expect(() => optimizer.importModel(invalidModel as any)).toThrow();
});
it('should reproduce same behavior after import', () => {
// Train original
optimizer.optimize(100);
// Export and import
const model = optimizer.exportModel();
const newTopology = createTestTopology(5);
const newOptimizer = new NeuralTopologyOptimizer(newTopology);
newOptimizer.importModel(model);
// Both should have same epsilon
expect(newOptimizer.getStats().currentEpsilon).toBe(
optimizer.getStats().currentEpsilon
);
});
});
describe('topology modifications', () => {
it('should add connections during optimization', () => {
const sparseTopology = createTopology();
sparseTopology.addAgent(createAgent('agent-1', 'worker'));
sparseTopology.addAgent(createAgent('agent-2', 'worker'));
sparseTopology.addAgent(createAgent('agent-3', 'worker'));
// No initial connections
const sparseOptimizer = new NeuralTopologyOptimizer(sparseTopology, {
epsilon: 1.0, // Force exploration
});
const initialConnections = sparseTopology.connections.length;
// Run many steps to ensure some add_connection actions
for (let i = 0; i < 100; i++) {
sparseOptimizer.optimizeStep();
}
const stats = sparseOptimizer.getStats();
expect(stats.actionCounts['add_connection']).toBeGreaterThan(0);
});
it('should remove connections during optimization', () => {
const denseTopology = buildMeshTopology([
createAgent('agent-1', 'worker'),
createAgent('agent-2', 'worker'),
createAgent('agent-3', 'worker'),
createAgent('agent-4', 'worker'),
]);
const denseOptimizer = new NeuralTopologyOptimizer(denseTopology, {
epsilon: 1.0, // Force exploration
});
for (let i = 0; i < 100; i++) {
denseOptimizer.optimizeStep();
}
const stats = denseOptimizer.getStats();
expect(stats.actionCounts['remove_connection']).toBeGreaterThan(0);
});
it('should modify connection weights', () => {
optimizer.optimize(100);
const stats = optimizer.getStats();
const weightModifications =
stats.actionCounts['strengthen_connection'] +
stats.actionCounts['weaken_connection'];
expect(weightModifications).toBeGreaterThan(0);
});
});
describe('learning behavior', () => {
it('should learn from experience replay', () => {
const learningOptimizer = new NeuralTopologyOptimizer(topology, {
minExperiencesForTraining: 10,
batchSize: 8,
});
// Generate enough experiences
for (let i = 0; i < 20; i++) {
learningOptimizer.optimizeStep();
}
// Should have trained from replay
const stats = learningOptimizer.getStats();
expect(stats.totalSteps).toBe(20);
});
it('should converge value estimates over time', { retry: 3 }, () => {
const learningOptimizer = new NeuralTopologyOptimizer(topology, {
learningRate: 0.01,
minExperiencesForTraining: 50,
});
// Track TD errors over time
const earlyTdErrors: number[] = [];
const lateTdErrors: number[] = [];
for (let i = 0; i < 200; i++) {
const result = learningOptimizer.optimizeStep();
if (i < 50) {
earlyTdErrors.push(Math.abs(result.tdError));
} else if (i >= 150) {
lateTdErrors.push(Math.abs(result.tdError));
}
}
const earlyAvg = earlyTdErrors.reduce((a, b) => a + b, 0) / earlyTdErrors.length;
const lateAvg = lateTdErrors.reduce((a, b) => a + b, 0) / lateTdErrors.length;
// TD errors should generally decrease (value estimates improve)
// Probabilistic: widen tolerance to 3x and retry up to 3 times (Issue #251)
expect(lateAvg).toBeLessThanOrEqual(earlyAvg * 3);
});
});
describe('multi-objective reward', () => {
it('should consider efficiency in reward', () => {
const efficiencyOptimizer = new NeuralTopologyOptimizer(topology, {
efficiencyWeight: 1.0,
loadBalanceWeight: 0,
latencyWeight: 0,
});
const result = efficiencyOptimizer.optimizeStep();
expect(typeof result.communicationEfficiency).toBe('number');
expect(result.communicationEfficiency).toBeGreaterThanOrEqual(0);
expect(result.communicationEfficiency).toBeLessThanOrEqual(1);
});
it('should consider load balance in reward', () => {
const loadOptimizer = new NeuralTopologyOptimizer(topology, {
efficiencyWeight: 0,
loadBalanceWeight: 1.0,
latencyWeight: 0,
});
const result = loadOptimizer.optimizeStep();
expect(typeof result.loadBalance).toBe('number');
});
it('should bound reward to [-1, 1]', () => {
for (let i = 0; i < 50; i++) {
const result = optimizer.optimizeStep();
expect(result.reward).toBeGreaterThanOrEqual(-1);
expect(result.reward).toBeLessThanOrEqual(1);
}
});
});
describe('factory function', () => {
it('createNeuralTopologyOptimizer should create optimizer', () => {
const factoryOptimizer = createNeuralTopologyOptimizer(topology, {
epsilon: 0.2,
});
const stats = factoryOptimizer.getStats();
expect(stats.currentEpsilon).toBe(0.2);
});
});
});
describe('action helpers', () => {
describe('actionToIndex', () => {
it('should map actions to indices', () => {
expect(actionToIndex({ type: 'add_connection', from: 'a', to: 'b' })).toBe(0);
expect(actionToIndex({ type: 'remove_connection', from: 'a', to: 'b' })).toBe(1);
expect(
actionToIndex({ type: 'strengthen_connection', from: 'a', to: 'b', delta: 0.1 })
).toBe(2);
expect(
actionToIndex({ type: 'weaken_connection', from: 'a', to: 'b', delta: 0.1 })
).toBe(3);
expect(actionToIndex({ type: 'no_op' })).toBe(4);
});
});
describe('indexToActionType', () => {
it('should map indices to action types', () => {
expect(indexToActionType(0)).toBe('add_connection');
expect(indexToActionType(1)).toBe('remove_connection');
expect(indexToActionType(2)).toBe('strengthen_connection');
expect(indexToActionType(3)).toBe('weaken_connection');
expect(indexToActionType(4)).toBe('no_op');
});
it('should wrap around for large indices', () => {
expect(indexToActionType(5)).toBe('add_connection');
expect(indexToActionType(6)).toBe('remove_connection');
});
});
});