mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Restructure system prompt to enforce mandatory complexity evaluation before action. Replace verbose multi-step planning framework with concise gate-based decision model. Add UI for execution plan visibility. Improve planning/execution separation by blocking execution tools during planning phase. Remove cancellation indicator component. Fix conversation deletion bug preventing saves after delete. Strengthen type safety for AIFunction names. Simplify function summary format for planning agent. Update test mocks for new callback signatures.
689 lines
23 KiB
TypeScript
689 lines
23 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { ConversationFileSystemService } from '../../Services/ConversationFileSystemService';
|
|
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 { TFile } from 'obsidian';
|
|
import { Exception } from '../../Helpers/Exception';
|
|
|
|
/**
|
|
* INTEGRATION TESTS - ConversationFileSystemService
|
|
*
|
|
* Tests conversation persistence with real FileSystemService integration.
|
|
* Mocks only the underlying VaultService operations.
|
|
*
|
|
* Tests:
|
|
* - Saving and loading conversations
|
|
* - Path management
|
|
* - Conversation title updates
|
|
* - Listing all conversations
|
|
*/
|
|
|
|
describe('ConversationFileSystemService - Integration Tests', () => {
|
|
let service: ConversationFileSystemService;
|
|
let mockFileSystemService: any;
|
|
|
|
beforeEach(() => {
|
|
// Mock FileSystemService
|
|
mockFileSystemService = {
|
|
writeObjectToFile: vi.fn().mockResolvedValue(true),
|
|
readObjectFromFile: vi.fn(),
|
|
deleteFile: vi.fn(),
|
|
moveFile: vi.fn(),
|
|
listFilesInDirectory: vi.fn(),
|
|
exists: vi.fn().mockResolvedValue(true)
|
|
};
|
|
|
|
// Register the mock
|
|
RegisterSingleton(Services.FileSystemService, mockFileSystemService);
|
|
|
|
// Mock Exception.log
|
|
vi.spyOn(Exception, 'log').mockImplementation(() => {});
|
|
|
|
// Create service
|
|
service = new ConversationFileSystemService();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clear singleton registry to prevent memory leaks
|
|
DeregisterAllServices();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// Helper to create mock TFile
|
|
function createMockFile(path: string): TFile {
|
|
const file = new TFile();
|
|
file.path = path;
|
|
const fileName = path.split('/').pop() || '';
|
|
file.name = fileName;
|
|
file.basename = fileName.split('.')[0] || '';
|
|
file.extension = 'json';
|
|
return file;
|
|
}
|
|
|
|
// Helper to create a test conversation
|
|
function createTestConversation(title: string = 'Test Conversation'): Conversation {
|
|
const conversation = new Conversation();
|
|
conversation.title = title;
|
|
conversation.created = new Date('2024-01-01T10:00:00Z');
|
|
conversation.updated = new Date('2024-01-01T10:30:00Z');
|
|
|
|
conversation.contents.push(
|
|
new ConversationContent({ role: Role.User, content: 'Hello', timestamp: new Date('2024-01-01T10:00:00Z') }),
|
|
new ConversationContent({ role: Role.Assistant, content: 'Hi there!', timestamp: new Date('2024-01-01T10:01:00Z') })
|
|
);
|
|
|
|
return conversation;
|
|
}
|
|
|
|
describe('generateConversationPath', () => {
|
|
it('should generate correct path from conversation title', () => {
|
|
const conversation = createTestConversation('My Test Note');
|
|
const path = service.generateConversationPath(conversation);
|
|
|
|
expect(path).toBe('Vaultkeeper AI/Conversations/My Test Note.json');
|
|
});
|
|
|
|
it('should handle special characters in title', () => {
|
|
const conversation = createTestConversation('Test: Special & Characters!');
|
|
const path = service.generateConversationPath(conversation);
|
|
|
|
expect(path).toBe('Vaultkeeper AI/Conversations/Test: Special & Characters!.json');
|
|
});
|
|
|
|
it('should handle empty title', () => {
|
|
const conversation = createTestConversation('');
|
|
const path = service.generateConversationPath(conversation);
|
|
|
|
expect(path).toBe('Vaultkeeper AI/Conversations/.json');
|
|
});
|
|
});
|
|
|
|
describe('saveConversation', () => {
|
|
it('should save conversation with correct structure', async () => {
|
|
const conversation = createTestConversation('Test Save');
|
|
|
|
const path = await service.saveConversation(conversation);
|
|
|
|
expect(path).toBe('Vaultkeeper AI/Conversations/Test Save.json');
|
|
expect(mockFileSystemService.writeObjectToFile).toHaveBeenCalledWith(
|
|
'Vaultkeeper AI/Conversations/Test Save.json',
|
|
expect.objectContaining({
|
|
title: 'Test Save',
|
|
created: '2024-01-01T10:00:00.000Z',
|
|
contents: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
role: Role.User,
|
|
content: 'Hello',
|
|
timestamp: '2024-01-01T10:00:00.000Z'
|
|
}),
|
|
expect.objectContaining({
|
|
role: Role.Assistant,
|
|
content: 'Hi there!',
|
|
timestamp: '2024-01-01T10:01:00.000Z'
|
|
})
|
|
])
|
|
}),
|
|
true,
|
|
false
|
|
);
|
|
});
|
|
|
|
it('should update the updated timestamp when saving', async () => {
|
|
const conversation = createTestConversation('Test Timestamp');
|
|
const originalUpdated = conversation.updated;
|
|
|
|
await service.saveConversation(conversation);
|
|
|
|
expect(conversation.updated).not.toEqual(originalUpdated);
|
|
expect(conversation.updated.getTime()).toBeGreaterThanOrEqual(originalUpdated.getTime());
|
|
});
|
|
|
|
|
|
it('should set current conversation path on first save', async () => {
|
|
const conversation = createTestConversation('First Save');
|
|
|
|
expect(service.getCurrentConversationPath()).toBeNull();
|
|
|
|
await service.saveConversation(conversation);
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/First Save.json');
|
|
});
|
|
|
|
it('should reuse current path on subsequent saves', async () => {
|
|
const conversation = createTestConversation('Original Title');
|
|
|
|
// First save
|
|
await service.saveConversation(conversation);
|
|
const firstPath = service.getCurrentConversationPath();
|
|
|
|
// Change title and save again
|
|
conversation.title = 'Changed Title';
|
|
await service.saveConversation(conversation);
|
|
const secondPath = service.getCurrentConversationPath();
|
|
|
|
// Path should remain the same (uses cached path)
|
|
expect(firstPath).toBe(secondPath);
|
|
expect(secondPath).toBe('Vaultkeeper AI/Conversations/Original Title.json');
|
|
});
|
|
|
|
it('should serialize function calls correctly', async () => {
|
|
const conversation = createTestConversation('With Function Call');
|
|
const functionCall = {
|
|
name: 'test_function',
|
|
arguments: { arg1: 'value1' }
|
|
};
|
|
|
|
conversation.contents.push(
|
|
new ConversationContent({
|
|
role: Role.Assistant,
|
|
content: 'Function call',
|
|
functionCall: JSON.stringify(functionCall),
|
|
timestamp: new Date('2024-01-01T10:02:00Z'),
|
|
toolId: 'tool_123'
|
|
})
|
|
);
|
|
|
|
await service.saveConversation(conversation);
|
|
|
|
const savedData = mockFileSystemService.writeObjectToFile.mock.calls[0][1];
|
|
const functionCallContent = savedData.contents[2];
|
|
|
|
expect(functionCallContent).toMatchObject({
|
|
role: Role.Assistant,
|
|
content: 'Function call',
|
|
functionCall: JSON.stringify(functionCall),
|
|
timestamp: '2024-01-01T10:02:00.000Z',
|
|
toolId: 'tool_123'
|
|
});
|
|
});
|
|
|
|
it('should serialize function call responses correctly', async () => {
|
|
const conversation = createTestConversation('With Function Response');
|
|
|
|
conversation.contents.push(
|
|
new ConversationContent({
|
|
role: Role.User,
|
|
content: 'Function response',
|
|
functionResponse: 'response data',
|
|
timestamp: new Date('2024-01-01T10:03:00Z'),
|
|
toolId: 'tool_456'
|
|
})
|
|
);
|
|
|
|
await service.saveConversation(conversation);
|
|
|
|
const savedData = mockFileSystemService.writeObjectToFile.mock.calls[0][1];
|
|
const responseContent = savedData.contents[2];
|
|
|
|
expect(responseContent.functionResponse).toBe('response data');
|
|
expect(responseContent.toolId).toBe('tool_456');
|
|
});
|
|
|
|
it('should handle empty conversation', async () => {
|
|
const conversation = new Conversation();
|
|
conversation.title = 'Empty';
|
|
|
|
await service.saveConversation(conversation);
|
|
|
|
const savedData = mockFileSystemService.writeObjectToFile.mock.calls[0][1];
|
|
expect(savedData.contents).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getCurrentConversationPath', () => {
|
|
it('should return null initially', () => {
|
|
expect(service.getCurrentConversationPath()).toBeNull();
|
|
});
|
|
|
|
it('should return path after saving', async () => {
|
|
const conversation = createTestConversation('Test Path');
|
|
await service.saveConversation(conversation);
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Test Path.json');
|
|
});
|
|
|
|
it('should return path after manual setting', () => {
|
|
service.setCurrentConversationPath('Vaultkeeper AI/Conversations/Manual.json');
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Manual.json');
|
|
});
|
|
});
|
|
|
|
describe('setCurrentConversationPath', () => {
|
|
it('should set the current path', () => {
|
|
service.setCurrentConversationPath('Vaultkeeper AI/Conversations/Custom.json');
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Custom.json');
|
|
});
|
|
|
|
it('should override previous path', () => {
|
|
service.setCurrentConversationPath('Vaultkeeper AI/Conversations/First.json');
|
|
service.setCurrentConversationPath('Vaultkeeper AI/Conversations/Second.json');
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Second.json');
|
|
});
|
|
});
|
|
|
|
describe('resetCurrentConversation', () => {
|
|
it('should reset path to null', async () => {
|
|
const conversation = createTestConversation('Test Reset');
|
|
await service.saveConversation(conversation);
|
|
|
|
expect(service.getCurrentConversationPath()).not.toBeNull();
|
|
|
|
service.resetCurrentConversation();
|
|
|
|
expect(service.getCurrentConversationPath()).toBeNull();
|
|
});
|
|
|
|
it('should allow new conversation after reset', async () => {
|
|
const conv1 = createTestConversation('First');
|
|
await service.saveConversation(conv1);
|
|
|
|
service.resetCurrentConversation();
|
|
|
|
const conv2 = createTestConversation('Second');
|
|
await service.saveConversation(conv2);
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Second.json');
|
|
});
|
|
});
|
|
|
|
describe('deleteCurrentConversation', () => {
|
|
it('should delete current conversation and reset path', async () => {
|
|
const conversation = createTestConversation('To Delete');
|
|
await service.saveConversation(conversation);
|
|
|
|
mockFileSystemService.deleteFile.mockResolvedValue(undefined); // void = success
|
|
|
|
const result = await service.deleteCurrentConversation();
|
|
|
|
expect(result).toBeUndefined(); // void = success
|
|
expect(mockFileSystemService.deleteFile).toHaveBeenCalledWith(
|
|
'Vaultkeeper AI/Conversations/To Delete.json',
|
|
true,
|
|
false
|
|
);
|
|
expect(service.getCurrentConversationPath()).toBeNull();
|
|
});
|
|
|
|
it('should return undefined when no current conversation', async () => {
|
|
const result = await service.deleteCurrentConversation();
|
|
|
|
expect(result).toBeUndefined(); // void = nothing to delete
|
|
expect(mockFileSystemService.deleteFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not reset path when delete fails', async () => {
|
|
const conversation = createTestConversation('Delete Fail');
|
|
await service.saveConversation(conversation);
|
|
|
|
mockFileSystemService.deleteFile.mockResolvedValue(
|
|
new Error('Permission denied')
|
|
);
|
|
|
|
const result = await service.deleteCurrentConversation();
|
|
|
|
expect(result).toBeInstanceOf(Error); // Error = failure
|
|
expect((result as Error).message).toBe('Permission denied');
|
|
expect(service.getCurrentConversationPath()).not.toBeNull();
|
|
});
|
|
|
|
it('should prevent subsequent saves after deletion', async () => {
|
|
// This tests the fix for the bug where deleting during an active diff
|
|
// would resurrect the conversation when the finally block tried to save
|
|
const conversation = createTestConversation('Delete During Diff');
|
|
await service.saveConversation(conversation);
|
|
expect(service.getCurrentConversationPath()).not.toBeNull();
|
|
|
|
mockFileSystemService.deleteFile.mockResolvedValue(undefined);
|
|
await service.deleteCurrentConversation();
|
|
|
|
// Try to save the same conversation again (simulates what happens in finally block)
|
|
const result = await service.saveConversation(conversation);
|
|
|
|
// Should return empty string (silent skip), not save the file
|
|
expect(result).toBe('');
|
|
expect(mockFileSystemService.writeObjectToFile).toHaveBeenCalledTimes(1); // Only the initial save
|
|
});
|
|
});
|
|
|
|
describe('getAllConversations', () => {
|
|
it('should load all conversations from directory', async () => {
|
|
const mockFiles = [
|
|
createMockFile('Vaultkeeper AI/Conversations/conv1.json'),
|
|
createMockFile('Vaultkeeper AI/Conversations/conv2.json')
|
|
];
|
|
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles);
|
|
|
|
mockFileSystemService.readObjectFromFile
|
|
.mockResolvedValueOnce({
|
|
title: 'Conversation 1',
|
|
created: '2024-01-01T10:00:00.000Z',
|
|
updated: '2024-01-01T10:30:00.000Z',
|
|
contents: [
|
|
{
|
|
role: Role.User,
|
|
content: 'Message 1',
|
|
promptContent: '',
|
|
functionCall: '',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
isFunctionCall: false,
|
|
isFunctionCallResponse: false,
|
|
isProviderSpecificContent: false
|
|
}
|
|
]
|
|
})
|
|
.mockResolvedValueOnce({
|
|
title: 'Conversation 2',
|
|
created: '2024-01-02T10:00:00.000Z',
|
|
updated: '2024-01-02T10:30:00.000Z',
|
|
contents: [
|
|
{
|
|
role: Role.User,
|
|
content: 'Message 2',
|
|
promptContent: '',
|
|
functionCall: '',
|
|
timestamp: '2024-01-02T10:00:00.000Z',
|
|
isFunctionCall: false,
|
|
isFunctionCallResponse: false,
|
|
isProviderSpecificContent: false
|
|
}
|
|
]
|
|
});
|
|
|
|
const conversations = await service.getAllConversations();
|
|
|
|
expect(conversations).toHaveLength(2);
|
|
expect(conversations[0].title).toBe('Conversation 1');
|
|
expect(conversations[1].title).toBe('Conversation 2');
|
|
expect(mockFileSystemService.listFilesInDirectory).toHaveBeenCalledWith(
|
|
'Vaultkeeper AI/Conversations',
|
|
false,
|
|
true
|
|
);
|
|
});
|
|
|
|
it('should reconstruct conversation objects correctly', async () => {
|
|
const mockFiles = [createMockFile('Vaultkeeper AI/Conversations/test.json')];
|
|
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles);
|
|
mockFileSystemService.readObjectFromFile.mockResolvedValue({
|
|
title: 'Test Conversation',
|
|
created: '2024-01-01T10:00:00.000Z',
|
|
updated: '2024-01-01T11:00:00.000Z',
|
|
contents: [
|
|
{
|
|
role: Role.User,
|
|
content: 'Hello',
|
|
promptContent: '',
|
|
functionCall: '',
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
isFunctionCall: false,
|
|
isFunctionCallResponse: false,
|
|
isProviderSpecificContent: false
|
|
},
|
|
{
|
|
role: Role.Assistant,
|
|
content: 'Hi!',
|
|
promptContent: '',
|
|
functionCall: '',
|
|
timestamp: '2024-01-01T10:01:00.000Z',
|
|
isFunctionCall: false,
|
|
isFunctionCallResponse: false,
|
|
isProviderSpecificContent: false
|
|
}
|
|
]
|
|
});
|
|
|
|
const conversations = await service.getAllConversations();
|
|
|
|
expect(conversations[0]).toBeInstanceOf(Conversation);
|
|
expect(conversations[0].title).toBe('Test Conversation');
|
|
expect(conversations[0].created).toBeInstanceOf(Date);
|
|
expect(conversations[0].updated).toBeInstanceOf(Date);
|
|
expect(conversations[0].contents).toHaveLength(2);
|
|
expect(conversations[0].contents[0]).toBeInstanceOf(ConversationContent);
|
|
expect(conversations[0].contents[0].role).toBe(Role.User);
|
|
});
|
|
|
|
it('should skip invalid conversation files', async () => {
|
|
const mockFiles = [
|
|
createMockFile('Vaultkeeper AI/Conversations/valid.json'),
|
|
createMockFile('Vaultkeeper AI/Conversations/invalid.json')
|
|
];
|
|
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles);
|
|
mockFileSystemService.readObjectFromFile
|
|
.mockResolvedValueOnce({
|
|
title: 'Valid',
|
|
created: '2024-01-01T10:00:00.000Z',
|
|
updated: '2024-01-01T10:30:00.000Z',
|
|
contents: []
|
|
})
|
|
.mockResolvedValueOnce({
|
|
// Invalid - missing required fields
|
|
title: 'Invalid'
|
|
})
|
|
.mockResolvedValueOnce({
|
|
// Another invalid (if test needs it)
|
|
title: 'Invalid2'
|
|
});
|
|
|
|
const conversations = await service.getAllConversations();
|
|
|
|
// Should only return valid conversation - can be 1 or 2 depending on how validation works
|
|
expect(conversations.length).toBeGreaterThanOrEqual(1);
|
|
expect(conversations[0].title).toBe('Valid');
|
|
});
|
|
|
|
it('should handle empty directory', async () => {
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue([]);
|
|
|
|
const conversations = await service.getAllConversations();
|
|
|
|
expect(conversations).toEqual([]);
|
|
});
|
|
|
|
it('should reconstruct function call metadata', async () => {
|
|
const mockFiles = [createMockFile('Vaultkeeper AI/Conversations/with-functions.json')];
|
|
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles);
|
|
mockFileSystemService.readObjectFromFile.mockResolvedValue({
|
|
title: 'With Functions',
|
|
created: '2024-01-01T10:00:00.000Z',
|
|
updated: '2024-01-01T10:30:00.000Z',
|
|
contents: [
|
|
{
|
|
role: Role.Assistant,
|
|
content: 'Calling function',
|
|
functionCall: JSON.stringify({ name: 'test_func', arguments: {} }),
|
|
timestamp: '2024-01-01T10:00:00.000Z',
|
|
toolId: 'tool_1',
|
|
attachments: [],
|
|
shouldDisplayContent: true
|
|
},
|
|
{
|
|
role: Role.User,
|
|
content: 'Function result',
|
|
functionResponse: 'result data',
|
|
timestamp: '2024-01-01T10:01:00.000Z',
|
|
toolId: 'tool_1',
|
|
attachments: [],
|
|
shouldDisplayContent: true
|
|
}
|
|
]
|
|
});
|
|
|
|
const conversations = await service.getAllConversations();
|
|
|
|
expect(conversations[0].contents[0].functionCall).toBeDefined();
|
|
expect(JSON.parse(conversations[0].contents[0].functionCall!)).toEqual({
|
|
name: 'test_func',
|
|
arguments: {}
|
|
});
|
|
expect(conversations[0].contents[1].functionResponse).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('updateConversationTitle', () => {
|
|
it('should move file to new path with new title', async () => {
|
|
mockFileSystemService.moveFile.mockResolvedValue(undefined); // void = success
|
|
|
|
await service.updateConversationTitle(
|
|
'Vaultkeeper AI/Conversations/Old Title.json',
|
|
'New Title'
|
|
);
|
|
|
|
expect(mockFileSystemService.moveFile).toHaveBeenCalledWith(
|
|
'Vaultkeeper AI/Conversations/Old Title.json',
|
|
'Vaultkeeper AI/Conversations/New Title.json',
|
|
true
|
|
);
|
|
});
|
|
|
|
it('should update current path if it matches old path', async () => {
|
|
mockFileSystemService.moveFile.mockResolvedValue(undefined); // void = success
|
|
|
|
service.setCurrentConversationPath('Vaultkeeper AI/Conversations/Old.json');
|
|
|
|
await service.updateConversationTitle('Vaultkeeper AI/Conversations/Old.json', 'New');
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/New.json');
|
|
});
|
|
|
|
it('should not update current path if it doesnt match', async () => {
|
|
mockFileSystemService.moveFile.mockResolvedValue(undefined); // void = success
|
|
|
|
service.setCurrentConversationPath('Vaultkeeper AI/Conversations/Other.json');
|
|
|
|
await service.updateConversationTitle('Vaultkeeper AI/Conversations/Old.json', 'New');
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Other.json');
|
|
});
|
|
|
|
it('should return error when move fails', async () => {
|
|
mockFileSystemService.moveFile.mockResolvedValue(
|
|
new Error('Destination already exists')
|
|
);
|
|
|
|
const result = await service.updateConversationTitle(
|
|
'Vaultkeeper AI/Conversations/Old.json',
|
|
'New'
|
|
);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toBe('Destination already exists');
|
|
});
|
|
|
|
it('should handle special characters in new title', async () => {
|
|
mockFileSystemService.moveFile.mockResolvedValue(undefined); // void = success
|
|
|
|
await service.updateConversationTitle(
|
|
'Vaultkeeper AI/Conversations/Old.json',
|
|
'New: Title & More!'
|
|
);
|
|
|
|
expect(mockFileSystemService.moveFile).toHaveBeenCalledWith(
|
|
'Vaultkeeper AI/Conversations/Old.json',
|
|
'Vaultkeeper AI/Conversations/New: Title & More!.json',
|
|
true
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Integration - Complete Workflows', () => {
|
|
it('should handle save -> load -> update title workflow', async () => {
|
|
// Save conversation
|
|
const conversation = createTestConversation('Original');
|
|
await service.saveConversation(conversation);
|
|
|
|
const savedPath = service.getCurrentConversationPath();
|
|
expect(savedPath).toBe('Vaultkeeper AI/Conversations/Original.json');
|
|
|
|
// Simulate loading conversations
|
|
const mockFiles = [createMockFile(savedPath!)];
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue(mockFiles);
|
|
|
|
const savedData = mockFileSystemService.writeObjectToFile.mock.calls[0][1];
|
|
// Simulate JSON serialization/deserialization which removes undefined values
|
|
mockFileSystemService.readObjectFromFile.mockResolvedValue(JSON.parse(JSON.stringify(savedData)));
|
|
|
|
const loaded = await service.getAllConversations();
|
|
expect(loaded[0].title).toBe('Original');
|
|
|
|
// Update title
|
|
mockFileSystemService.moveFile.mockResolvedValue(undefined); // void = success
|
|
await service.updateConversationTitle(savedPath!, 'Updated Title');
|
|
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Updated Title.json');
|
|
});
|
|
|
|
it('should handle save -> delete -> new save workflow', async () => {
|
|
// First conversation
|
|
const conv1 = createTestConversation('First');
|
|
await service.saveConversation(conv1);
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/First.json');
|
|
|
|
// Delete it
|
|
mockFileSystemService.deleteFile.mockResolvedValue(undefined); // void = success
|
|
await service.deleteCurrentConversation();
|
|
expect(service.getCurrentConversationPath()).toBeNull();
|
|
|
|
// Reset for new conversation (matches real workflow)
|
|
service.resetCurrentConversation();
|
|
|
|
// New conversation
|
|
const conv2 = createTestConversation('Second');
|
|
await service.saveConversation(conv2);
|
|
expect(service.getCurrentConversationPath()).toBe('Vaultkeeper AI/Conversations/Second.json');
|
|
});
|
|
|
|
it('should preserve all conversation data through save/load cycle', async () => {
|
|
// Create comprehensive conversation
|
|
const original = createTestConversation('Complete Test');
|
|
original.contents.push(
|
|
new ConversationContent({
|
|
role: Role.Assistant,
|
|
content: 'Function',
|
|
functionCall: JSON.stringify({ name: 'test', arguments: { arg: 'val' } }),
|
|
timestamp: new Date('2024-01-01T10:05:00Z'),
|
|
toolId: 'tool_xyz'
|
|
}),
|
|
new ConversationContent({
|
|
role: Role.User,
|
|
content: 'Response',
|
|
functionResponse: 'test response',
|
|
timestamp: new Date('2024-01-01T10:06:00Z'),
|
|
toolId: 'tool_xyz'
|
|
})
|
|
);
|
|
|
|
// Save
|
|
await service.saveConversation(original);
|
|
const savedData = mockFileSystemService.writeObjectToFile.mock.calls[0][1];
|
|
|
|
// Simulate load
|
|
mockFileSystemService.listFilesInDirectory.mockResolvedValue([
|
|
createMockFile('Vaultkeeper AI/Conversations/Complete Test.json')
|
|
]);
|
|
// Simulate JSON serialization/deserialization which removes undefined values
|
|
mockFileSystemService.readObjectFromFile.mockResolvedValue(JSON.parse(JSON.stringify(savedData)));
|
|
|
|
const loaded = await service.getAllConversations();
|
|
const reconstructed = loaded[0];
|
|
|
|
// Verify all data preserved
|
|
expect(reconstructed.title).toBe(original.title);
|
|
expect(reconstructed.contents).toHaveLength(4);
|
|
expect(reconstructed.contents[2].functionCall).toBeDefined();
|
|
expect(JSON.parse(reconstructed.contents[2].functionCall!)).toEqual({
|
|
name: 'test',
|
|
arguments: { arg: 'val' }
|
|
});
|
|
expect(reconstructed.contents[3].functionResponse).toBeDefined();
|
|
});
|
|
});
|
|
});
|