andy-stack_vaultkeeper-ai/__tests__/Services/MultiAgentIntegration.test.ts
Andrew Beal 540b76e0ea test: update tests to use AIToolResponsePayload wrapper class
Refactor all test files to wrap tool response data in AIToolResponsePayload instances instead of passing raw objects directly to AIToolResponse constructor. Update assertions to access response data via payload.response property.
2026-04-08 21:16:59 +01:00

458 lines
13 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 { AIToolResponsePayload } from '../../AIClasses/ToolDefinitions/AIToolResponsePayload';
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, new AIToolResponsePayload({ 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);
// Mock SettingsService
RegisterSingleton(Services.SettingsService, {
settings: { enableMemories: false, allowUpdatingMemories: false }
});
});
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);
});
});
});