mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
0b128530c1
- 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>
344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Value Network Tests
|
|
* ADR-034: Neural Topology Optimizer
|
|
*
|
|
* Tests for the ValueNetwork implementation used in RL-based topology optimization.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
ValueNetwork,
|
|
createValueNetwork,
|
|
createValueNetworkFromWeights,
|
|
} from '../../../src/neural-optimizer/value-network';
|
|
|
|
describe('ValueNetwork', () => {
|
|
describe('initialization', () => {
|
|
it('should create network with specified dimensions', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const dims = network.getDimensions();
|
|
|
|
expect(dims.inputSize).toBe(10);
|
|
expect(dims.hiddenSize).toBe(32);
|
|
});
|
|
|
|
it('should initialize with non-zero weights', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const weights = network.export();
|
|
|
|
// Check that weights are not all zero
|
|
const hasNonZeroHidden = weights.wHidden.some((row) =>
|
|
row.some((w) => w !== 0)
|
|
);
|
|
expect(hasNonZeroHidden).toBe(true);
|
|
|
|
const hasNonZeroOutput = weights.wOutput.some((w) => w !== 0);
|
|
expect(hasNonZeroOutput).toBe(true);
|
|
});
|
|
|
|
it('should use Xavier/He initialization scale', () => {
|
|
const inputSize = 100;
|
|
const hiddenSize = 64;
|
|
const network = new ValueNetwork(inputSize, hiddenSize);
|
|
const weights = network.export();
|
|
|
|
// Check that weights are within reasonable bounds for He initialization
|
|
// Expected std ~ sqrt(2/inputSize) ~ 0.14
|
|
const hiddenWeights = weights.wHidden.flat();
|
|
const maxWeight = Math.max(...hiddenWeights.map(Math.abs));
|
|
|
|
// Should be within 4 standard deviations most of the time
|
|
expect(maxWeight).toBeLessThan(1.0);
|
|
});
|
|
});
|
|
|
|
describe('estimate', () => {
|
|
it('should return a number for valid input', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const value = network.estimate(state);
|
|
|
|
expect(typeof value).toBe('number');
|
|
expect(Number.isFinite(value)).toBe(true);
|
|
});
|
|
|
|
it('should handle zero input', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0);
|
|
|
|
const value = network.estimate(state);
|
|
|
|
expect(typeof value).toBe('number');
|
|
expect(Number.isFinite(value)).toBe(true);
|
|
});
|
|
|
|
it('should pad shorter input vectors', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const shortState = [0.5, 0.5, 0.5]; // Only 3 elements
|
|
|
|
// Should not throw
|
|
const value = network.estimate(shortState);
|
|
expect(typeof value).toBe('number');
|
|
});
|
|
|
|
it('should truncate longer input vectors', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const longState = Array(20).fill(0.5); // 20 elements
|
|
|
|
// Should not throw
|
|
const value = network.estimate(longState);
|
|
expect(typeof value).toBe('number');
|
|
});
|
|
|
|
it('should produce different outputs for different inputs', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state1 = Array(10).fill(0.1);
|
|
const state2 = Array(10).fill(0.9);
|
|
|
|
const value1 = network.estimate(state1);
|
|
const value2 = network.estimate(state2);
|
|
|
|
// Different inputs should generally produce different outputs
|
|
// (with high probability for randomly initialized network)
|
|
expect(value1).not.toBe(value2);
|
|
});
|
|
|
|
it('should update lastEstimate', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const value = network.estimate(state);
|
|
|
|
expect(network.getLastEstimate()).toBe(value);
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('should modify weights based on TD error', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const weightsBefore = network.export();
|
|
network.update(state, 1.0, 0.01);
|
|
const weightsAfter = network.export();
|
|
|
|
// At least some weights should have changed
|
|
let changed = false;
|
|
for (let j = 0; j < weightsBefore.wHidden.length; j++) {
|
|
for (let i = 0; i < weightsBefore.wHidden[j].length; i++) {
|
|
if (weightsBefore.wHidden[j][i] !== weightsAfter.wHidden[j][i]) {
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (changed) break;
|
|
}
|
|
|
|
expect(changed).toBe(true);
|
|
});
|
|
|
|
it('should increase value estimate for positive TD error', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const valueBefore = network.estimate(state);
|
|
network.update(state, 1.0, 0.1); // Positive TD error
|
|
const valueAfter = network.estimate(state);
|
|
|
|
// Value should increase (or stay similar) for positive error
|
|
expect(valueAfter).toBeGreaterThanOrEqual(valueBefore - 0.1);
|
|
});
|
|
|
|
it('should decrease value estimate for negative TD error', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const valueBefore = network.estimate(state);
|
|
network.update(state, -1.0, 0.1); // Negative TD error
|
|
const valueAfter = network.estimate(state);
|
|
|
|
// Value should decrease (or stay similar) for negative error
|
|
expect(valueAfter).toBeLessThanOrEqual(valueBefore + 0.1);
|
|
});
|
|
|
|
it('should clip large gradients', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
// Very large TD error
|
|
network.update(state, 1000, 0.1);
|
|
|
|
// Network should still be stable
|
|
const value = network.estimate(state);
|
|
expect(Number.isFinite(value)).toBe(true);
|
|
});
|
|
|
|
it('should handle zero learning rate', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const weightsBefore = JSON.stringify(network.export());
|
|
network.update(state, 1.0, 0); // Zero learning rate
|
|
const weightsAfter = JSON.stringify(network.export());
|
|
|
|
// Weights should not change
|
|
expect(weightsAfter).toBe(weightsBefore);
|
|
});
|
|
});
|
|
|
|
describe('copyFrom', () => {
|
|
it('should copy weights from another network', () => {
|
|
const network1 = new ValueNetwork(10, 32);
|
|
const network2 = new ValueNetwork(10, 32);
|
|
|
|
// Update network1
|
|
network1.update(Array(10).fill(0.5), 1.0, 0.1);
|
|
|
|
// Copy to network2
|
|
network2.copyFrom(network1);
|
|
|
|
// Both should produce same estimate
|
|
const state = Array(10).fill(0.3);
|
|
expect(network2.estimate(state)).toBe(network1.estimate(state));
|
|
});
|
|
});
|
|
|
|
describe('softUpdate', () => {
|
|
it('should perform Polyak averaging', () => {
|
|
const network1 = new ValueNetwork(10, 32);
|
|
const network2 = new ValueNetwork(10, 32);
|
|
|
|
const weights1Before = network1.export().wHidden[0][0];
|
|
const weights2 = network2.export().wHidden[0][0];
|
|
|
|
network1.softUpdate(network2, 0.5);
|
|
|
|
const weights1After = network1.export().wHidden[0][0];
|
|
|
|
// Should be average with tau=0.5
|
|
const expected = 0.5 * weights2 + 0.5 * weights1Before;
|
|
expect(weights1After).toBeCloseTo(expected, 5);
|
|
});
|
|
|
|
it('should maintain network stability with small tau', () => {
|
|
const network1 = new ValueNetwork(10, 32);
|
|
const network2 = new ValueNetwork(10, 32);
|
|
|
|
const weightsBefore = network1.export().wHidden[0][0];
|
|
|
|
// Small tau should result in small change
|
|
network1.softUpdate(network2, 0.01);
|
|
|
|
const weightsAfter = network1.export().wHidden[0][0];
|
|
const change = Math.abs(weightsAfter - weightsBefore);
|
|
|
|
expect(change).toBeLessThan(0.1);
|
|
});
|
|
});
|
|
|
|
describe('export/import', () => {
|
|
it('should export weights in correct format', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const weights = network.export();
|
|
|
|
expect(weights.wHidden).toHaveLength(32);
|
|
expect(weights.wHidden[0]).toHaveLength(10);
|
|
expect(weights.bHidden).toHaveLength(32);
|
|
expect(weights.wOutput).toHaveLength(32);
|
|
expect(typeof weights.bOutput).toBe('number');
|
|
});
|
|
|
|
it('should import weights and reproduce estimates', () => {
|
|
const network1 = new ValueNetwork(10, 32);
|
|
const weights = network1.export();
|
|
|
|
const network2 = new ValueNetwork(10, 32);
|
|
network2.import(weights);
|
|
|
|
const state = Array(10).fill(0.5);
|
|
expect(network2.estimate(state)).toBe(network1.estimate(state));
|
|
});
|
|
|
|
it('should throw on dimension mismatch', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const wrongWeights = {
|
|
wHidden: [[1, 2, 3]], // Wrong dimensions
|
|
bHidden: [1],
|
|
wOutput: [1],
|
|
bOutput: 0,
|
|
};
|
|
|
|
expect(() => network.import(wrongWeights)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getWeightNorm', () => {
|
|
it('should return positive L2 norm', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const norm = network.getWeightNorm();
|
|
|
|
expect(norm).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should increase after updates', () => {
|
|
const network = new ValueNetwork(10, 32);
|
|
const state = Array(10).fill(0.5);
|
|
|
|
const normBefore = network.getWeightNorm();
|
|
|
|
// Multiple updates with large errors
|
|
for (let i = 0; i < 10; i++) {
|
|
network.update(state, 1.0, 0.1);
|
|
}
|
|
|
|
// Norm might increase or decrease depending on gradients
|
|
const normAfter = network.getWeightNorm();
|
|
expect(typeof normAfter).toBe('number');
|
|
expect(Number.isFinite(normAfter)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('factory functions', () => {
|
|
it('createValueNetwork should create valid network', () => {
|
|
const network = createValueNetwork(16, 64);
|
|
const dims = network.getDimensions();
|
|
|
|
expect(dims.inputSize).toBe(16);
|
|
expect(dims.hiddenSize).toBe(64);
|
|
});
|
|
|
|
it('createValueNetworkFromWeights should restore network', () => {
|
|
const original = createValueNetwork(10, 32);
|
|
const weights = original.export();
|
|
|
|
const restored = createValueNetworkFromWeights(weights);
|
|
|
|
const state = Array(10).fill(0.5);
|
|
expect(restored.estimate(state)).toBe(original.estimate(state));
|
|
});
|
|
});
|
|
|
|
describe('learning convergence', () => {
|
|
it('should learn to estimate constant value', () => {
|
|
const network = new ValueNetwork(4, 16);
|
|
const targetValue = 0.5;
|
|
|
|
// Train to estimate constant value
|
|
for (let i = 0; i < 100; i++) {
|
|
const state = [Math.random(), Math.random(), Math.random(), Math.random()];
|
|
const estimate = network.estimate(state);
|
|
const tdError = targetValue - estimate;
|
|
network.update(state, tdError, 0.01);
|
|
}
|
|
|
|
// Test on new states
|
|
const testState = [0.3, 0.3, 0.3, 0.3];
|
|
const finalEstimate = network.estimate(testState);
|
|
|
|
// Should be reasonably close to target
|
|
expect(Math.abs(finalEstimate - targetValue)).toBeLessThan(0.5);
|
|
});
|
|
});
|
|
});
|