mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Implements the Execution Optimizer Rework plan that enables the system to actually use the optimizer metadata (parallelGroups, agentType, recommendedModel, requiresFreshContext, estimatedTokens) that was previously generated but ignored. New components: - ExecutionBridge: Central coordinator that loads optimized plans, manages parallel execution within groups, and coordinates with SpawnOrchestrator for session-based execution - ModelSelector: Routes tasks to appropriate models (opus/sonnet/haiku) based on user defaults and agent type overrides. Optimizer recommendations are advisory only - user preferences always win - GroupScheduler: Builds topologically ordered execution groups, manages dependencies, determines execution mode (session vs task-tool) - ContextManager: Handles fresh context requirements via /clear+/init or new session spawning Features: - Parallel task execution within groups (configurable limit) - Group-level dependency tracking (lower groups complete first) - Partial failure handling (continue with non-dependent tasks) - Model configuration in App Settings > Models tab - Agent type overrides (explore, implement, test, review) - Execution control API endpoints (start, pause, resume, cancel) - SSE events for real-time execution progress visibility - Execution history tracking API endpoints: - GET/POST/PUT /api/execution/* for execution control - GET/PUT /api/execution/model-config for model settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
271 lines
7.6 KiB
TypeScript
271 lines
7.6 KiB
TypeScript
/**
|
|
* @fileoverview Tests for the Execution Bridge system.
|
|
*
|
|
* Tests the core functionality of:
|
|
* - ModelSelector
|
|
* - GroupScheduler
|
|
* - ExecutionBridge
|
|
*
|
|
* Uses mocks to avoid spawning real Claude sessions.
|
|
*
|
|
* Port: none (unit tests, no server)
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
ModelSelector,
|
|
resetModelSelector,
|
|
createDefaultModelConfig,
|
|
type ModelConfig,
|
|
type AgentType,
|
|
} from '../src/model-selector.js';
|
|
import {
|
|
GroupScheduler,
|
|
resetGroupScheduler,
|
|
type GroupTask,
|
|
} from '../src/group-scheduler.js';
|
|
import {
|
|
ExecutionBridge,
|
|
resetExecutionBridge,
|
|
type PlanItem,
|
|
} from '../src/execution-bridge.js';
|
|
|
|
describe('ModelSelector', () => {
|
|
let selector: ModelSelector;
|
|
|
|
beforeEach(() => {
|
|
resetModelSelector();
|
|
selector = new ModelSelector();
|
|
});
|
|
|
|
afterEach(() => {
|
|
resetModelSelector();
|
|
});
|
|
|
|
it('should use default model when no overrides', () => {
|
|
const selection = selector.selectModel('task-1', { agentType: 'explore' });
|
|
expect(selection.model).toBe('sonnet');
|
|
expect(selection.usedUserDefault).toBe(true);
|
|
});
|
|
|
|
it('should respect user default model', () => {
|
|
selector.updateConfig({ defaultModel: 'opus' });
|
|
const selection = selector.selectModel('task-1', { agentType: 'explore' });
|
|
expect(selection.model).toBe('opus');
|
|
expect(selection.usedUserDefault).toBe(true);
|
|
});
|
|
|
|
it('should use agent type override when set', () => {
|
|
selector.updateConfig({
|
|
agentTypeOverrides: { explore: 'haiku' },
|
|
});
|
|
const selection = selector.selectModel('task-1', { agentType: 'explore' });
|
|
expect(selection.model).toBe('haiku');
|
|
expect(selection.usedUserDefault).toBe(false);
|
|
});
|
|
|
|
it('should include optimizer recommendation when different', () => {
|
|
selector.updateConfig({ defaultModel: 'opus' });
|
|
const selection = selector.selectModel('task-1', {
|
|
agentType: 'explore',
|
|
recommendedModel: 'haiku',
|
|
});
|
|
expect(selection.model).toBe('opus');
|
|
expect(selection.optimizerRecommendation).toBe('haiku');
|
|
});
|
|
|
|
it('should select session mode for high token tasks', () => {
|
|
const mode = selector.selectExecutionMode({ estimatedTokens: 60000 });
|
|
expect(mode.mode).toBe('session');
|
|
});
|
|
|
|
it('should select task-tool mode for low token explore tasks', () => {
|
|
const mode = selector.selectExecutionMode({
|
|
estimatedTokens: 10000,
|
|
agentType: 'explore',
|
|
});
|
|
expect(mode.mode).toBe('task-tool');
|
|
});
|
|
|
|
it('should return correct cost multipliers', () => {
|
|
expect(selector.getModelCostMultiplier('opus')).toBe(5.0);
|
|
expect(selector.getModelCostMultiplier('sonnet')).toBe(1.0);
|
|
expect(selector.getModelCostMultiplier('haiku')).toBe(0.04);
|
|
});
|
|
});
|
|
|
|
describe('GroupScheduler', () => {
|
|
let scheduler: GroupScheduler;
|
|
|
|
beforeEach(() => {
|
|
resetGroupScheduler();
|
|
scheduler = new GroupScheduler();
|
|
});
|
|
|
|
afterEach(() => {
|
|
resetGroupScheduler();
|
|
});
|
|
|
|
it('should build schedule from plan items', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
|
|
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 0 },
|
|
{ id: 't3', title: 'Task 3', description: 'Desc 3', parallelGroup: 1 },
|
|
];
|
|
|
|
const schedule = scheduler.buildSchedule(items);
|
|
|
|
expect(schedule.groups).toHaveLength(2);
|
|
expect(schedule.groups[0].tasks).toHaveLength(2);
|
|
expect(schedule.groups[1].tasks).toHaveLength(1);
|
|
expect(schedule.totalTasks).toBe(3);
|
|
});
|
|
|
|
it('should order groups by number', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 2 },
|
|
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 0 },
|
|
{ id: 't3', title: 'Task 3', description: 'Desc 3', parallelGroup: 1 },
|
|
];
|
|
|
|
const schedule = scheduler.buildSchedule(items);
|
|
|
|
expect(schedule.groups[0].groupNumber).toBe(0);
|
|
expect(schedule.groups[1].groupNumber).toBe(1);
|
|
expect(schedule.groups[2].groupNumber).toBe(2);
|
|
});
|
|
|
|
it('should track group dependencies', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
|
|
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 1, dependencies: ['t1'] },
|
|
];
|
|
|
|
const schedule = scheduler.buildSchedule(items);
|
|
|
|
expect(schedule.groups[1].dependsOnGroups).toContain(0);
|
|
});
|
|
|
|
it('should return first group as ready initially', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
|
|
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 1 },
|
|
];
|
|
|
|
scheduler.buildSchedule(items);
|
|
const nextGroup = scheduler.getNextReadyGroup();
|
|
|
|
expect(nextGroup).not.toBeNull();
|
|
expect(nextGroup!.groupNumber).toBe(0);
|
|
});
|
|
|
|
it('should update task status', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
|
|
];
|
|
|
|
scheduler.buildSchedule(items);
|
|
scheduler.startGroup(0);
|
|
scheduler.updateTaskStatus('t1', 'completed');
|
|
|
|
const stats = scheduler.getStats();
|
|
expect(stats.completedTasks).toBe(1);
|
|
});
|
|
|
|
it('should determine execution mode based on task characteristics', () => {
|
|
const items: PlanItem[] = [
|
|
{
|
|
id: 't1',
|
|
title: 'High token task',
|
|
description: 'Desc',
|
|
parallelGroup: 0,
|
|
estimatedTokens: 60000,
|
|
},
|
|
];
|
|
|
|
const schedule = scheduler.buildSchedule(items);
|
|
expect(schedule.groups[0].executionMode).toBe('session');
|
|
});
|
|
});
|
|
|
|
describe('ExecutionBridge', () => {
|
|
let bridge: ExecutionBridge;
|
|
|
|
beforeEach(() => {
|
|
resetExecutionBridge();
|
|
resetGroupScheduler();
|
|
resetModelSelector();
|
|
bridge = new ExecutionBridge();
|
|
});
|
|
|
|
afterEach(() => {
|
|
bridge.reset();
|
|
resetExecutionBridge();
|
|
resetGroupScheduler();
|
|
resetModelSelector();
|
|
});
|
|
|
|
it('should have idle status initially', () => {
|
|
expect(bridge.status).toBe('idle');
|
|
});
|
|
|
|
it('should load a plan', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1', parallelGroup: 0 },
|
|
{ id: 't2', title: 'Task 2', description: 'Desc 2', parallelGroup: 1 },
|
|
];
|
|
|
|
const schedule = bridge.loadPlan(items);
|
|
|
|
expect(schedule).not.toBeNull();
|
|
expect(schedule.totalTasks).toBe(2);
|
|
expect(bridge.status).toBe('idle');
|
|
});
|
|
|
|
it('should report progress', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1' },
|
|
];
|
|
|
|
bridge.loadPlan(items);
|
|
const progress = bridge.getProgress();
|
|
|
|
expect(progress.totalTasks).toBe(1);
|
|
expect(progress.completedTasks).toBe(0);
|
|
expect(progress.status).toBe('idle');
|
|
});
|
|
|
|
it('should update model config', () => {
|
|
bridge.updateModelConfig({ defaultModel: 'opus' });
|
|
const config = bridge.getModelConfig();
|
|
expect(config.defaultModel).toBe('opus');
|
|
});
|
|
|
|
it('should reset properly', () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1' },
|
|
];
|
|
|
|
bridge.loadPlan(items);
|
|
bridge.reset();
|
|
|
|
expect(bridge.status).toBe('idle');
|
|
expect(bridge.getProgress().totalTasks).toBe(0);
|
|
});
|
|
|
|
it('should throw when starting without a spawner', async () => {
|
|
const items: PlanItem[] = [
|
|
{ id: 't1', title: 'Task 1', description: 'Desc 1' },
|
|
];
|
|
|
|
bridge.loadPlan(items);
|
|
|
|
await expect(bridge.start()).rejects.toThrow('No agent spawner configured');
|
|
});
|
|
|
|
it('should track execution history', () => {
|
|
const history = bridge.getHistory();
|
|
expect(Array.isArray(history)).toBe(true);
|
|
});
|
|
});
|