andy-stack_vaultkeeper-ai/__tests__/Services/MultiAgentIntegration.test.ts
Andrew Beal d18f5ef655 refactor: replace Replan with granular orchestration tools
Replace the single Replan tool with three targeted alternatives:
ReviseStep, RevisePlan, and SkipStep. This gives the orchestration
agent more precise control over plan recovery — revising only the
current step, replacing all remaining steps, or skipping a step
entirely — rather than triggering a full replan each time.

Also give the orchestration agent read access to vault files so it
can resolve execution failures without routing back to the planning
agent, and update the execution agent prompt to stop it from
attempting self-recovery (gap resolution is now the orchestrator's
responsibility).
2026-02-19 20:48:59 +00:00

452 lines
12 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { MainAgent } from '../../Services/AIServices/MainAgent';
import { PlanningAgent } from '../../Services/AIServices/PlanningAgent';
import { ExecutionAgent } from '../../Services/AIServices/ExecutionAgent';
import { OrchestrationAgent } from '../../Services/AIServices/OrchestrationAgent';
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
import { Services } from '../../Services/Services';
import { Conversation } from '../../Conversations/Conversation';
import { ConversationContent } from '../../Conversations/ConversationContent';
import { Role } from '../../Enums/Role';
import { AITool } from '../../Enums/AITool';
import { AIToolCall } from '../../AIClasses/AIToolCall';
import { AIToolResponse } from '../../AIClasses/ToolDefinitions/AIToolResponse';
import type { ExecutionStep } from '../../Types/ExecutionStep';
/**
* INTEGRATION TESTS - Multi-Agent Architecture
*
* Tests the multi-agent components independently:
* - MainAgent initialization and setup
* - PlanningAgent plan creation
* - ExecutionAgent step execution
* - OrchestrationAgent setup
*
* Note: Full end-to-end workflow tests would require extensive mocking of nested
* agent instantiation, which is not practical given the current architecture.
* These tests verify that each agent can be initialized and has the correct interface.
*/
describe('Multi-Agent Integration Tests', () => {
let mockAI: any;
let mockPrompt: any;
let mockAIToolService: any;
const createMockCallbacks = () => ({
onSubmit: vi.fn(),
onStreamingUpdate: vi.fn(),
onThoughtUpdate: vi.fn(),
onToolCallStarted: vi.fn(),
onPlanningStarted: vi.fn(),
onPlanningFinished: vi.fn(),
onUserQuestion: vi.fn().mockResolvedValue('User answer'),
onPlanUpdate: vi.fn(),
onPlanStepUpdate: vi.fn(),
onPlanReset: vi.fn(),
onComplete: vi.fn(),
onCancel: vi.fn()
});
beforeEach(() => {
// Mock IPrompt
mockPrompt = {
systemInstruction: vi.fn().mockReturnValue('System instruction'),
userInstruction: vi.fn().mockResolvedValue('User instruction'),
planningInstruction: vi.fn().mockReturnValue('Planning instruction'),
orchestrationInstruction: vi.fn().mockReturnValue('Orchestration instruction'),
executionInstruction: vi.fn().mockReturnValue('Execution instruction')
};
RegisterSingleton(Services.IPrompt, mockPrompt);
// Mock AIToolService
mockAIToolService = {
performAITool: vi.fn().mockResolvedValue(
new AIToolResponse(AITool.SearchVaultFiles, { results: ['file1.md', 'file2.md'] }, 'test-tool-id')
)
};
RegisterSingleton(Services.AIToolService, mockAIToolService);
// Mock IAIClass
mockAI = {
systemPrompt: '',
userInstruction: '',
toolDefinitions: [],
agentType: undefined,
streamRequest: vi.fn()
};
RegisterSingleton(Services.IAIClass, mockAI);
});
afterEach(() => {
DeregisterAllServices();
vi.restoreAllMocks();
});
describe('MainAgent Integration', () => {
it('should initialize MainAgent with all services', () => {
const service = new MainAgent();
expect(service).toBeDefined();
expect(service.runMainAgent).toBeDefined();
});
it('should resolve AI provider', () => {
const service = new MainAgent();
service.resolveAIProvider();
expect(service).toBeDefined();
});
it('should throw error if running without resolved provider', async () => {
const service = new MainAgent();
const conversation = new Conversation();
const callbacks = createMockCallbacks();
await expect(service.runMainAgent(conversation, true, false, callbacks))
.rejects.toThrow('Error: No AI provider has been set!');
});
});
describe('PlanningAgent Integration', () => {
it('should initialize PlanningAgent', () => {
const service = new PlanningAgent();
expect(service).toBeDefined();
expect(service.runPlanningAgent).toBeDefined();
});
it('should create a simple plan', async () => {
const service = new PlanningAgent();
const conversation = new Conversation();
conversation.contents.push(new ConversationContent({
role: Role.User,
content: 'Create a plan'
}));
const callbacks = createMockCallbacks();
mockAI.streamRequest.mockImplementation(async function* () {
yield {
content: 'Plan',
toolCall: new AIToolCall(
AITool.SubmitPlan,
{
steps: [
{
description: 'Test step',
instruction: 'Test instruction'
}
]
},
'tool-1'
),
isComplete: true
};
});
service.resolveAIProvider();
const result = await service.runPlanningAgent(conversation, callbacks);
expect(result).toBeDefined();
expect(result?.executionSteps).toHaveLength(1);
});
});
describe('ExecutionAgent Integration', () => {
it('should initialize ExecutionAgent', () => {
const service = new ExecutionAgent();
expect(service).toBeDefined();
expect(service.runExecutionAgent).toBeDefined();
});
it('should execute a step successfully', async () => {
const service = new ExecutionAgent();
const step: ExecutionStep = {
description: 'Test step',
instruction: 'Do something'
};
const callbacks = createMockCallbacks();
mockAI.streamRequest.mockImplementation(async function* () {
yield {
content: 'Done',
toolCall: new AIToolCall(
AITool.CompleteTask,
{
success: true,
description: 'Task completed'
},
'tool-1'
),
isComplete: true
};
});
service.resolveAIProvider();
const result = await service.runExecutionAgent(step, callbacks);
expect(result).toBeDefined();
expect(result?.success).toBe(true);
expect(result?.description).toBe('Task completed');
});
it('should handle step failure', async () => {
const service = new ExecutionAgent();
const step: ExecutionStep = {
description: 'Test step',
instruction: 'Do something'
};
const callbacks = createMockCallbacks();
mockAI.streamRequest.mockImplementation(async function* () {
yield {
content: 'Failed',
toolCall: new AIToolCall(
AITool.CompleteTask,
{
success: false,
description: 'Task failed'
},
'tool-1'
),
isComplete: true
};
});
service.resolveAIProvider();
const result = await service.runExecutionAgent(step, callbacks);
expect(result).toBeDefined();
expect(result?.success).toBe(false);
expect(result?.description).toBe('Task failed');
});
it('should include context in execution', async () => {
const service = new ExecutionAgent();
const step: ExecutionStep = {
description: 'Test step',
instruction: 'Process data',
context: 'Previous result: 42'
};
const callbacks = createMockCallbacks();
mockAI.streamRequest.mockImplementation(async function* () {
yield {
content: 'Done',
toolCall: new AIToolCall(
AITool.CompleteTask,
{
success: true,
description: 'Processed with context',
context: 'Used previous result'
},
'tool-1'
),
isComplete: true
};
});
service.resolveAIProvider();
const result = await service.runExecutionAgent(step, callbacks);
expect(result).toBeDefined();
expect(result?.success).toBe(true);
expect(result?.context).toContain('Used previous result');
});
});
describe('OrchestrationAgent Integration', () => {
it('should initialize OrchestrationAgent', () => {
const service = new OrchestrationAgent();
expect(service).toBeDefined();
expect(service.runPlannedWorkflow).toBeDefined();
});
it('should have correct method interface', () => {
const service = new OrchestrationAgent();
expect(typeof service.runPlannedWorkflow).toBe('function');
});
});
describe('Agent Depth Limits', () => {
it('PlanningAgent should respect max depth', async () => {
const service = new PlanningAgent();
const conversation = new Conversation();
conversation.contents.push(new ConversationContent({
role: Role.User,
content: 'Create plan'
}));
const callbacks = createMockCallbacks();
let attemptCount = 0;
mockAI.streamRequest.mockImplementation(async function* () {
attemptCount++;
// Never submit plan, force retries
yield {
content: 'Still thinking...',
isComplete: true
};
});
service.resolveAIProvider();
const result = await service.runPlanningAgent(conversation, callbacks);
// Should return undefined after max depth
expect(result).toBeUndefined();
expect(attemptCount).toBe(3); // MAX_AGENT_DEPTH
});
it('ExecutionAgent should respect max depth', async () => {
const service = new ExecutionAgent();
const step: ExecutionStep = {
description: 'Test step',
instruction: 'Do something'
};
const callbacks = createMockCallbacks();
let attemptCount = 0;
mockAI.streamRequest.mockImplementation(async function* () {
attemptCount++;
// Never complete task, force retries
yield {
content: 'Still working...',
isComplete: true
};
});
service.resolveAIProvider();
const result = await service.runExecutionAgent(step, callbacks);
// Should return undefined after max depth
expect(result).toBeUndefined();
expect(attemptCount).toBe(3); // MAX_AGENT_DEPTH
});
});
describe('Agent Interoperability', () => {
it('should allow PlanningAgent and ExecutionAgent to run independently', async () => {
const planningAgent = new PlanningAgent();
const executionAgent = new ExecutionAgent();
expect(planningAgent).toBeDefined();
expect(executionAgent).toBeDefined();
expect(planningAgent).not.toBe(executionAgent);
});
it('should create independent conversation contexts', () => {
const conversation1 = new Conversation();
const conversation2 = new Conversation();
conversation1.contents.push(new ConversationContent({
role: Role.User,
content: 'Request 1'
}));
conversation2.contents.push(new ConversationContent({
role: Role.User,
content: 'Request 2'
}));
expect(conversation1.contents).toHaveLength(1);
expect(conversation2.contents).toHaveLength(1);
expect(conversation1.contents[0].content).not.toBe(conversation2.contents[0].content);
});
});
describe('Agent Tool Restrictions', () => {
it('should allow PlanningAgent to use read-only tools', async () => {
const service = new PlanningAgent();
const conversation = new Conversation();
conversation.contents.push(new ConversationContent({
role: Role.User,
content: 'Plan based on vault content'
}));
const callbacks = createMockCallbacks();
let searchCalled = false;
mockAI.streamRequest.mockImplementation(async function* () {
if (!searchCalled) {
searchCalled = true;
yield {
content: 'Searching',
toolCall: new AIToolCall(
AITool.SearchVaultFiles,
{
search_terms: ['test'],
user_message: 'Searching'
},
'tool-1'
),
isComplete: true
};
} else {
yield {
content: 'Plan',
toolCall: new AIToolCall(
AITool.SubmitPlan,
{
steps: [
{
description: 'Process files',
instruction: 'Process found files'
}
]
},
'tool-2'
),
isComplete: true
};
}
});
service.resolveAIProvider();
const result = await service.runPlanningAgent(conversation, callbacks);
expect(mockAIToolService.performAITool).toHaveBeenCalled();
expect(result).toBeDefined();
});
it('should allow ExecutionAgent to use all vault tools', async () => {
const service = new ExecutionAgent();
const step: ExecutionStep = {
description: 'Search and process',
instruction: 'Find and read files'
};
const callbacks = createMockCallbacks();
let toolCallCount = 0;
mockAI.streamRequest.mockImplementation(async function* () {
toolCallCount++;
if (toolCallCount === 1) {
yield {
content: 'Searching',
toolCall: new AIToolCall(
AITool.SearchVaultFiles,
{
search_terms: ['test'],
user_message: 'Searching'
},
'tool-1'
),
isComplete: true
};
} else {
yield {
content: 'Done',
toolCall: new AIToolCall(
AITool.CompleteTask,
{
success: true,
description: 'Found and processed files'
},
'tool-2'
),
isComplete: true
};
}
});
service.resolveAIProvider();
const result = await service.runExecutionAgent(step, callbacks);
expect(mockAIToolService.performAITool).toHaveBeenCalled();
expect(result?.success).toBe(true);
});
});
});