mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
487 lines
13 KiB
TypeScript
487 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 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(),
|
|
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, false);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.executionSteps).toHaveLength(1);
|
|
expect(result?.isReplan).toBe(false);
|
|
});
|
|
|
|
it('should mark plan as replan when requested', async () => {
|
|
const service = new PlanningAgent();
|
|
const conversation = new Conversation();
|
|
conversation.contents.push(new ConversationContent({
|
|
role: Role.User,
|
|
content: 'Revise plan'
|
|
}));
|
|
const callbacks = createMockCallbacks();
|
|
|
|
mockAI.streamRequest.mockImplementation(async function* () {
|
|
yield {
|
|
content: 'Replan',
|
|
toolCall: new AIToolCall(
|
|
AITool.SubmitPlan,
|
|
{
|
|
steps: [
|
|
{
|
|
description: 'Revised step',
|
|
instruction: 'New approach'
|
|
}
|
|
]
|
|
},
|
|
'tool-1'
|
|
),
|
|
isComplete: true
|
|
};
|
|
});
|
|
|
|
service.resolveAIProvider();
|
|
const result = await service.runPlanningAgent(conversation, callbacks, true);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.isReplan).toBe(true);
|
|
});
|
|
});
|
|
|
|
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, false);
|
|
|
|
// 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, false);
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|