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>
404 lines
11 KiB
TypeScript
404 lines
11 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Replay Buffer Tests
|
|
* ADR-034: Neural Topology Optimizer
|
|
*
|
|
* Tests for PrioritizedReplayBuffer and UniformReplayBuffer implementations.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
PrioritizedReplayBuffer,
|
|
UniformReplayBuffer,
|
|
createPrioritizedReplayBuffer,
|
|
createUniformReplayBuffer,
|
|
} from '../../../src/neural-optimizer/replay-buffer';
|
|
import type { Experience } from '../../../src/neural-optimizer/types';
|
|
|
|
function createExperience(
|
|
reward: number,
|
|
tdError: number = Math.abs(reward)
|
|
): Experience {
|
|
return {
|
|
state: [Math.random(), Math.random(), Math.random()],
|
|
actionIdx: Math.floor(Math.random() * 5),
|
|
reward,
|
|
nextState: [Math.random(), Math.random(), Math.random()],
|
|
done: false,
|
|
tdError,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
describe('PrioritizedReplayBuffer', () => {
|
|
let buffer: PrioritizedReplayBuffer;
|
|
|
|
beforeEach(() => {
|
|
buffer = new PrioritizedReplayBuffer(100);
|
|
});
|
|
|
|
describe('initialization', () => {
|
|
it('should start empty', () => {
|
|
expect(buffer.length).toBe(0);
|
|
});
|
|
|
|
it('should accept custom options', () => {
|
|
const customBuffer = new PrioritizedReplayBuffer(50, {
|
|
alpha: 0.8,
|
|
beta: 0.6,
|
|
betaAnnealing: 0.002,
|
|
});
|
|
|
|
expect(customBuffer.length).toBe(0);
|
|
expect(customBuffer.getBeta()).toBe(0.6);
|
|
});
|
|
});
|
|
|
|
describe('push', () => {
|
|
it('should add experiences', () => {
|
|
buffer.push(createExperience(1.0));
|
|
|
|
expect(buffer.length).toBe(1);
|
|
});
|
|
|
|
it('should increment length up to capacity', () => {
|
|
for (let i = 0; i < 50; i++) {
|
|
buffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
expect(buffer.length).toBe(50);
|
|
});
|
|
|
|
it('should not exceed capacity', () => {
|
|
const smallBuffer = new PrioritizedReplayBuffer(10);
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
smallBuffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
expect(smallBuffer.length).toBe(10);
|
|
});
|
|
|
|
it('should overwrite old experiences when full', () => {
|
|
const smallBuffer = new PrioritizedReplayBuffer(5);
|
|
|
|
// Fill buffer
|
|
for (let i = 0; i < 5; i++) {
|
|
smallBuffer.push(createExperience(i));
|
|
}
|
|
|
|
// Add one more
|
|
smallBuffer.push(createExperience(999));
|
|
|
|
// Should still be at capacity
|
|
expect(smallBuffer.length).toBe(5);
|
|
|
|
// Sample should contain the new experience
|
|
const samples = smallBuffer.sample(5);
|
|
const hasNew = samples.some((exp) => exp.reward === 999);
|
|
expect(hasNew).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('sample', () => {
|
|
beforeEach(() => {
|
|
// Fill buffer with experiences
|
|
for (let i = 0; i < 50; i++) {
|
|
buffer.push(createExperience(Math.random(), Math.random() + 0.1));
|
|
}
|
|
});
|
|
|
|
it('should return requested number of samples', () => {
|
|
const samples = buffer.sample(10);
|
|
|
|
expect(samples.length).toBe(10);
|
|
});
|
|
|
|
it('should return fewer samples if buffer is smaller', () => {
|
|
const smallBuffer = new PrioritizedReplayBuffer(100);
|
|
smallBuffer.push(createExperience(1.0));
|
|
smallBuffer.push(createExperience(2.0));
|
|
|
|
const samples = smallBuffer.sample(10);
|
|
|
|
expect(samples.length).toBe(2);
|
|
});
|
|
|
|
it('should return valid experiences', () => {
|
|
const samples = buffer.sample(10);
|
|
|
|
for (const exp of samples) {
|
|
expect(exp.state).toHaveLength(3);
|
|
expect(exp.nextState).toHaveLength(3);
|
|
expect(typeof exp.reward).toBe('number');
|
|
expect(typeof exp.actionIdx).toBe('number');
|
|
expect(typeof exp.done).toBe('boolean');
|
|
}
|
|
});
|
|
|
|
it('should return empty array for empty buffer', () => {
|
|
const emptyBuffer = new PrioritizedReplayBuffer(100);
|
|
const samples = emptyBuffer.sample(10);
|
|
|
|
expect(samples).toHaveLength(0);
|
|
});
|
|
|
|
it('should prioritize high TD error experiences', () => {
|
|
const priorityBuffer = new PrioritizedReplayBuffer(100, { alpha: 1.0 });
|
|
|
|
// Add low priority experiences
|
|
for (let i = 0; i < 90; i++) {
|
|
priorityBuffer.push(createExperience(0.1, 0.01));
|
|
}
|
|
|
|
// Add high priority experiences
|
|
for (let i = 0; i < 10; i++) {
|
|
priorityBuffer.push(createExperience(1.0, 10.0));
|
|
}
|
|
|
|
// Sample multiple times and count high priority
|
|
let highPriorityCount = 0;
|
|
for (let trial = 0; trial < 100; trial++) {
|
|
const samples = priorityBuffer.sample(10);
|
|
highPriorityCount += samples.filter((exp) => exp.reward === 1.0).length;
|
|
}
|
|
|
|
// High priority should appear more often than uniform would suggest
|
|
// With alpha=1.0, they should appear much more frequently
|
|
const avgHighPriority = highPriorityCount / 100;
|
|
expect(avgHighPriority).toBeGreaterThan(1); // > 10% uniform rate
|
|
});
|
|
});
|
|
|
|
describe('sampleWithWeights', () => {
|
|
beforeEach(() => {
|
|
for (let i = 0; i < 50; i++) {
|
|
buffer.push(createExperience(Math.random(), Math.random() + 0.1));
|
|
}
|
|
});
|
|
|
|
it('should return experiences with weights and indices', () => {
|
|
const result = buffer.sampleWithWeights(10);
|
|
|
|
expect(result.experiences.length).toBe(10);
|
|
expect(result.weights.length).toBe(10);
|
|
expect(result.indices.length).toBe(10);
|
|
});
|
|
|
|
it('should return positive weights', () => {
|
|
const { weights } = buffer.sampleWithWeights(10);
|
|
|
|
for (const weight of weights) {
|
|
expect(weight).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should return weights <= 1.0 (normalized)', () => {
|
|
const { weights } = buffer.sampleWithWeights(10);
|
|
|
|
for (const weight of weights) {
|
|
expect(weight).toBeLessThanOrEqual(1.01); // Small tolerance
|
|
}
|
|
});
|
|
|
|
it('should return valid indices', () => {
|
|
const { indices } = buffer.sampleWithWeights(10);
|
|
|
|
for (const idx of indices) {
|
|
expect(idx).toBeGreaterThanOrEqual(0);
|
|
expect(idx).toBeLessThan(buffer.length);
|
|
}
|
|
});
|
|
|
|
it('should anneal beta over time', () => {
|
|
const initialBeta = buffer.getBeta();
|
|
|
|
// Sample multiple times
|
|
for (let i = 0; i < 100; i++) {
|
|
buffer.sampleWithWeights(5);
|
|
}
|
|
|
|
expect(buffer.getBeta()).toBeGreaterThan(initialBeta);
|
|
});
|
|
});
|
|
|
|
describe('updatePriorities', () => {
|
|
it('should update priorities for sampled experiences', () => {
|
|
// Add experiences
|
|
for (let i = 0; i < 20; i++) {
|
|
buffer.push(createExperience(Math.random(), 0.1));
|
|
}
|
|
|
|
// Sample and get indices
|
|
const { indices } = buffer.sampleWithWeights(5);
|
|
|
|
// Update with high priorities
|
|
const newPriorities = Array(5).fill(100.0);
|
|
buffer.updatePriorities(indices, newPriorities);
|
|
|
|
// Stats should reflect higher priority
|
|
const stats = buffer.getStats();
|
|
expect(stats.maxPriority).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should empty the buffer', () => {
|
|
for (let i = 0; i < 20; i++) {
|
|
buffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
buffer.clear();
|
|
|
|
expect(buffer.length).toBe(0);
|
|
});
|
|
|
|
it('should reset priorities', () => {
|
|
for (let i = 0; i < 20; i++) {
|
|
buffer.push(createExperience(Math.random(), 10.0));
|
|
}
|
|
|
|
buffer.clear();
|
|
|
|
const stats = buffer.getStats();
|
|
expect(stats.totalPriority).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('getStats', () => {
|
|
it('should return valid statistics', () => {
|
|
for (let i = 0; i < 20; i++) {
|
|
buffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
const stats = buffer.getStats();
|
|
|
|
expect(stats.count).toBe(20);
|
|
expect(stats.capacity).toBe(100);
|
|
expect(stats.totalPriority).toBeGreaterThan(0);
|
|
expect(stats.maxPriority).toBeGreaterThan(0);
|
|
expect(typeof stats.beta).toBe('number');
|
|
});
|
|
});
|
|
|
|
describe('factory function', () => {
|
|
it('createPrioritizedReplayBuffer should create buffer', () => {
|
|
const buf = createPrioritizedReplayBuffer(50, { alpha: 0.7 });
|
|
|
|
expect(buf.length).toBe(0);
|
|
buf.push(createExperience(1.0));
|
|
expect(buf.length).toBe(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('UniformReplayBuffer', () => {
|
|
let buffer: UniformReplayBuffer;
|
|
|
|
beforeEach(() => {
|
|
buffer = new UniformReplayBuffer(100);
|
|
});
|
|
|
|
describe('initialization', () => {
|
|
it('should start empty', () => {
|
|
expect(buffer.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('push', () => {
|
|
it('should add experiences', () => {
|
|
buffer.push(createExperience(1.0));
|
|
|
|
expect(buffer.length).toBe(1);
|
|
});
|
|
|
|
it('should not exceed capacity', () => {
|
|
const smallBuffer = new UniformReplayBuffer(10);
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
smallBuffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
expect(smallBuffer.length).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('sample', () => {
|
|
beforeEach(() => {
|
|
for (let i = 0; i < 50; i++) {
|
|
buffer.push(createExperience(i / 50));
|
|
}
|
|
});
|
|
|
|
it('should return requested number of samples', () => {
|
|
const samples = buffer.sample(10);
|
|
|
|
expect(samples.length).toBe(10);
|
|
});
|
|
|
|
it('should return unique samples (no duplicates)', () => {
|
|
const samples = buffer.sample(10);
|
|
const rewards = samples.map((exp) => exp.reward);
|
|
const uniqueRewards = new Set(rewards);
|
|
|
|
expect(uniqueRewards.size).toBe(10);
|
|
});
|
|
|
|
it('should sample uniformly', () => {
|
|
// Fill with known values
|
|
const uniformBuffer = new UniformReplayBuffer(100);
|
|
for (let i = 0; i < 100; i++) {
|
|
uniformBuffer.push(createExperience(i < 50 ? 0 : 1, 1.0));
|
|
}
|
|
|
|
// Sample many times
|
|
let count0 = 0;
|
|
let count1 = 0;
|
|
for (let trial = 0; trial < 100; trial++) {
|
|
const samples = uniformBuffer.sample(10);
|
|
count0 += samples.filter((exp) => exp.reward === 0).length;
|
|
count1 += samples.filter((exp) => exp.reward === 1).length;
|
|
}
|
|
|
|
// Should be roughly equal (uniform sampling)
|
|
const ratio = count0 / (count0 + count1);
|
|
expect(ratio).toBeGreaterThan(0.35);
|
|
expect(ratio).toBeLessThan(0.65);
|
|
});
|
|
});
|
|
|
|
describe('updatePriorities', () => {
|
|
it('should be no-op (uniform sampling)', () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
buffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
// Should not throw
|
|
buffer.updatePriorities([0, 1, 2], [100, 100, 100]);
|
|
|
|
expect(buffer.length).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should empty the buffer', () => {
|
|
for (let i = 0; i < 20; i++) {
|
|
buffer.push(createExperience(Math.random()));
|
|
}
|
|
|
|
buffer.clear();
|
|
|
|
expect(buffer.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('factory function', () => {
|
|
it('createUniformReplayBuffer should create buffer', () => {
|
|
const buf = createUniformReplayBuffer(50);
|
|
|
|
expect(buf.length).toBe(0);
|
|
buf.push(createExperience(1.0));
|
|
expect(buf.length).toBe(1);
|
|
});
|
|
});
|
|
});
|