mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
1aa321691d
## Phase 5: Plan Learning - PlanLearning: EMA-based action success rate tracking - PlanSimilarity: Plan signature matching for reuse (<100ms) - Q-Learning integration for GOAP action selection - Database persistence for learning history ## Phase 6: Live Agent Execution - Real agent spawning via AgentRegistry (not just dry-run) - Output parsing for real-time world state updates - Plan signature storage after successful execution - Learning feedback loop integration ## New Files - src/planning/PlanLearning.ts - src/planning/PlanSimilarity.ts - tests/integration/goap-live-execution.test.ts (17 tests) - tests/integration/goap-phase5-real-integration.test.ts (15 tests) - tests/integration/goap-plan-learning.test.ts (31 tests) ## Test Coverage - 84 GOAP-related tests passing - Output parsing methods verified - Live vs dry-run code paths tested 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
794 lines
26 KiB
TypeScript
794 lines
26 KiB
TypeScript
/**
|
|
* GOAP Plan Learning Integration Tests
|
|
*
|
|
* Tests for Phase 5: Plan Learning system including:
|
|
* - PlanSimilarity: Fast plan matching (<100ms)
|
|
* - PlanLearning: Learning from executions
|
|
* - QLearning: GOAP state encoding integration
|
|
*
|
|
* @module tests/integration/goap-plan-learning
|
|
*/
|
|
|
|
import Database from 'better-sqlite3';
|
|
import { PlanSimilarity, PlanSignature, SimilarPlan } from '../../src/planning/PlanSimilarity';
|
|
import { PlanLearning, ActionStats, PlanLearningOutcome } from '../../src/planning/PlanLearning';
|
|
import { QLearning, DiscreteGOAPState } from '../../src/learning/QLearning';
|
|
import {
|
|
WorldState,
|
|
GOAPAction,
|
|
GOAPPlan,
|
|
ExecutedAction,
|
|
StateConditions,
|
|
DEFAULT_WORLD_STATE
|
|
} from '../../src/planning/types';
|
|
|
|
describe('GOAP Plan Learning (Phase 5)', () => {
|
|
let db: Database.Database;
|
|
|
|
beforeAll(() => {
|
|
// Use in-memory database for tests
|
|
db = new Database(':memory:');
|
|
|
|
// Create required GOAP tables
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS goap_plans (
|
|
id TEXT PRIMARY KEY,
|
|
goal_id TEXT,
|
|
initial_state TEXT,
|
|
goal_state TEXT,
|
|
action_sequence TEXT,
|
|
total_cost REAL,
|
|
estimated_duration INTEGER,
|
|
actual_duration INTEGER,
|
|
status TEXT DEFAULT 'pending',
|
|
success INTEGER,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS goap_actions (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
agent_type TEXT NOT NULL,
|
|
preconditions TEXT NOT NULL,
|
|
effects TEXT NOT NULL,
|
|
cost REAL NOT NULL DEFAULT 1.0,
|
|
duration_estimate INTEGER,
|
|
success_rate REAL DEFAULT 1.0,
|
|
execution_count INTEGER DEFAULT 0,
|
|
category TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
});
|
|
|
|
afterAll(() => {
|
|
db.close();
|
|
});
|
|
|
|
describe('PlanSimilarity', () => {
|
|
let similarity: PlanSimilarity;
|
|
|
|
beforeEach(() => {
|
|
similarity = new PlanSimilarity(db);
|
|
similarity.ensureSchema();
|
|
});
|
|
|
|
afterEach(() => {
|
|
similarity.clearCache();
|
|
});
|
|
|
|
describe('State Vector Extraction', () => {
|
|
it('should extract consistent feature vectors from WorldState', () => {
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
state.coverage.line = 75;
|
|
state.quality.testsPassing = 90;
|
|
state.quality.securityScore = 85;
|
|
state.fleet.availableAgents = ['test-generator', 'coverage-analyzer'];
|
|
|
|
const vector = similarity.extractStateVector(state);
|
|
|
|
// Should have 20 dimensions
|
|
expect(vector).toHaveLength(20);
|
|
|
|
// All values should be normalized to [0,1]
|
|
vector.forEach((v, i) => {
|
|
expect(v).toBeGreaterThanOrEqual(0);
|
|
expect(v).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
// Coverage should be 0.75
|
|
expect(vector[0]).toBeCloseTo(0.75, 2);
|
|
});
|
|
|
|
it('should produce same vector for same state', () => {
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
state.coverage.line = 50;
|
|
|
|
const vector1 = similarity.extractStateVector(state);
|
|
const vector2 = similarity.extractStateVector(state);
|
|
|
|
expect(vector1).toEqual(vector2);
|
|
});
|
|
});
|
|
|
|
describe('Goal Signature', () => {
|
|
it('should create deterministic hash for goal conditions', () => {
|
|
const goal: StateConditions = {
|
|
'quality.gateStatus': { eq: 'passed' },
|
|
'coverage.line': { gte: 80 }
|
|
};
|
|
|
|
const sig1 = similarity.createGoalSignature(goal);
|
|
const sig2 = similarity.createGoalSignature(goal);
|
|
|
|
expect(sig1).toEqual(sig2);
|
|
expect(sig1).toHaveLength(16);
|
|
});
|
|
|
|
it('should create different hashes for different goals', () => {
|
|
const goal1: StateConditions = { 'coverage.line': { gte: 80 } };
|
|
const goal2: StateConditions = { 'coverage.line': { gte: 90 } };
|
|
|
|
const sig1 = similarity.createGoalSignature(goal1);
|
|
const sig2 = similarity.createGoalSignature(goal2);
|
|
|
|
expect(sig1).not.toEqual(sig2);
|
|
});
|
|
|
|
it('should produce same hash regardless of key order', () => {
|
|
const goal1: StateConditions = {
|
|
'coverage.line': { gte: 80 },
|
|
'quality.testsPassing': { gte: 90 }
|
|
};
|
|
const goal2: StateConditions = {
|
|
'quality.testsPassing': { gte: 90 },
|
|
'coverage.line': { gte: 80 }
|
|
};
|
|
|
|
const sig1 = similarity.createGoalSignature(goal1);
|
|
const sig2 = similarity.createGoalSignature(goal2);
|
|
|
|
expect(sig1).toEqual(sig2);
|
|
});
|
|
});
|
|
|
|
describe('Cosine Similarity', () => {
|
|
it('should return 1 for identical vectors', () => {
|
|
const v = [0.5, 0.7, 0.3, 0.9];
|
|
expect(similarity.cosineSimilarity(v, v)).toBeCloseTo(1, 5);
|
|
});
|
|
|
|
it('should return 0 for orthogonal vectors', () => {
|
|
const v1 = [1, 0, 0, 0];
|
|
const v2 = [0, 1, 0, 0];
|
|
expect(similarity.cosineSimilarity(v1, v2)).toBeCloseTo(0, 5);
|
|
});
|
|
|
|
it('should return high similarity for similar vectors', () => {
|
|
const v1 = [0.5, 0.6, 0.7, 0.8];
|
|
const v2 = [0.51, 0.59, 0.71, 0.79];
|
|
expect(similarity.cosineSimilarity(v1, v2)).toBeGreaterThan(0.99);
|
|
});
|
|
});
|
|
|
|
describe('Plan Signature Storage and Retrieval', () => {
|
|
it('should store and retrieve plan signatures', () => {
|
|
const goalConditions: StateConditions = {
|
|
'quality.gateStatus': { eq: 'passed' }
|
|
};
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
const actions: GOAPAction[] = [
|
|
{
|
|
id: 'action-1',
|
|
name: 'Run Tests',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 1.0,
|
|
category: 'test'
|
|
}
|
|
];
|
|
|
|
const signature = similarity.storePlanSignature(
|
|
'plan-1',
|
|
goalConditions,
|
|
state,
|
|
actions,
|
|
1.0
|
|
);
|
|
|
|
expect(signature.planId).toBe('plan-1');
|
|
expect(signature.actionSequence).toEqual(['action-1']);
|
|
expect(signature.totalCost).toBe(1.0);
|
|
});
|
|
|
|
it('should find similar plans by goal match', async () => {
|
|
const goal: StateConditions = { 'coverage.line': { gte: 80 } };
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
|
|
// Store a plan
|
|
similarity.storePlanSignature('plan-existing', goal, state, [], 1.0);
|
|
|
|
// Search for similar plans
|
|
const results = await similarity.findSimilarPlans(goal, state);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].goalMatch).toBe(true);
|
|
expect(results[0].planId).toBe('plan-existing');
|
|
});
|
|
|
|
it('should find similar plans within 100ms performance target', async () => {
|
|
// Store multiple plans
|
|
for (let i = 0; i < 50; i++) {
|
|
const goal: StateConditions = { 'coverage.line': { gte: 60 + (i % 40) } };
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
state.coverage.line = 60 + (i % 40);
|
|
similarity.storePlanSignature(`plan-${i}`, goal, state, [], 1.0 + i * 0.1);
|
|
}
|
|
|
|
const searchGoal: StateConditions = { 'coverage.line': { gte: 80 } };
|
|
const searchState: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
searchState.coverage.line = 78;
|
|
|
|
const startTime = Date.now();
|
|
const results = await similarity.findSimilarPlans(searchGoal, searchState);
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
expect(elapsed).toBeLessThan(100); // Performance target: <100ms
|
|
expect(results.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Plan Reuse Statistics', () => {
|
|
it('should track plan reuse correctly', () => {
|
|
const goal: StateConditions = { 'coverage.line': { gte: 80 } };
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
|
|
similarity.storePlanSignature('plan-tracked', goal, state, [], 1.0);
|
|
|
|
// Record some reuses
|
|
similarity.recordPlanReuse('plan-tracked', true);
|
|
similarity.recordPlanReuse('plan-tracked', true);
|
|
similarity.recordPlanReuse('plan-tracked', false);
|
|
|
|
const stats = similarity.getReuseStats();
|
|
|
|
expect(stats.totalPlans).toBeGreaterThan(0);
|
|
expect(stats.reusedPlans).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('PlanLearning', () => {
|
|
let learning: PlanLearning;
|
|
|
|
beforeEach(() => {
|
|
learning = new PlanLearning(db, {
|
|
learningRate: 0.1,
|
|
enableQLearning: true
|
|
});
|
|
learning.ensureSchema();
|
|
});
|
|
|
|
afterEach(() => {
|
|
learning.clearCaches();
|
|
});
|
|
|
|
describe('GOAP State Encoding', () => {
|
|
it('should encode WorldState to discrete GOAP state', () => {
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
state.coverage.line = 75;
|
|
state.quality.testsPassing = 85;
|
|
state.quality.securityScore = 90;
|
|
|
|
const encoded = learning.encodeWorldState(state);
|
|
|
|
expect(encoded.coverageLevel).toBe('medium');
|
|
expect(encoded.qualityLevel).toBe('medium');
|
|
expect(encoded.securityLevel).toBe('high');
|
|
});
|
|
|
|
it('should encode GOAP action correctly', () => {
|
|
const action: GOAPAction = {
|
|
id: 'action-test',
|
|
name: 'Test Action',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 2.5,
|
|
category: 'test'
|
|
};
|
|
|
|
const encoded = learning.encodeGOAPAction(action);
|
|
|
|
expect(encoded.category).toBe('test');
|
|
expect(encoded.agentType).toBe('test-executor');
|
|
expect(encoded.costLevel).toBe('medium');
|
|
});
|
|
});
|
|
|
|
describe('Reward Calculation', () => {
|
|
it('should calculate positive reward for successful execution', () => {
|
|
const action: GOAPAction = {
|
|
id: 'action-1',
|
|
name: 'Run Tests',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 1.0,
|
|
durationEstimate: 60000,
|
|
category: 'test'
|
|
};
|
|
|
|
const stateBefore: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
const stateAfter: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
stateAfter.coverage.line = 10; // Improvement
|
|
|
|
const executed: ExecutedAction = {
|
|
action,
|
|
success: true,
|
|
stateBefore,
|
|
stateAfter,
|
|
executionTimeMs: 50000 // Faster than expected
|
|
};
|
|
|
|
const reward = learning.calculateReward(executed);
|
|
|
|
expect(reward).toBeGreaterThan(0);
|
|
expect(reward).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
it('should calculate negative reward for failed execution', () => {
|
|
const action: GOAPAction = {
|
|
id: 'action-1',
|
|
name: 'Run Tests',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 1.0,
|
|
durationEstimate: 60000,
|
|
category: 'test'
|
|
};
|
|
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
|
|
const executed: ExecutedAction = {
|
|
action,
|
|
success: false,
|
|
stateBefore: state,
|
|
stateAfter: state,
|
|
executionTimeMs: 100000
|
|
};
|
|
|
|
const reward = learning.calculateReward(executed);
|
|
|
|
expect(reward).toBeLessThan(0);
|
|
expect(reward).toBeGreaterThanOrEqual(-1);
|
|
});
|
|
});
|
|
|
|
describe('Learning from Execution', () => {
|
|
it('should learn from successful plan execution', async () => {
|
|
const action: GOAPAction = {
|
|
id: 'action-learn-1',
|
|
name: 'Test Action',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 1.0,
|
|
durationEstimate: 60000,
|
|
category: 'test'
|
|
};
|
|
|
|
const plan: GOAPPlan = {
|
|
id: 'plan-learn-1',
|
|
actions: [action],
|
|
totalCost: 1.0,
|
|
estimatedDuration: 60000,
|
|
goalConditions: { 'coverage.line': { gte: 80 } },
|
|
initialState: { ...DEFAULT_WORLD_STATE }
|
|
};
|
|
|
|
const stateBefore: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
const stateAfter: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
stateAfter.coverage.line = 85;
|
|
|
|
const executedActions: ExecutedAction[] = [{
|
|
action,
|
|
success: true,
|
|
stateBefore,
|
|
stateAfter,
|
|
executionTimeMs: 55000
|
|
}];
|
|
|
|
const outcome = await learning.learnFromExecution(plan, executedActions, true);
|
|
|
|
expect(outcome.success).toBe(true);
|
|
expect(outcome.actionsUpdated).toBe(1);
|
|
expect(outcome.qValueUpdates).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should update action statistics after execution', async () => {
|
|
const action: GOAPAction = {
|
|
id: 'action-stats-1',
|
|
name: 'Test Action',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 1.0,
|
|
durationEstimate: 60000,
|
|
category: 'test'
|
|
};
|
|
|
|
const plan: GOAPPlan = {
|
|
id: 'plan-stats-1',
|
|
actions: [action],
|
|
totalCost: 1.0,
|
|
estimatedDuration: 60000,
|
|
goalConditions: {},
|
|
initialState: { ...DEFAULT_WORLD_STATE }
|
|
};
|
|
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
const executedActions: ExecutedAction[] = [{
|
|
action,
|
|
success: true,
|
|
stateBefore: state,
|
|
stateAfter: state,
|
|
executionTimeMs: 50000
|
|
}];
|
|
|
|
await learning.learnFromExecution(plan, executedActions, true);
|
|
|
|
const stats = learning.getActionStats('action-stats-1');
|
|
|
|
expect(stats).not.toBeNull();
|
|
expect(stats!.executionCount).toBeGreaterThan(0);
|
|
expect(stats!.successCount).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Plan Reuse', () => {
|
|
it('should find reusable plans for similar goals', async () => {
|
|
const goal: StateConditions = { 'coverage.line': { gte: 80 } };
|
|
const state: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
state.coverage.line = 70;
|
|
|
|
const action: GOAPAction = {
|
|
id: 'action-reuse-1',
|
|
name: 'Generate Tests',
|
|
agentType: 'test-generator',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 2.0,
|
|
category: 'test'
|
|
};
|
|
|
|
// Store a successful plan
|
|
const plan: GOAPPlan = {
|
|
id: 'plan-reuse-1',
|
|
actions: [action],
|
|
totalCost: 2.0,
|
|
estimatedDuration: 120000,
|
|
goalConditions: goal,
|
|
initialState: state
|
|
};
|
|
|
|
const executedActions: ExecutedAction[] = [{
|
|
action,
|
|
success: true,
|
|
stateBefore: state,
|
|
stateAfter: { ...state, coverage: { ...state.coverage, line: 85 } },
|
|
executionTimeMs: 100000
|
|
}];
|
|
|
|
await learning.learnFromExecution(plan, executedActions, true);
|
|
|
|
// Try to find reusable plan
|
|
const similarState: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
similarState.coverage.line = 72;
|
|
|
|
const reusable = await learning.findReusablePlan(goal, similarState);
|
|
|
|
expect(reusable).not.toBeNull();
|
|
expect(reusable!.planId).toBe('plan-reuse-1');
|
|
});
|
|
});
|
|
|
|
describe('Learning Metrics', () => {
|
|
it('should provide comprehensive learning metrics', () => {
|
|
const metrics = learning.getLearningMetrics();
|
|
|
|
expect(metrics).toHaveProperty('actionStats');
|
|
expect(metrics).toHaveProperty('planReuse');
|
|
expect(metrics).toHaveProperty('qLearning');
|
|
expect(metrics).toHaveProperty('learningHistory');
|
|
|
|
expect(typeof metrics.actionStats.total).toBe('number');
|
|
expect(typeof metrics.planReuse.reuseRate).toBe('number');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('QLearning GOAP Integration', () => {
|
|
describe('Static GOAP Methods', () => {
|
|
it('should discretize WorldState correctly', () => {
|
|
const state = {
|
|
coverage: { line: 75, branch: 60, function: 80 },
|
|
quality: { testsPassing: 85, securityScore: 70, performanceScore: 90 },
|
|
fleet: { activeAgents: 3, availableAgents: ['a', 'b', 'c', 'd', 'e'] },
|
|
resources: { timeRemaining: 1200, memoryAvailable: 4096, parallelSlots: 4 },
|
|
context: { environment: 'staging', changeSize: 'medium', riskLevel: 'medium' }
|
|
};
|
|
|
|
const discrete = QLearning.discretizeWorldState(state);
|
|
|
|
expect(discrete.coverageLevel).toBe('medium');
|
|
expect(discrete.qualityLevel).toBe('medium');
|
|
expect(discrete.securityLevel).toBe('medium');
|
|
expect(discrete.fleetCapacity).toBe('normal');
|
|
expect(discrete.timeConstraint).toBe('normal');
|
|
expect(discrete.riskLevel).toBe('medium');
|
|
});
|
|
|
|
it('should encode GOAP state to string key', () => {
|
|
const discrete: DiscreteGOAPState = {
|
|
coverageLevel: 'high',
|
|
qualityLevel: 'high',
|
|
securityLevel: 'medium',
|
|
fleetCapacity: 'normal',
|
|
timeConstraint: 'relaxed',
|
|
riskLevel: 'low'
|
|
};
|
|
|
|
const key = QLearning.encodeGOAPState(discrete);
|
|
|
|
expect(key).toBe('high:high:medium:normal:relaxed:low');
|
|
});
|
|
|
|
it('should encode GOAP action to string key', () => {
|
|
const action = {
|
|
id: 'test-action',
|
|
category: 'test',
|
|
agentType: 'test-executor',
|
|
cost: 2.5
|
|
};
|
|
|
|
const key = QLearning.encodeGOAPAction(action);
|
|
|
|
expect(key).toBe('test:test-executor:M');
|
|
});
|
|
|
|
it('should convert WorldState to TaskState', () => {
|
|
const state = {
|
|
coverage: { line: 85, branch: 80, function: 90 },
|
|
quality: { testsPassing: 95, securityScore: 88, performanceScore: 92 },
|
|
fleet: { activeAgents: 5, availableAgents: ['a', 'b', 'c', 'd', 'e', 'f'] },
|
|
resources: { timeRemaining: 2000, memoryAvailable: 8192, parallelSlots: 8 },
|
|
context: { environment: 'production', changeSize: 'small', riskLevel: 'low' }
|
|
};
|
|
|
|
const taskState = QLearning.worldStateToTaskState(state);
|
|
|
|
expect(taskState.taskComplexity).toBe(0.3); // low risk
|
|
expect(taskState.requiredCapabilities).toContain('high');
|
|
expect(taskState.availableResources).toBe(0.6); // normal fleet
|
|
expect(taskState.timeConstraint).toBe(2000000); // Converted to ms
|
|
});
|
|
|
|
it('should convert GOAP action to AgentAction', () => {
|
|
const goapAction = {
|
|
id: 'test-action',
|
|
category: 'security',
|
|
agentType: 'security-scanner',
|
|
cost: 4.0 // high cost
|
|
};
|
|
|
|
const agentAction = QLearning.goapActionToAgentAction(goapAction);
|
|
|
|
expect(agentAction.strategy).toBe('security');
|
|
expect(agentAction.toolsUsed).toContain('security-scanner');
|
|
expect(agentAction.parallelization).toBe(0.3); // High cost = low parallelization
|
|
expect(agentAction.resourceAllocation).toBe(0.8); // High cost = high resources
|
|
});
|
|
|
|
it('should calculate GOAP reward correctly', () => {
|
|
// Successful, fast, low cost
|
|
const reward1 = QLearning.calculateGOAPReward(
|
|
true, 50000, 60000, 1.0, 10, 5
|
|
);
|
|
expect(reward1).toBeGreaterThan(0.5);
|
|
|
|
// Failed execution
|
|
const reward2 = QLearning.calculateGOAPReward(
|
|
false, 100000, 60000, 2.0, 0, 0
|
|
);
|
|
expect(reward2).toBeLessThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Factory Method', () => {
|
|
it('should create GOAP-configured Q-Learner', () => {
|
|
const qLearner = QLearning.createForGOAP('test-goap');
|
|
|
|
expect(qLearner).toBeInstanceOf(QLearning);
|
|
expect(qLearner.getAlgorithmName()).toBe('Q-Learning');
|
|
});
|
|
});
|
|
|
|
describe('Instance GOAP Methods', () => {
|
|
let qLearner: QLearning;
|
|
|
|
beforeEach(() => {
|
|
qLearner = QLearning.createForGOAP('test-goap-instance');
|
|
});
|
|
|
|
it('should get Q-value for GOAP state-action pair', () => {
|
|
const state = {
|
|
coverage: { line: 75, branch: 60, function: 80 },
|
|
quality: { testsPassing: 85, securityScore: 70, performanceScore: 90 },
|
|
fleet: { activeAgents: 3, availableAgents: ['a', 'b'] },
|
|
resources: { timeRemaining: 1200, memoryAvailable: 4096, parallelSlots: 4 },
|
|
context: { environment: 'staging', changeSize: 'medium', riskLevel: 'medium' }
|
|
};
|
|
|
|
const action = {
|
|
id: 'test-action',
|
|
category: 'test',
|
|
agentType: 'test-executor',
|
|
cost: 1.5
|
|
};
|
|
|
|
// Initially Q-value should be 0 (no learning yet)
|
|
const qValue = qLearner.getGOAPQValue(state, action);
|
|
expect(qValue).toBe(0);
|
|
});
|
|
|
|
it('should get best GOAP action from available actions', () => {
|
|
const state = {
|
|
coverage: { line: 75, branch: 60, function: 80 },
|
|
quality: { testsPassing: 85, securityScore: 70, performanceScore: 90 },
|
|
fleet: { activeAgents: 3, availableAgents: ['a', 'b'] },
|
|
resources: { timeRemaining: 1200, memoryAvailable: 4096, parallelSlots: 4 },
|
|
context: { environment: 'staging', changeSize: 'medium', riskLevel: 'medium' }
|
|
};
|
|
|
|
const actions = [
|
|
{ id: 'action-1', category: 'test', agentType: 'test-executor', cost: 1.0 },
|
|
{ id: 'action-2', category: 'coverage', agentType: 'coverage-analyzer', cost: 2.0 },
|
|
{ id: 'action-3', category: 'security', agentType: 'security-scanner', cost: 3.0 }
|
|
];
|
|
|
|
// Without learning, should return first or random action
|
|
const bestAction = qLearner.getBestGOAPAction(state, actions);
|
|
|
|
expect(bestAction).toBeDefined();
|
|
expect(actions).toContainEqual(bestAction);
|
|
});
|
|
|
|
it('should return null for empty action list', () => {
|
|
const state = {
|
|
coverage: { line: 75, branch: 60, function: 80 },
|
|
quality: { testsPassing: 85, securityScore: 70, performanceScore: 90 },
|
|
fleet: { activeAgents: 3, availableAgents: [] },
|
|
resources: { timeRemaining: 1200, memoryAvailable: 4096, parallelSlots: 4 },
|
|
context: { environment: 'staging', changeSize: 'medium', riskLevel: 'medium' }
|
|
};
|
|
|
|
const bestAction = qLearner.getBestGOAPAction(state, []);
|
|
expect(bestAction).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('End-to-End Plan Learning Flow', () => {
|
|
it('should complete full learning cycle: plan -> execute -> learn -> reuse', async () => {
|
|
const learning = new PlanLearning(db);
|
|
const similarity = learning.getSimilarity();
|
|
|
|
// Step 1: Create a goal and initial state
|
|
const goal: StateConditions = {
|
|
'coverage.line': { gte: 80 },
|
|
'quality.testsPassing': { gte: 90 }
|
|
};
|
|
|
|
const initialState: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
initialState.coverage.line = 60;
|
|
initialState.quality.testsPassing = 70;
|
|
|
|
// Step 2: Create actions and plan
|
|
const actions: GOAPAction[] = [
|
|
{
|
|
id: 'action-e2e-1',
|
|
name: 'Generate Missing Tests',
|
|
agentType: 'test-generator',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 2.0,
|
|
durationEstimate: 120000,
|
|
category: 'test'
|
|
},
|
|
{
|
|
id: 'action-e2e-2',
|
|
name: 'Run Unit Tests',
|
|
agentType: 'test-executor',
|
|
preconditions: {},
|
|
effects: {},
|
|
cost: 1.0,
|
|
durationEstimate: 60000,
|
|
category: 'test'
|
|
}
|
|
];
|
|
|
|
const plan: GOAPPlan = {
|
|
id: 'plan-e2e-1',
|
|
actions,
|
|
totalCost: 3.0,
|
|
estimatedDuration: 180000,
|
|
goalConditions: goal,
|
|
initialState
|
|
};
|
|
|
|
// Step 3: Simulate execution
|
|
const midState: WorldState = { ...initialState };
|
|
midState.coverage.line = 82;
|
|
|
|
const finalState: WorldState = { ...midState };
|
|
finalState.quality.testsPassing = 95;
|
|
|
|
const executedActions: ExecutedAction[] = [
|
|
{
|
|
action: actions[0],
|
|
success: true,
|
|
stateBefore: initialState,
|
|
stateAfter: midState,
|
|
executionTimeMs: 100000
|
|
},
|
|
{
|
|
action: actions[1],
|
|
success: true,
|
|
stateBefore: midState,
|
|
stateAfter: finalState,
|
|
executionTimeMs: 55000
|
|
}
|
|
];
|
|
|
|
// Step 4: Learn from execution
|
|
const outcome = await learning.learnFromExecution(plan, executedActions, true);
|
|
|
|
expect(outcome.success).toBe(true);
|
|
expect(outcome.actionsUpdated).toBe(2);
|
|
|
|
// Step 5: Verify plan was stored for reuse
|
|
const stats = similarity.getReuseStats();
|
|
expect(stats.totalPlans).toBeGreaterThan(0);
|
|
|
|
// Step 6: Try to reuse the plan for similar goal
|
|
const similarState: WorldState = { ...DEFAULT_WORLD_STATE };
|
|
similarState.coverage.line = 62;
|
|
similarState.quality.testsPassing = 72;
|
|
|
|
const reusable = await learning.findReusablePlan(goal, similarState);
|
|
|
|
expect(reusable).not.toBeNull();
|
|
expect(reusable!.planId).toBe('plan-e2e-1');
|
|
|
|
// Step 7: Verify action stats were updated
|
|
const actionStats = learning.getAllActionStats();
|
|
expect(actionStats.length).toBeGreaterThan(0);
|
|
expect(actionStats.some(s => s.executionCount > 0)).toBe(true);
|
|
|
|
// Step 8: Record reuse and verify metrics
|
|
learning.recordPlanReuse('plan-e2e-1', true);
|
|
const metrics = learning.getLearningMetrics();
|
|
|
|
expect(metrics.actionStats.total).toBeGreaterThan(0);
|
|
expect(metrics.planReuse.totalPlans).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|