Refactor error handling, client, and tests for Ollama integration
This commit is contained in:
+338
-11
@@ -28,6 +28,8 @@ jest.mock('obsidian', () => ({
|
||||
const mockSettings: PluginSettings = {
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: 'llama3',
|
||||
vaultSearchLimit: 3,
|
||||
maxMessageHistory: 50,
|
||||
lastIndexTime: 0,
|
||||
};
|
||||
|
||||
@@ -157,37 +159,362 @@ describe('ChatView', () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield { content: 'test' };
|
||||
},
|
||||
} as any);
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'test' };
|
||||
})()
|
||||
);
|
||||
await (view as any).handleUserInput('test');
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
// Verify that messages were added to conversation history
|
||||
expect((view as any).messages.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle empty user input gracefully', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = ' ';
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat');
|
||||
await (view as any).handleUserInput(' ');
|
||||
expect(chatSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle streaming responses and update UI', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'First ' };
|
||||
yield { role: 'assistant', content: 'chunk ' };
|
||||
yield { role: 'assistant', content: 'of response' };
|
||||
})()
|
||||
);
|
||||
await (view as any).handleUserInput('test');
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
// Should have processed all chunks
|
||||
const messages = (view as any).messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
expect(lastMessage.isStreaming).toBe(false);
|
||||
});
|
||||
|
||||
it('should limit conversation history to maxMessageHistory', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
// Mock a stream that returns quickly
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'response' };
|
||||
})()
|
||||
);
|
||||
|
||||
// Add enough messages to exceed maxMessageHistory
|
||||
for (let i = 0; i < 60; i++) {
|
||||
(view as any).messages.push({
|
||||
id: `msg-${i}`,
|
||||
role: 'user',
|
||||
content: `message ${i}`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
await (view as any).handleUserInput('test');
|
||||
|
||||
// Should be limited to maxMessageHistory
|
||||
expect((view as any).messages.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
it('should call vaultIndexer.searchVault with user input', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'search query';
|
||||
|
||||
const searchSpy = jest.spyOn(view['vaultIndexer'], 'searchVault').mockResolvedValue([]);
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'response' };
|
||||
})()
|
||||
);
|
||||
|
||||
await (view as any).handleUserInput('search query');
|
||||
|
||||
expect(searchSpy).toHaveBeenCalledWith('search query', 3); // Should use DEFAULT_VAULT_SEARCH_LIMIT
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors during user input gracefully', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
// Mock a stream that throws an error
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
throw new Error('Network error');
|
||||
})()
|
||||
);
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await (view as any).handleUserInput('test');
|
||||
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Error handling user input:', expect.any(Error));
|
||||
});
|
||||
|
||||
it('should handle empty user input gracefully', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = ' ';
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat');
|
||||
await (view as any).handleUserInput(' ');
|
||||
expect(chatSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle streaming responses and update UI', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'First ' };
|
||||
yield { role: 'assistant', content: 'chunk ' };
|
||||
yield { role: 'assistant', content: 'of response' };
|
||||
})()
|
||||
);
|
||||
await (view as any).handleUserInput('test');
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
// Should have processed all chunks
|
||||
const messages = (view as any).messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
expect(lastMessage.isStreaming).toBe(false);
|
||||
});
|
||||
|
||||
it('should process tool calls with follow-up context', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockResolvedValue({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
role: 'assistant',
|
||||
content: 'test',
|
||||
tool_calls: [{ function: { name: 'create_file', arguments: '{}' } }],
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'tool_1',
|
||||
type: 'function',
|
||||
function: { name: 'create_file', arguments: '{}' },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
} as any);
|
||||
})()
|
||||
);
|
||||
const followUpSpy = jest
|
||||
.spyOn(view['ollamaClient'], 'chat')
|
||||
.mockResolvedValue({ content: ' follow-up' });
|
||||
.mockResolvedValue({ role: 'assistant', content: ' follow-up' });
|
||||
await (view as any).handleUserInput('test');
|
||||
expect(followUpSpy).toHaveBeenCalled();
|
||||
// Verify that tool calls resulted in follow-up messages
|
||||
expect((view as any).messages.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('should limit conversation history to maxMessageHistory', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
// Mock a stream that returns quickly
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'response' };
|
||||
})()
|
||||
);
|
||||
|
||||
// Add enough messages to exceed maxMessageHistory
|
||||
for (let i = 0; i < 60; i++) {
|
||||
(view as any).messages.push({
|
||||
id: `msg-${i}`,
|
||||
role: 'user',
|
||||
content: `message ${i}`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
await (view as any).handleUserInput('test');
|
||||
|
||||
// Should be limited to maxMessageHistory
|
||||
expect((view as any).messages.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
it('should call vaultIndexer.searchVault with user input', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'search query';
|
||||
|
||||
const searchSpy = jest.spyOn(view['vaultIndexer'], 'searchVault').mockResolvedValue([]);
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
yield { role: 'assistant', content: 'response' };
|
||||
})()
|
||||
);
|
||||
|
||||
await (view as any).handleUserInput('search query');
|
||||
|
||||
expect(searchSpy).toHaveBeenCalledWith('search query', 3); // Should use DEFAULT_VAULT_SEARCH_LIMIT
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors during user input gracefully', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
// Mock a stream that throws an error
|
||||
const chatSpy = jest.spyOn(view['ollamaClient'], 'streamChat').mockReturnValue(
|
||||
(async function* () {
|
||||
throw new Error('Network error');
|
||||
})()
|
||||
);
|
||||
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await (view as any).handleUserInput('test');
|
||||
|
||||
expect(chatSpy).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Error handling user input:', expect.any(Error));
|
||||
});
|
||||
|
||||
describe('event handlers', () => {
|
||||
it('should handle send button click', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
|
||||
const handler = view.getSendButtonClickHandler?.bind(view);
|
||||
if (!handler) throw new Error('Handler not available');
|
||||
await handler();
|
||||
|
||||
expect(handleSpy).toHaveBeenCalledWith('test');
|
||||
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle Enter key press in input', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
|
||||
const event = new KeyboardEvent('keydown', { key: 'Enter' }) as any;
|
||||
|
||||
await (view as any).inputKeyDownHandler!(event);
|
||||
|
||||
expect(handleSpy).toHaveBeenCalledWith('test');
|
||||
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
|
||||
});
|
||||
|
||||
it('should not handle Shift+Enter in input', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
|
||||
const event = new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true }) as any;
|
||||
|
||||
await (view as any).inputKeyDownHandler!(event);
|
||||
|
||||
expect(handleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle new chat button click', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
view['newChatButton'] = document.createElement('button');
|
||||
|
||||
const clearSpy = jest.spyOn(view as any, 'clearConversation');
|
||||
const handler = view.getNewChatButtonClickHandler?.bind(view);
|
||||
if (!handler) throw new Error('Handler not available');
|
||||
await handler();
|
||||
|
||||
expect(clearSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('event listeners', () => {
|
||||
it('should setup event listeners on open', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
await view.onOpen();
|
||||
expect(view['sendButtonClickHandler']).not.toBeNull();
|
||||
expect(view['inputKeyDownHandler']).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should remove event listeners on close', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
await view.onOpen();
|
||||
const removeSpy = jest.spyOn(view, 'removeEventListeners' as any);
|
||||
await view.onClose();
|
||||
expect(removeSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('event handlers', () => {
|
||||
it('should handle send button click', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
|
||||
const handler = view.getSendButtonClickHandler?.bind(view);
|
||||
if (!handler) throw new Error('Handler not available');
|
||||
await handler();
|
||||
|
||||
expect(handleSpy).toHaveBeenCalledWith('test');
|
||||
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle Enter key press in input', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
|
||||
const event = new KeyboardEvent('keydown', { key: 'Enter' }) as any;
|
||||
|
||||
await (view as any).inputKeyDownHandler!(event);
|
||||
|
||||
expect(handleSpy).toHaveBeenCalledWith('test');
|
||||
expect((view['inputEl'] as HTMLTextAreaElement).value).toBe('');
|
||||
});
|
||||
|
||||
it('should not handle Shift+Enter in input', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
(view['inputEl'] as HTMLTextAreaElement).value = 'test';
|
||||
|
||||
const handleSpy = jest.spyOn(view as any, 'handleUserInput');
|
||||
const event = new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true }) as any;
|
||||
|
||||
await (view as any).inputKeyDownHandler!(event);
|
||||
|
||||
expect(handleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle new chat button click', async () => {
|
||||
view['sendButton'] = document.createElement('button');
|
||||
view['inputEl'] = document.createElement('textarea');
|
||||
view['newChatButton'] = document.createElement('button');
|
||||
|
||||
const clearSpy = jest.spyOn(view as any, 'clearConversation');
|
||||
|
||||
await (view as any).newChatButtonClickHandler!();
|
||||
|
||||
expect(clearSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('event listeners', () => {
|
||||
|
||||
Reference in New Issue
Block a user