Files
proffesor-for-testing__agen…/tests/integration/goap-plan-learning.test.ts
Profa 1aa321691d feat(goap): Phase 5 & 6 - Plan Learning & Live Agent Execution
## 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>
2025-12-30 11:45:42 +00:00

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);
});
});
});