Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

392 lines
13 KiB
TypeScript

/**
* Unit tests for PlanExecutor
*
* Verifies:
* - Step-by-step execution
* - Retry logic
* - Replanning on failure
* - State tracking
* - Cancellation
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
PlanExecutor,
MockAgentSpawner,
createMockExecutor,
AgentSpawner,
AgentSpawnResult,
ExecutionConfig,
} from '../../../src/planning/plan-executor.js';
import { GOAPPlanner } from '../../../src/planning/goap-planner.js';
import { GOAPPlan, GOAPAction, V3WorldState, DEFAULT_V3_WORLD_STATE } from '../../../src/planning/types.js';
describe('PlanExecutor', () => {
let planner: GOAPPlanner;
let executor: PlanExecutor;
let mockSpawner: MockAgentSpawner;
beforeEach(async () => {
planner = new GOAPPlanner();
await planner.initialize();
mockSpawner = new MockAgentSpawner({ successRate: 1.0, executionDelay: 10 });
executor = new PlanExecutor(planner, mockSpawner);
await executor.initialize();
});
afterEach(async () => {
await executor.close();
await planner.close();
});
// Helper to create test actions
function createTestAction(
id: string,
name: string,
effects: Record<string, unknown> = {}
): GOAPAction {
return {
id,
name,
agentType: 'test-agent',
preconditions: {},
effects: effects as GOAPAction['effects'],
cost: 1.0,
successRate: 1.0,
executionCount: 0,
category: 'test',
};
}
// Helper to create test plan with unique ID to avoid history conflicts
function createTestPlan(actions: GOAPAction[], planId?: string): GOAPPlan {
return {
id: planId ?? `test-plan-${Date.now()}-${Math.random().toString(36).slice(2)}`,
initialState: { ...DEFAULT_V3_WORLD_STATE },
goalState: { 'coverage.line': { min: 80 } },
actions,
totalCost: actions.reduce((sum, a) => sum + a.cost, 0),
estimatedDurationMs: 1000,
status: 'pending',
};
}
describe('Basic Execution', () => {
it('should execute a simple plan with one action', async () => {
const action = createTestAction('action-1', 'Test Action', {
'coverage.line': { delta: 20 },
});
const plan = createTestPlan([action]);
const result = await executor.execute(plan);
expect(result.status).toBe('completed');
expect(result.stepsCompleted).toBe(1);
expect(result.stepsFailed).toBe(0);
expect(result.steps).toHaveLength(1);
expect(result.steps[0].status).toBe('completed');
});
it('should execute a plan with multiple actions in sequence', async () => {
const actions = [
createTestAction('action-1', 'First Action', { 'coverage.line': { delta: 10 } }),
createTestAction('action-2', 'Second Action', { 'coverage.branch': { delta: 15 } }),
createTestAction('action-3', 'Third Action', { 'coverage.function': { delta: 20 } }),
];
const plan = createTestPlan(actions);
const result = await executor.execute(plan);
expect(result.status).toBe('completed');
expect(result.stepsCompleted).toBe(3);
expect(result.stepsFailed).toBe(0);
expect(result.steps).toHaveLength(3);
result.steps.forEach((step, i) => {
expect(step.status).toBe('completed');
expect(step.stepOrder).toBe(i);
});
});
it('should track execution duration', async () => {
const action = createTestAction('action-1', 'Test Action');
const plan = createTestPlan([action]);
const result = await executor.execute(plan);
expect(result.totalDurationMs).toBeGreaterThan(0);
expect(result.steps[0].durationMs).toBeGreaterThan(0);
});
});
describe('State Tracking', () => {
it('should record world state before and after each step', async () => {
const action = createTestAction('action-1', 'Coverage Action', {
'coverage.line': { delta: 25 },
});
const plan = createTestPlan([action]);
const initialState: V3WorldState = {
...DEFAULT_V3_WORLD_STATE,
coverage: { ...DEFAULT_V3_WORLD_STATE.coverage, line: 50 },
};
const result = await executor.execute(plan, initialState);
expect(result.steps[0].worldStateBefore).toBeDefined();
expect(result.steps[0].worldStateBefore?.coverage.line).toBe(50);
expect(result.steps[0].worldStateAfter).toBeDefined();
expect(result.steps[0].worldStateAfter?.coverage.line).toBe(75);
});
it('should update final world state correctly', async () => {
const actions = [
createTestAction('action-1', 'First', { 'coverage.line': { delta: 20 } }),
createTestAction('action-2', 'Second', { 'coverage.branch': { delta: 15 } }),
];
const plan = createTestPlan(actions);
const initialState: V3WorldState = {
...DEFAULT_V3_WORLD_STATE,
coverage: { ...DEFAULT_V3_WORLD_STATE.coverage, line: 30, branch: 20 },
};
const result = await executor.execute(plan, initialState);
expect(result.finalWorldState).toBeDefined();
expect(result.finalWorldState?.coverage.line).toBe(50);
expect(result.finalWorldState?.coverage.branch).toBe(35);
});
});
describe('Retry Logic', () => {
it('should retry failed steps up to maxRetries', async () => {
// Create spawner that fails first attempt, succeeds on 2nd
let attempts = 0;
const failingSpawner: AgentSpawner = {
spawn: async (agentType: string, task: string): Promise<AgentSpawnResult> => {
attempts++;
if (attempts < 2) {
return {
agentId: `agent-${attempts}`,
output: '',
success: false,
error: `Attempt ${attempts} failed`,
};
}
return {
agentId: `agent-${attempts}`,
output: 'Success on retry',
success: true,
};
},
};
const retryExecutor = new PlanExecutor(planner, failingSpawner, undefined, {
maxRetries: 2,
});
await retryExecutor.initialize();
const action = createTestAction('action-1', 'Retry Action');
const plan = createTestPlan([action]);
const result = await retryExecutor.execute(plan);
expect(result.status).toBe('completed');
// retries tracks the last failed attempt number (0 = first attempt failed, then success on attempt 1)
expect(result.steps[0].retries).toBe(0);
expect(attempts).toBe(2);
await retryExecutor.close();
});
it('should fail after exhausting retries', async () => {
const failingSpawner = new MockAgentSpawner({ successRate: 0.0 });
const failExecutor = new PlanExecutor(planner, failingSpawner, undefined, {
maxRetries: 2,
replanOnFailure: false,
});
await failExecutor.initialize();
const action = createTestAction('action-1', 'Always Fails');
const plan = createTestPlan([action]);
const result = await failExecutor.execute(plan);
expect(result.status).toBe('failed');
expect(result.stepsFailed).toBe(1);
expect(result.steps[0].retries).toBe(2);
await failExecutor.close();
});
});
describe('Cancellation', () => {
it('should cancel execution mid-flight', async () => {
let stepCount = 0;
const slowSpawner: AgentSpawner = {
spawn: async (): Promise<AgentSpawnResult> => {
stepCount++;
// Only slow down after first step to allow cancellation to happen
if (stepCount > 1) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
return { agentId: 'slow-agent', output: 'Done', success: true };
},
};
const slowExecutor = new PlanExecutor(planner, slowSpawner);
await slowExecutor.initialize();
const actions = [
createTestAction('action-1', 'Fast Action 1'),
createTestAction('action-2', 'Slow Action 2'),
createTestAction('action-3', 'Slow Action 3'),
];
const plan = createTestPlan(actions);
// Start execution and cancel after first step completes
const executePromise = slowExecutor.execute(plan);
// Cancel after first step completes (should be quick), before second finishes
setTimeout(() => {
slowExecutor.cancel();
}, 100);
const result = await executePromise;
// Either cancelled or completed first step
expect(['cancelled', 'completed']).toContain(result.status);
if (result.status === 'cancelled') {
expect(result.stepsCompleted).toBeLessThan(3);
}
await slowExecutor.close();
});
it('should report isExecuting status correctly', async () => {
const slowSpawner: AgentSpawner = {
spawn: async (): Promise<AgentSpawnResult> => {
await new Promise((resolve) => setTimeout(resolve, 100));
return { agentId: 'agent', output: 'Done', success: true };
},
};
const slowExecutor = new PlanExecutor(planner, slowSpawner);
await slowExecutor.initialize();
expect(slowExecutor.isExecuting()).toBe(false);
const action = createTestAction('action-1', 'Test');
const plan = createTestPlan([action]);
const executePromise = slowExecutor.execute(plan);
// Should be executing after starting
await new Promise((resolve) => setTimeout(resolve, 10));
expect(slowExecutor.isExecuting()).toBe(true);
await executePromise;
expect(slowExecutor.isExecuting()).toBe(false);
await slowExecutor.close();
});
});
describe('Callbacks', () => {
it('should call onStepStart and onStepComplete callbacks', async () => {
const startCalls: number[] = [];
const completeCalls: number[] = [];
const actions = [
createTestAction('action-1', 'First'),
createTestAction('action-2', 'Second'),
];
const plan = createTestPlan(actions);
await executor.executeWithCallbacks(
plan,
(step) => startCalls.push(step.stepOrder),
(step) => completeCalls.push(step.stepOrder),
);
expect(startCalls).toEqual([0, 1]);
expect(completeCalls).toEqual([0, 1]);
});
});
describe('Execution History', () => {
it('should persist and retrieve execution history', async () => {
const action = createTestAction('action-1', 'History Test');
const plan = createTestPlan([action]);
await executor.execute(plan);
const history = await executor.getExecutionHistory(plan.id);
expect(history).toHaveLength(1);
expect(history[0].planId).toBe(plan.id);
expect(history[0].status).toBe('completed');
expect(history[0].stepsCompleted).toBe(1);
});
it('should limit history results', async () => {
const action = createTestAction('action-1', 'Limit Test');
const plan = createTestPlan([action]);
// Execute multiple times
await executor.execute(plan);
await executor.execute(plan);
await executor.execute(plan);
const limited = await executor.getExecutionHistory(plan.id, 2);
expect(limited).toHaveLength(2);
});
});
describe('Factory Functions', () => {
it('should create mock executor via factory', async () => {
const mockExecutor = createMockExecutor(planner, { successRate: 1.0 });
await mockExecutor.initialize();
const action = createTestAction('action-1', 'Factory Test');
const plan = createTestPlan([action]);
const result = await mockExecutor.execute(plan);
expect(result.status).toBe('completed');
await mockExecutor.close();
});
});
});
describe('MockAgentSpawner', () => {
it('should succeed with 100% success rate', async () => {
const spawner = new MockAgentSpawner({ successRate: 1.0 });
const result = await spawner.spawn('test-agent', 'Test task');
expect(result.success).toBe(true);
expect(result.agentId).toBeDefined();
expect(result.output).toContain('Successfully executed');
});
it('should fail with 0% success rate', async () => {
const spawner = new MockAgentSpawner({ successRate: 0.0 });
const result = await spawner.spawn('test-agent', 'Test task');
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
it('should respect execution delay', async () => {
const spawner = new MockAgentSpawner({ executionDelay: 50 });
const start = Date.now();
await spawner.spawn('test-agent', 'Test task');
const elapsed = Date.now() - start;
// Use a slightly lower threshold (45ms) to account for timing variance in CI
expect(elapsed).toBeGreaterThanOrEqual(45);
});
});