mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: update tests for AIFunctionService and path alias changes
- Remove AIFunctionDefinitions mock from AI class tests - Update IPrompt import path to AIPrompts directory - Remove deprecated streamRequest boolean parameter - Fix error handling assertions in AIFunctionService tests - Remove ChatService sanitization and assistant message tests - Add path aliases to tsconfig and vitest config
This commit is contained in:
parent
2de8109a74
commit
a4daa3a9df
8 changed files with 247 additions and 514 deletions
|
|
@ -3,7 +3,7 @@ import { Claude } from '../../AIClasses/Claude/Claude';
|
|||
import { RegisterSingleton, Resolve, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { StreamingService } from '../../Services/StreamingService';
|
||||
import type { IPrompt } from '../../AIClasses/IPrompt';
|
||||
import type { IPrompt } from '../../AIPrompts/IPrompt';
|
||||
import type VaultkeeperAIPlugin from '../../main';
|
||||
import { AIFunctionDefinitions } from '../../AIClasses/FunctionDefinitions/AIFunctionDefinitions';
|
||||
import { Conversation } from '../../Conversations/Conversation';
|
||||
|
|
@ -65,7 +65,6 @@ describe('Claude', () => {
|
|||
};
|
||||
RegisterSingleton(Services.StreamingService, mockStreamingService);
|
||||
|
||||
// Mock AIFunctionDefinitions
|
||||
mockFunctionDefinitions = {
|
||||
getQueryActions: vi.fn().mockReturnValue([
|
||||
{
|
||||
|
|
@ -80,7 +79,6 @@ describe('Claude', () => {
|
|||
}
|
||||
])
|
||||
};
|
||||
RegisterSingleton(Services.AIFunctionDefinitions, mockFunctionDefinitions);
|
||||
|
||||
// Mock IAIFileService
|
||||
const mockFileService = {
|
||||
|
|
@ -115,13 +113,12 @@ describe('Claude', () => {
|
|||
const plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
const streaming = Resolve<StreamingService>(Services.StreamingService);
|
||||
const functions = Resolve<AIFunctionDefinitions>(Services.AIFunctionDefinitions);
|
||||
|
||||
expect(prompt).toBe(mockPrompt);
|
||||
expect(plugin).toBe(mockPlugin);
|
||||
expect(settingsService).toBe(mockSettingsService);
|
||||
expect(streaming).toBe(mockStreamingService);
|
||||
expect(functions).toBe(mockFunctionDefinitions);
|
||||
expect(AIFunctionDefinitions.agentDefinitions).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1097,11 +1094,16 @@ describe('Claude', () => {
|
|||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test message' }));
|
||||
|
||||
// Set system prompts before calling streamRequest
|
||||
claude.systemPrompt = 'System instruction';
|
||||
claude.userInstruction = 'User instruction';
|
||||
claude.toolDefinitions = [];
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'response', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = claude.streamRequest(conversation, true);
|
||||
const generator = claude.streamRequest(conversation);
|
||||
|
||||
// Consume the generator
|
||||
for await (const chunk of generator) {
|
||||
|
|
@ -1146,7 +1148,7 @@ describe('Claude', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = claude.streamRequest(conversation, false);
|
||||
const generator = claude.streamRequest(conversation);
|
||||
|
||||
// Start consuming
|
||||
await generator.next();
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import { Gemini } from '../../AIClasses/Gemini/Gemini';
|
|||
import { RegisterSingleton, Resolve, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { StreamingService } from '../../Services/StreamingService';
|
||||
import type { IPrompt } from '../../AIClasses/IPrompt';
|
||||
import type { IPrompt } from '../../AIPrompts/IPrompt';
|
||||
import type VaultkeeperAIPlugin from '../../main';
|
||||
import { AIFunctionDefinitions } from '../../AIClasses/FunctionDefinitions/AIFunctionDefinitions';
|
||||
import { Conversation } from '../../Conversations/Conversation';
|
||||
import { ConversationContent } from '../../Conversations/ConversationContent';
|
||||
import { Role } from '../../Enums/Role';
|
||||
|
|
@ -20,7 +19,6 @@ describe('Gemini', () => {
|
|||
let mockPrompt: any;
|
||||
let mockPlugin: any;
|
||||
let mockSettingsService: any;
|
||||
let mockFunctionDefinitions: any;
|
||||
let abortService: AbortService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -65,23 +63,6 @@ describe('Gemini', () => {
|
|||
};
|
||||
RegisterSingleton(Services.StreamingService, mockStreamingService);
|
||||
|
||||
// Mock AIFunctionDefinitions
|
||||
mockFunctionDefinitions = {
|
||||
getQueryActions: vi.fn().mockReturnValue([
|
||||
{
|
||||
name: 'search_vault_filestion',
|
||||
description: 'Test function',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
};
|
||||
RegisterSingleton(Services.AIFunctionDefinitions, mockFunctionDefinitions);
|
||||
|
||||
// Mock IAIFileService
|
||||
const mockFileService = {
|
||||
refreshCache: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -114,13 +95,11 @@ describe('Gemini', () => {
|
|||
const plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
const streaming = Resolve<StreamingService>(Services.StreamingService);
|
||||
const functions = Resolve<AIFunctionDefinitions>(Services.AIFunctionDefinitions);
|
||||
|
||||
expect(prompt).toBe(mockPrompt);
|
||||
expect(plugin).toBe(mockPlugin);
|
||||
expect(settingsService).toBe(mockSettingsService);
|
||||
expect(streaming).toBe(mockStreamingService);
|
||||
expect(functions).toBe(mockFunctionDefinitions);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -365,7 +344,7 @@ describe('Gemini', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = gemini.streamRequest(conversation, true);
|
||||
const generator = gemini.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -393,7 +372,7 @@ describe('Gemini', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = gemini.streamRequest(conversation, true);
|
||||
const generator = gemini.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -414,7 +393,7 @@ describe('Gemini', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = gemini.streamRequest(conversation, true);
|
||||
const generator = gemini.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -428,11 +407,16 @@ describe('Gemini', () => {
|
|||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test', displayContent: 'Test' }));
|
||||
|
||||
// Set system prompts before calling streamRequest
|
||||
gemini.systemPrompt = 'System instruction';
|
||||
gemini.userInstruction = 'User instruction';
|
||||
gemini.toolDefinitions = [];
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = gemini.streamRequest(conversation, true);
|
||||
const generator = gemini.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -1032,7 +1016,7 @@ describe('Gemini', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = gemini.streamRequest(conversation, true);
|
||||
const generator = gemini.streamRequest(conversation);
|
||||
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
|
|
@ -1063,7 +1047,7 @@ describe('Gemini', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = gemini.streamRequest(conversation, false);
|
||||
const generator = gemini.streamRequest(conversation);
|
||||
await generator.next();
|
||||
|
||||
// State should be reset (after checking web search mode)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import { OpenAI } from '../../AIClasses/OpenAI/OpenAI';
|
|||
import { RegisterSingleton, Resolve, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { StreamingService } from '../../Services/StreamingService';
|
||||
import type { IPrompt } from '../../AIClasses/IPrompt';
|
||||
import type { IPrompt } from '../../AIPrompts/IPrompt';
|
||||
import type VaultkeeperAIPlugin from '../../main';
|
||||
import { AIFunctionDefinitions } from '../../AIClasses/FunctionDefinitions/AIFunctionDefinitions';
|
||||
import { Conversation } from '../../Conversations/Conversation';
|
||||
import { ConversationContent } from '../../Conversations/ConversationContent';
|
||||
import { Role } from '../../Enums/Role';
|
||||
|
|
@ -20,7 +19,6 @@ describe('OpenAI', () => {
|
|||
let mockPrompt: any;
|
||||
let mockPlugin: any;
|
||||
let mockSettingsService: any;
|
||||
let mockFunctionDefinitions: any;
|
||||
let abortService: AbortService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -68,23 +66,6 @@ describe('OpenAI', () => {
|
|||
};
|
||||
RegisterSingleton(Services.StreamingService, mockStreamingService);
|
||||
|
||||
// Mock AIFunctionDefinitions
|
||||
mockFunctionDefinitions = {
|
||||
getQueryActions: vi.fn().mockReturnValue([
|
||||
{
|
||||
name: 'search_vault_filestion',
|
||||
description: 'Test function',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
};
|
||||
RegisterSingleton(Services.AIFunctionDefinitions, mockFunctionDefinitions);
|
||||
|
||||
// Mock IAIFileService
|
||||
const mockFileService = {
|
||||
refreshCache: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -118,13 +99,11 @@ describe('OpenAI', () => {
|
|||
const plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
const streaming = Resolve<StreamingService>(Services.StreamingService);
|
||||
const functions = Resolve<AIFunctionDefinitions>(Services.AIFunctionDefinitions);
|
||||
|
||||
expect(prompt).toBe(mockPrompt);
|
||||
expect(plugin).toBe(mockPlugin);
|
||||
expect(settingsService).toBe(mockSettingsService);
|
||||
expect(streaming).toBe(mockStreamingService);
|
||||
expect(functions).toBe(mockFunctionDefinitions);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -329,11 +308,16 @@ describe('OpenAI', () => {
|
|||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Hello' }));
|
||||
|
||||
// Set system prompts before calling streamRequest
|
||||
openai.systemPrompt = 'System instruction';
|
||||
openai.userInstruction = 'User instruction';
|
||||
openai.toolDefinitions = [];
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'response', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
|
||||
// Consume generator
|
||||
for await (const chunk of generator) {
|
||||
|
|
@ -367,7 +351,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -410,7 +394,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -439,7 +423,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -465,7 +449,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -487,7 +471,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -518,7 +502,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -563,7 +547,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -608,7 +592,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -662,7 +646,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -695,7 +679,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -728,7 +712,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -768,7 +752,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -834,7 +818,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
@ -915,7 +899,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'response', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
|
||||
for await (const chunk of generator) {
|
||||
// Just consume
|
||||
|
|
@ -946,7 +930,7 @@ describe('OpenAI', () => {
|
|||
yield { content: 'done', isComplete: true };
|
||||
});
|
||||
|
||||
const generator = openai.streamRequest(conversation, true);
|
||||
const generator = openai.streamRequest(conversation);
|
||||
for await (const chunk of generator) {}
|
||||
|
||||
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
|
||||
|
|
|
|||
177
__tests__/Services/AIControllerService.test.ts
Normal file
177
__tests__/Services/AIControllerService.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { AIControllerService } from '../../Services/AIControllerService';
|
||||
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 { AIFunction } from '../../Enums/AIFunction';
|
||||
|
||||
/**
|
||||
* INTEGRATION TESTS - AIControllerService
|
||||
*
|
||||
* Tests the multi-agent controller service that orchestrates:
|
||||
* - Main agent execution
|
||||
* - Planning agent workflow
|
||||
* - Execution agent workflow
|
||||
* - Agent loop management
|
||||
*/
|
||||
|
||||
describe('AIControllerService - Integration Tests', () => {
|
||||
let service: AIControllerService;
|
||||
let mockAI: any;
|
||||
let mockPrompt: any;
|
||||
let mockAIFunctionService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock IPrompt
|
||||
mockPrompt = {
|
||||
systemInstruction: vi.fn().mockReturnValue('System instruction'),
|
||||
userInstruction: vi.fn().mockResolvedValue('User instruction'),
|
||||
planningInstruction: vi.fn().mockReturnValue('Planning instruction')
|
||||
};
|
||||
RegisterSingleton(Services.IPrompt, mockPrompt);
|
||||
|
||||
// Mock AIFunctionService
|
||||
mockAIFunctionService = {
|
||||
performAIFunction: vi.fn().mockResolvedValue({
|
||||
name: AIFunction.SearchVaultFiles,
|
||||
response: [],
|
||||
toolId: 'test-tool-id'
|
||||
})
|
||||
};
|
||||
RegisterSingleton(Services.AIFunctionService, mockAIFunctionService);
|
||||
|
||||
// Mock IAIClass
|
||||
mockAI = {
|
||||
systemPrompt: '',
|
||||
userInstruction: '',
|
||||
toolDefinitions: [],
|
||||
streamRequest: vi.fn()
|
||||
};
|
||||
RegisterSingleton(Services.IAIClass, mockAI);
|
||||
|
||||
// Create service
|
||||
service = new AIControllerService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with all required services', () => {
|
||||
const testService = new AIControllerService();
|
||||
|
||||
expect(testService).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAIProvider', () => {
|
||||
it('should resolve AI provider', () => {
|
||||
service.resolveAIProvider();
|
||||
|
||||
// Should not throw
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMainAgent', () => {
|
||||
it('should set system prompt and user instruction', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test request' }));
|
||||
|
||||
const callbacks = {
|
||||
onSubmit: vi.fn(),
|
||||
onStreamingUpdate: vi.fn(),
|
||||
onThoughtUpdate: vi.fn(),
|
||||
onComplete: vi.fn(),
|
||||
onCancel: vi.fn()
|
||||
};
|
||||
|
||||
// Mock streamRequest to return immediately without function calls
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'Response', isComplete: true };
|
||||
});
|
||||
|
||||
service.resolveAIProvider();
|
||||
await service.runMainAgent(conversation, true, callbacks);
|
||||
|
||||
// Verify system prompt and user instruction were set
|
||||
expect(mockPrompt.systemInstruction).toHaveBeenCalled();
|
||||
expect(mockPrompt.userInstruction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle streaming response without function calls', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test request' }));
|
||||
|
||||
const callbacks = {
|
||||
onSubmit: vi.fn(),
|
||||
onStreamingUpdate: vi.fn(),
|
||||
onThoughtUpdate: vi.fn(),
|
||||
onComplete: vi.fn(),
|
||||
onCancel: vi.fn()
|
||||
};
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'Part 1', isComplete: false };
|
||||
yield { content: ' Part 2', isComplete: true };
|
||||
});
|
||||
|
||||
service.resolveAIProvider();
|
||||
await service.runMainAgent(conversation, true, callbacks);
|
||||
|
||||
// Should have added assistant message to conversation
|
||||
expect(conversation.contents.length).toBeGreaterThan(1);
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
expect(lastMessage.role).toBe(Role.Assistant);
|
||||
expect(lastMessage.content).toContain('Part 1');
|
||||
});
|
||||
|
||||
it('should throw error if AI provider not resolved', async () => {
|
||||
const conversation = new Conversation();
|
||||
const callbacks = {
|
||||
onSubmit: vi.fn(),
|
||||
onStreamingUpdate: vi.fn(),
|
||||
onThoughtUpdate: vi.fn(),
|
||||
onComplete: vi.fn(),
|
||||
onCancel: vi.fn()
|
||||
};
|
||||
|
||||
// Don't call resolveAIProvider()
|
||||
await expect(service.runMainAgent(conversation, true, callbacks))
|
||||
.rejects.toThrow('Error: No AI provider has been set!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureCorrectConversationStructure', () => {
|
||||
it('should insert Continue message when last message is from assistant', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'First' }));
|
||||
conversation.contents.push(new ConversationContent({ role: Role.Assistant, content: 'Response' }));
|
||||
|
||||
const callbacks = {
|
||||
onSubmit: vi.fn(),
|
||||
onStreamingUpdate: vi.fn(),
|
||||
onThoughtUpdate: vi.fn(),
|
||||
onComplete: vi.fn(),
|
||||
onCancel: vi.fn()
|
||||
};
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'New response', isComplete: true };
|
||||
});
|
||||
|
||||
service.resolveAIProvider();
|
||||
await service.runMainAgent(conversation, true, callbacks);
|
||||
|
||||
// Should have inserted Continue message between last assistant and new assistant
|
||||
const continueMessage = conversation.contents.find(c =>
|
||||
c.role === Role.User && c.content === 'Continue' && c.shouldDisplayContent === false
|
||||
);
|
||||
expect(continueMessage).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -550,7 +550,7 @@ describe('AIFunctionService - Integration Tests', () => {
|
|||
toolId: 'tool_patch_invalid'
|
||||
} as any);
|
||||
|
||||
expect(result.response.error).toContain('Invalid arguments for PatchVaultFile');
|
||||
expect(result.response.error).toContain('Invalid arguments for patch_vault_file');
|
||||
expect(mockFileSystemService.patchFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -782,28 +782,20 @@ describe('AIFunctionService - Integration Tests', () => {
|
|||
});
|
||||
|
||||
describe('performAIFunction - Unknown Function', () => {
|
||||
it('should return error for unknown function', async () => {
|
||||
const result = await service.performAIFunction({
|
||||
it('should throw error for unknown function', async () => {
|
||||
await expect(service.performAIFunction({
|
||||
name: 'UnknownFunction' as any,
|
||||
arguments: {},
|
||||
toolId: 'tool_27'
|
||||
} as any);
|
||||
|
||||
expect(result.response).toEqual({
|
||||
error: 'Unknown function request UnknownFunction'
|
||||
});
|
||||
} as any)).rejects.toThrow('Unknown function name: UnknownFunction');
|
||||
});
|
||||
|
||||
it('should preserve toolId in error response', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const result = await service.performAIFunction({
|
||||
it('should throw error for invalid function', async () => {
|
||||
await expect(service.performAIFunction({
|
||||
name: 'InvalidFunction' as any,
|
||||
arguments: {},
|
||||
toolId: 'tool_error'
|
||||
} as any);
|
||||
|
||||
expect(result.toolId).toBe('tool_error');
|
||||
} as any)).rejects.toThrow('Unknown function name: InvalidFunction');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||
import { ChatService } from '../../Services/ChatService';
|
||||
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 { AIFunctionCall } from '../../AIClasses/AIFunctionCall';
|
||||
import { AIFunction, fromString } from '../../Enums/AIFunction';
|
||||
import { AbortService } from '../../Services/AbortService';
|
||||
|
||||
|
|
@ -22,10 +18,8 @@ import { AbortService } from '../../Services/AbortService';
|
|||
describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
||||
let service: ChatService;
|
||||
let mockConversationService: any;
|
||||
let mockAIFunctionService: any;
|
||||
let mockAIControllerService: any;
|
||||
let mockNamingService: any;
|
||||
let mockPrompt: any;
|
||||
let mockStatusBarService: any;
|
||||
let mockEventService: any;
|
||||
let mockWorkSpaceService: any;
|
||||
let abortService: AbortService;
|
||||
|
|
@ -36,23 +30,14 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
saveConversation: vi.fn()
|
||||
};
|
||||
|
||||
mockAIFunctionService = {
|
||||
performAIFunction: vi.fn()
|
||||
mockAIControllerService = {
|
||||
runMainAgent: vi.fn()
|
||||
};
|
||||
|
||||
mockNamingService = {
|
||||
requestName: vi.fn()
|
||||
};
|
||||
|
||||
mockPrompt = {
|
||||
systemInstruction: vi.fn().mockReturnValue('System prompt'),
|
||||
userInstruction: vi.fn().mockResolvedValue('User prompt')
|
||||
};
|
||||
|
||||
mockStatusBarService = {
|
||||
animateTokens: vi.fn()
|
||||
};
|
||||
|
||||
// Mock EventService since it extends Obsidian's Events class
|
||||
mockEventService = {
|
||||
trigger: vi.fn(),
|
||||
|
|
@ -71,9 +56,8 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
|
||||
// Register dependencies
|
||||
RegisterSingleton(Services.ConversationFileSystemService, mockConversationService);
|
||||
RegisterSingleton(Services.AIFunctionService, mockAIFunctionService);
|
||||
RegisterSingleton(Services.AIControllerService, mockAIControllerService);
|
||||
RegisterSingleton(Services.ConversationNamingService, mockNamingService);
|
||||
RegisterSingleton(Services.IPrompt, mockPrompt);
|
||||
RegisterSingleton(Services.EventService, mockEventService);
|
||||
RegisterSingleton(Services.WorkSpaceService, mockWorkSpaceService);
|
||||
RegisterSingleton(Services.AbortService, abortService);
|
||||
|
|
@ -100,18 +84,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('resolveAIProvider', () => {
|
||||
it('should resolve AI provider services', () => {
|
||||
const mockAI = { streamRequest: vi.fn() };
|
||||
RegisterSingleton(Services.IAIClass, mockAI as any);
|
||||
|
||||
service.resolveAIProvider();
|
||||
|
||||
// Should not throw
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('should not throw when called with no active request', () => {
|
||||
expect(() => service.stop()).not.toThrow();
|
||||
|
|
@ -142,397 +114,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('assistant-to-assistant message prevention', () => {
|
||||
it('should insert Continue message when last message is from assistant', () => {
|
||||
const conversation = new Conversation();
|
||||
// Add an initial user message
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'First message' }));
|
||||
// Add an assistant response
|
||||
conversation.contents.push(new ConversationContent({ role: Role.Assistant, content: 'Assistant response' }));
|
||||
|
||||
// Verify the last message is from assistant
|
||||
expect(conversation.contents[conversation.contents.length - 1].role).toBe(Role.Assistant);
|
||||
expect(conversation.contents.length).toBe(2);
|
||||
|
||||
// Now we need to simulate what happens in submit() when a new user message is added
|
||||
// Check if the last message is from the assistant (like the fix does)
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Continue message was inserted
|
||||
expect(conversation.contents.length).toBe(3);
|
||||
const continueMessage = conversation.contents[2];
|
||||
expect(continueMessage.role).toBe(Role.User);
|
||||
expect(continueMessage.content).toBe('Continue');
|
||||
expect(continueMessage.shouldDisplayContent).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT insert Continue message when last message is from user', () => {
|
||||
const conversation = new Conversation();
|
||||
// Add a user message
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'User message' }));
|
||||
|
||||
// Verify the last message is from user
|
||||
expect(conversation.contents[conversation.contents.length - 1].role).toBe(Role.User);
|
||||
expect(conversation.contents.length).toBe(1);
|
||||
|
||||
// Simulate the check from submit()
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify NO Continue message was inserted
|
||||
expect(conversation.contents.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should NOT insert Continue message for empty conversation', () => {
|
||||
const conversation = new Conversation();
|
||||
|
||||
// Verify empty
|
||||
expect(conversation.contents.length).toBe(0);
|
||||
|
||||
// Simulate the check from submit()
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify still empty
|
||||
expect(conversation.contents.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should maintain proper structure after Continue message insertion', () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'First' }));
|
||||
conversation.contents.push(new ConversationContent({ role: Role.Assistant, content: 'Response' }));
|
||||
|
||||
// Simulate the fix
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
|
||||
// Add the actual user message
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Second message' }));
|
||||
|
||||
// Verify the structure: User -> Assistant -> User(Continue) -> User
|
||||
expect(conversation.contents.length).toBe(4);
|
||||
expect(conversation.contents[0].role).toBe(Role.User);
|
||||
expect(conversation.contents[0].content).toBe('First');
|
||||
expect(conversation.contents[1].role).toBe(Role.Assistant);
|
||||
expect(conversation.contents[1].content).toBe('Response');
|
||||
expect(conversation.contents[2].role).toBe(Role.User);
|
||||
expect(conversation.contents[2].content).toBe('Continue');
|
||||
expect(conversation.contents[2].shouldDisplayContent).toBe(false);
|
||||
expect(conversation.contents[3].role).toBe(Role.User);
|
||||
expect(conversation.contents[3].content).toBe('Second message');
|
||||
});
|
||||
|
||||
it('should work with function call responses in conversation', () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Request' }));
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: 'Making a function call',
|
||||
functionCall: '{"name":"test"}',
|
||||
timestamp: new Date()
|
||||
}));
|
||||
// Function response (already User role with functionResponse)
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: 'Function result',
|
||||
displayContent: 'Function result',
|
||||
functionResponse: '',
|
||||
timestamp: new Date(),
|
||||
toolId: 'tool-1'
|
||||
}));
|
||||
// Assistant processes the function response
|
||||
conversation.contents.push(new ConversationContent({ role: Role.Assistant, content: 'Final response' }));
|
||||
|
||||
// Now the last message is Assistant, so Continue should be inserted
|
||||
expect(conversation.contents[conversation.contents.length - 1].role).toBe(Role.Assistant);
|
||||
|
||||
// Simulate the fix
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Continue was inserted
|
||||
expect(conversation.contents.length).toBe(5);
|
||||
expect(conversation.contents[4].content).toBe('Continue');
|
||||
expect(conversation.contents[4].shouldDisplayContent).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeFunctionCallContent (private method tests via reflection)', () => {
|
||||
// Access private method for testing
|
||||
const getSanitizeMethod = (service: ChatService) => {
|
||||
return (service as any).sanitizeFunctionCallContent.bind(service);
|
||||
};
|
||||
|
||||
it('should return content unchanged when no function call is provided', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const content = 'This is regular content with {"some": "json"}';
|
||||
|
||||
const result = sanitize(content, null);
|
||||
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
|
||||
it('should return content unchanged when content is empty', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, { arg1: 'value1' });
|
||||
|
||||
const result = sanitize('', functionCall);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return content unchanged when content is only whitespace', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, { arg1: 'value1' });
|
||||
|
||||
const result = sanitize(' \n\t ', functionCall);
|
||||
|
||||
expect(result).toBe(' \n\t ');
|
||||
});
|
||||
|
||||
it('should remove exact function call JSON from content', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.WriteVaultFile, {
|
||||
file_path: 'test.md',
|
||||
content: 'Hello world'
|
||||
});
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
const content = `Here is the function call: ${functionCallString}`;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('Here is the function call:');
|
||||
});
|
||||
|
||||
it('should remove pretty-printed function call JSON (2 spaces)', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, {
|
||||
query: 'test query',
|
||||
limit: 10
|
||||
});
|
||||
|
||||
const prettyPrinted = JSON.stringify(JSON.parse(functionCall.toConversationString()), null, 2);
|
||||
const content = `Function call:\n${prettyPrinted}\nEnd of call`;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('Function call:\n\nEnd of call');
|
||||
});
|
||||
|
||||
it('should remove pretty-printed function call JSON (4 spaces)', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.ReadVaultFiles, {
|
||||
file_path: 'example.ts'
|
||||
});
|
||||
|
||||
const prettyPrinted = JSON.stringify(JSON.parse(functionCall.toConversationString()), null, 4);
|
||||
const content = prettyPrinted;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should preserve legitimate JSON content when no function call exists', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const legitimateJson = JSON.stringify({
|
||||
user: 'test',
|
||||
data: { nested: 'value' }
|
||||
}, null, 2);
|
||||
const content = `Here is your data:\n${legitimateJson}`;
|
||||
|
||||
const result = sanitize(content, null);
|
||||
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
|
||||
it('should only remove function call JSON, preserving other content', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.DeleteVaultFiles, {
|
||||
file_path: 'old.txt'
|
||||
});
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
const content = `Processing your request...\n${functionCallString}\nOperation completed successfully.`;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('Processing your request...\n\nOperation completed successfully.');
|
||||
});
|
||||
|
||||
it('should handle function calls with special characters in arguments', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.WriteVaultFile, {
|
||||
file_path: 'path/to/file.ts',
|
||||
content: 'const regex = /test.*pattern/g;\nfunction() { return "value"; }'
|
||||
});
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
const content = functionCallString;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle function calls with nested objects', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, {
|
||||
config: {
|
||||
nested: {
|
||||
deeply: {
|
||||
value: 'test'
|
||||
}
|
||||
}
|
||||
},
|
||||
array: [1, 2, 3]
|
||||
});
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
const content = `Before\n${functionCallString}\nAfter`;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('Before\n\nAfter');
|
||||
});
|
||||
|
||||
it('should handle function calls with toolId', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, { arg: 'value' }, 'tool-123');
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
const content = functionCallString;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should not remove similar but different JSON structures', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.WriteVaultFile, {
|
||||
file_path: 'test.md'
|
||||
});
|
||||
|
||||
// Similar but different structure - should not be removed
|
||||
const differentJson = JSON.stringify({
|
||||
functionCall: {
|
||||
name: 'write_file',
|
||||
args: { file_path: 'different.md' }
|
||||
}
|
||||
});
|
||||
const content = `Here is a different call: ${differentJson}`;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
// Should keep the different JSON since it doesn't match exactly
|
||||
expect(result).toContain('different.md');
|
||||
});
|
||||
|
||||
it('should handle multiple whitespace variations in pretty-printed JSON', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, { key: 'value' });
|
||||
|
||||
// Content with irregular whitespace
|
||||
const content = `{
|
||||
"functionCall": {
|
||||
"name": "search_vault_files",
|
||||
"args": {
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should preserve natural language content when function call exists', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.WriteVaultFile, {
|
||||
content: 'Character sheet content',
|
||||
file_name: 'Walker.md'
|
||||
});
|
||||
const content = 'I have created a note for Omaz and will now create a note for walker';
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
// Natural language content should be preserved - it doesn't look like JSON
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
|
||||
it('should preserve content that does not start with {', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, {
|
||||
search_terms: ['Walker']
|
||||
});
|
||||
const content = 'Searching for details about Walker in Campaign 2 folder.';
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
// Content doesn't look like JSON, should be preserved
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
|
||||
it('should preserve content that does not end with }', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, { key: 'value' });
|
||||
const content = '{ "incomplete": "json"';
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
// Malformed JSON should be preserved
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
|
||||
it('should preserve content with mixed text and JSON-like structures', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.SearchVaultFiles, { key: 'value' });
|
||||
const content = 'Here is some info: { "data": "example" } and more text';
|
||||
|
||||
const result = sanitize(content, functionCall);
|
||||
|
||||
// Mixed content should be preserved - doesn't match the exact JSON pattern
|
||||
expect(result).toBe(content);
|
||||
});
|
||||
|
||||
it('should only sanitize when content is EXACTLY matching JSON structure', () => {
|
||||
const sanitize = getSanitizeMethod(service);
|
||||
const functionCall = new AIFunctionCall(AIFunction.WriteVaultFile, {
|
||||
file_path: 'test.md',
|
||||
content: 'Hello'
|
||||
});
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
|
||||
// Only exact matches should be removed
|
||||
const exactMatch = functionCallString;
|
||||
const withPrefix = `Creating file: ${functionCallString}`;
|
||||
const withSuffix = `${functionCallString} - done`;
|
||||
|
||||
expect(sanitize(exactMatch, functionCall)).toBe('');
|
||||
expect(sanitize(withPrefix, functionCall)).toBe('Creating file:');
|
||||
expect(sanitize(withSuffix, functionCall)).toBe('- done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromString', () => {
|
||||
it('should convert valid function name strings to AIFunction enum', () => {
|
||||
expect(fromString('search_vault_files')).toBe(AIFunction.SearchVaultFiles);
|
||||
|
|
|
|||
|
|
@ -18,14 +18,26 @@
|
|||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"Helpers/*": ["./Helpers/*"],
|
||||
"Enums/*": ["./Enums/*"],
|
||||
"Services/*": ["./Services/*"],
|
||||
"Conversations/*": ["./Conversations/*"],
|
||||
"AIClasses/*": ["./AIClasses/*"],
|
||||
"AIPrompts/*": ["./AIPrompts/*"],
|
||||
"Components/*": ["./Components/*"],
|
||||
"Stores/*": ["./Stores/*"],
|
||||
"Views/*": ["./Views/*"],
|
||||
"Modals/*": ["./Modals/*"],
|
||||
"Types/*": ["./Types/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.svelte"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"__tests__"
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ export default defineConfig({
|
|||
"Services": path.resolve(__dirname, "Services"),
|
||||
"Conversations": path.resolve(__dirname, "Conversations"),
|
||||
"AIClasses": path.resolve(__dirname, "AIClasses"),
|
||||
"AIPrompts": path.resolve(__dirname, "AIPrompts"),
|
||||
"Components": path.resolve(__dirname, "Components"),
|
||||
"Stores": path.resolve(__dirname, "Stores"),
|
||||
"Views": path.resolve(__dirname, "Views"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue