/** * Comprehensive tests for Wiki Search plugin * Covers tool list injection, tool execution, duration mechanism, message persistence, and integration scenarios */ import type { AgentDefinition } from '@services/agentDefinition/interface'; import type { IWikiService } from '@services/wiki/interface'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { AgentInstance } from '../../interface'; import type { AgentInstanceMessage } from '../../interface'; import type { IAgentInstanceService } from '../../interface'; import type { AIResponseContext, YieldNextRoundTarget } from '../types'; import { WikiChannel } from '@/constants/channels'; import serviceIdentifier from '@services/serviceIdentifier'; import type { AgentHandlerContext } from '@services/agentInstance/buildInAgentHandlers/type'; import { AgentPromptDescription } from '@services/agentInstance/promptConcat/promptConcatSchema'; import type { IPrompt } from '@services/agentInstance/promptConcat/promptConcatSchema'; import type { IPromptConcatPlugin } from '@services/agentInstance/promptConcat/promptConcatSchema'; import { cloneDeep } from 'lodash'; import defaultAgents from '../../buildInAgentHandlers/defaultAgents.json'; import { createHandlerHooks, PromptConcatHookContext } from '../index'; import { messageManagementPlugin } from '../messageManagementPlugin'; import { wikiSearchPlugin } from '../wikiSearchPlugin'; // Mock i18n vi.mock('@services/libs/i18n', () => ({ i18n: { t: vi.fn((key: string, options?: Record) => { const translations: Record = { 'Tool.WikiSearch.Success.NoResults': '在Wiki工作空间"{{workspaceName}}"中未找到过滤器"{{filter}}"的结果', 'Tool.WikiSearch.Success.Completed': 'Wiki搜索完成。找到{{totalResults}}个总结果,显示{{shownResults}}个:\n\n', 'Tool.WikiSearch.Error.WorkspaceNotFound': '工作空间名称或ID"{{workspaceName}}"不存在。可用工作空间:{{availableWorkspaces}}', 'Tool.WikiSearch.Error.WorkspaceNotExist': '工作空间{{workspaceID}}不存在', 'Tool.WikiSearch.Error.ExecutionFailed': '工具执行失败:{{error}}', }; let translation = translations[key] || key; // Handle interpolation if (options && typeof options === 'object') { Object.keys(options).forEach(optionKey => { if (optionKey !== 'ns' && optionKey !== 'defaultValue') { translation = translation.replace(new RegExp(`{{${optionKey}}}`, 'g'), String(options[optionKey])); } }); } return translation; }), }, })); // Use the real agent config const exampleAgent = defaultAgents[0]; const handlerConfig = exampleAgent.handlerConfig as AgentPromptDescription['handlerConfig']; // Services will be retrieved from container on demand inside each test/describe type ActionBag = { yieldNextRoundTo?: YieldNextRoundTarget; newUserMessage?: string | undefined }; describe('Wiki Search Plugin - Comprehensive Tests', () => { beforeEach(async () => { vi.clearAllMocks(); }); describe('Tool List Injection', () => { beforeEach(async () => { const { container } = await import('@services/container'); const agentInstanceService = container.get(serviceIdentifier.AgentInstance); // Replace agent instance methods with spies vi.spyOn(agentInstanceService, 'saveUserMessage').mockResolvedValue(undefined); vi.spyOn(agentInstanceService, 'debounceUpdateMessage').mockImplementation(() => undefined); // updateAgent returns Promise - return a minimal stub vi.spyOn(agentInstanceService, 'updateAgent').mockResolvedValue({} as AgentInstance); }); it('should inject wiki tools into prompts when configured', async () => { // Find the wiki search plugin config, make sure our default config const wikiPlugin = handlerConfig.plugins.find((p: unknown): p is IPromptConcatPlugin => (p as IPromptConcatPlugin).pluginId === 'wikiSearch'); expect(wikiPlugin).toBeDefined(); if (!wikiPlugin) { // throw error to keep ts believe the plugin exists throw new Error('Wiki plugin not found'); } // Verify the plugin has the correct parameter structure expect(wikiPlugin.wikiSearchParam).toBeDefined(); expect(wikiPlugin.wikiSearchParam?.toolListPosition).toBeDefined(); // Create a copy of prompts to test modification const prompts = cloneDeep(handlerConfig.prompts); const messages = [ { id: 'user-1', role: 'user' as const, content: 'Help me search for information in my wiki', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), }, ]; const context: PromptConcatHookContext = { handlerContext: { agent: { id: 'test', messages: [], agentDefId: 'test', status: { state: 'working' as const, modified: new Date() }, created: new Date() }, agentDef: { id: 'test', name: 'test', handlerConfig: {} }, isCancelled: () => false, }, pluginConfig: wikiPlugin, prompts: prompts, messages, }; // Use real hooks from the plugin system const promptHooks = createHandlerHooks(); wikiSearchPlugin(promptHooks); // Execute the processPrompts hook await promptHooks.processPrompts.promise(context); // Verify that tool information was injected into the prompts const promptTexts = JSON.stringify(prompts); expect(promptTexts).toContain('wiki-search'); expect(promptTexts).toContain('workspaceName'); expect(promptTexts).toContain('filter'); }); it('should skip injection when trigger condition is not met', async () => { // Create a plugin config with trigger that won't match const wikiPlugin = { id: 'test-wiki-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, retrievalAugmentedGenerationParam: { sourceType: 'wiki' as const, trigger: { search: 'specific-search-term-not-in-message', }, toolListPosition: { position: 'after' as const, targetId: 'default-before-tool', }, }, }; const prompts = cloneDeep(defaultAgents[0].handlerConfig.prompts); const originalPromptsText = JSON.stringify(prompts); const context = { pluginConfig: wikiPlugin, prompts, messages: [ { id: 'user-1', role: 'user' as const, content: 'Hello, how are you?', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), }, ], }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); // build a minimal PromptConcatHookContext to run the plugin's processPrompts const handlerCtx: AgentHandlerContext = { agent: { id: 'test', agentDefId: 'test', messages: [], status: { state: 'working' as const, modified: new Date() }, created: new Date(), } as AgentInstance, agentDef: { id: 'test', name: 'test', handlerConfig: {} } as AgentDefinition, isCancelled: () => false, }; const hookContext: PromptConcatHookContext = { handlerContext: handlerCtx, pluginConfig: wikiPlugin as IPromptConcatPlugin, prompts: prompts as IPrompt[], messages: context.messages as AgentInstanceMessage[], }; await hooks.processPrompts.promise(hookContext); // Prompts should not be modified since trigger condition wasn't met const modifiedPromptsText = JSON.stringify(prompts); expect(modifiedPromptsText).toBe(originalPromptsText); }); }); describe('Tool Execution & Duration Mechanism', () => { beforeEach(async () => { // Mock wiki search results const { container } = await import('@services/container'); const wikiService = container.get(serviceIdentifier.Wiki); if (!wikiService.wikiOperationInServer) { // ensure method exists for spying (exception allowed) (wikiService as unknown as { wikiOperationInServer: (...p: unknown[]) => Promise }).wikiOperationInServer = async () => []; } vi.spyOn(wikiService, 'wikiOperationInServer').mockImplementation( ((...args: unknown[]) => { const channel = args[0] as WikiChannel; const opArgs = args[2] as string[] | undefined; if (channel === WikiChannel.runFilter) { return Promise.resolve(['Important Note 1', 'Important Note 2']); } if (channel === WikiChannel.getTiddlersAsJson && opArgs && opArgs.length > 0) { const title = opArgs[0]; return Promise.resolve([ { title, text: `Content of ${title}: This contains important information.`, tags: ['important'], }, ]); } return Promise.resolve([]); }) as unknown as IWikiService['wikiOperationInServer'], ); }); it('should execute wiki search with correct duration=1 and trigger next round', async () => { // Find the real wikiSearch plugin config from defaultAgents.json const wikiPlugin = handlerConfig.plugins.find((p: unknown): p is IPromptConcatPlugin => (p as IPromptConcatPlugin).pluginId === 'wikiSearch'); expect(wikiPlugin).toBeDefined(); expect(wikiPlugin!.wikiSearchParam).toBeDefined(); const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ // Previous user message { id: 'user-msg-1', role: 'user' as const, content: 'Search for information', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // Should stay visible }, // AI tool call message (this is the message we're testing) { id: 'ai-tool-call-msg', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki 1", "filter": "[tag[important]]"}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // Should be set to 1 after tool execution }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; // Create a response that contains a valid tool call const response = { status: 'done' as const, content: '{"workspaceName": "Test Wiki 1", "filter": "[tag[important]]"}', requestId: 'test-request-123', }; const context = { handlerContext, response, requestId: 'test-request-123', isFinal: true, pluginConfig: wikiPlugin!, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: {} as ActionBag, }; // Use real handler hooks const hooks = createHandlerHooks(); // Register the plugin wikiSearchPlugin(hooks); // Execute the response complete hook await hooks.responseComplete.promise(context); // Verify that the search was executed and results were set up for next round expect(context.actions.yieldNextRoundTo).toBe('self'); // Verify that debounceUpdateMessage was called to notify frontend immediately (no delay) const { container } = await import('@services/container'); const agentInstanceService = container.get(serviceIdentifier.AgentInstance); expect(agentInstanceService.debounceUpdateMessage).toHaveBeenCalledWith( expect.objectContaining({ id: 'ai-tool-call-msg', duration: 1, }), 'test-agent', 0, // No delay for immediate update ); // Check that AI tool call message now has duration=1 (should gray out immediately) const aiToolCallMessage = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(aiToolCallMessage.id).toBe('ai-tool-call-msg'); expect(aiToolCallMessage.duration).toBe(1); // Should be 1 to gray out immediately expect(aiToolCallMessage.metadata?.containsToolCall).toBe(true); expect(aiToolCallMessage.metadata?.toolId).toBe('wiki-search'); // Verify tool result message was added to agent history with correct settings expect(handlerContext.agent.messages.length).toBe(3); // user + ai + tool_result const toolResultMessage = handlerContext.agent.messages[2] as AgentInstanceMessage; expect(toolResultMessage.role).toBe('tool'); // Tool result message expect(toolResultMessage.content).toContain(''); expect(toolResultMessage.content).toContain('Tool: wiki-search'); expect(toolResultMessage.content).toContain('Important Note 1'); expect(toolResultMessage.metadata?.isToolResult).toBe(true); expect(toolResultMessage.metadata?.isPersisted).toBe(false); // Should be false initially expect(toolResultMessage.duration).toBe(1); // Tool result uses configurable toolResultDuration (default 1) // Check that previous user message is unchanged const userMessage = handlerContext.agent.messages[0] as AgentInstanceMessage; expect(userMessage.id).toBe('user-msg-1'); expect(userMessage.duration).toBeUndefined(); // Should stay visible }); it('should handle wiki search errors gracefully and set duration=1 for both messages', async () => { const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ { id: 'ai-error-tool-call', role: 'assistant' as const, content: '{"workspaceName": "Nonexistent Wiki", "filter": "[tag[test]]"}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // Should be set to 1 after error handling }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; // Tool call with nonexistent workspace const response = { status: 'done' as const, content: '{"workspaceName": "Nonexistent Wiki", "filter": "[tag[test]]"}', requestId: 'test-request-error', }; const context = { handlerContext, response, requestId: 'test-request-error', isFinal: true, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, }, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: { yieldNextRoundTo: undefined, newUserMessage: undefined, }, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); await hooks.responseComplete.promise(context); // Should still set up next round with error message expect(context.actions.yieldNextRoundTo).toBe('self'); // Check that AI tool call message has duration=1 even after error (should gray out immediately) const aiToolCallMessage = handlerContext.agent.messages[0] as AgentInstanceMessage; expect(aiToolCallMessage.id).toBe('ai-error-tool-call'); expect(aiToolCallMessage.duration).toBe(1); // Should be 1 to gray out immediately expect(aiToolCallMessage.metadata?.containsToolCall).toBe(true); // Verify error message was added to agent history expect(handlerContext.agent.messages.length).toBe(2); // tool_call + error_result const errorResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(errorResultMessage.role).toBe('tool'); // Tool error message expect(errorResultMessage.content).toContain(''); expect(errorResultMessage.content).toContain('Error:'); expect(errorResultMessage.content).toContain('工作空间名称或ID'); expect(errorResultMessage.metadata?.isToolResult).toBe(true); expect(errorResultMessage.metadata?.isError).toBe(true); expect(errorResultMessage.metadata?.isPersisted).toBe(false); // Should be false initially expect(errorResultMessage.duration).toBe(1); // Now uses configurable toolResultDuration (default 1) }); it('should not modify duration of unrelated messages', async () => { const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ { id: 'unrelated-user-msg', role: 'user' as const, content: 'This is an unrelated message', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: 5, // Should remain unchanged }, { id: 'unrelated-ai-msg', role: 'assistant' as const, content: 'This is a regular AI response without tool calls', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // Should remain unchanged }, { id: 'ai-tool-call-msg', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki 1", "filter": "[tag[test]]"}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // This should be modified to 1 }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; const response = { status: 'done' as const, content: '{"workspaceName": "Test Wiki 1", "filter": "[tag[test]]"}', requestId: 'test-request-selective', }; const context = { handlerContext, response, requestId: 'test-request-selective', isFinal: true, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, }, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: { yieldNextRoundTo: undefined, newUserMessage: undefined, }, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); await hooks.responseComplete.promise(context); // Check that unrelated messages were not modified const unrelatedUserMsg = handlerContext.agent.messages[0] as AgentInstanceMessage; expect(unrelatedUserMsg.duration).toBe(5); // Should remain unchanged const unrelatedAiMsg = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(unrelatedAiMsg.duration).toBeUndefined(); // Should remain unchanged // Check that only the tool call message was modified const toolCallMsg = handlerContext.agent.messages[2] as AgentInstanceMessage; expect(toolCallMsg.duration).toBe(1); // Should be set to 1 expect(toolCallMsg.metadata?.containsToolCall).toBe(true); }); it('should skip execution when no tool call is detected', async () => { const handlerCtx = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', messages: [], status: { state: 'working' as const, modified: new Date() }, created: new Date(), } as AgentInstance, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} } as AgentDefinition, isCancelled: () => false, }; const context: AIResponseContext = { handlerContext: handlerCtx, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' } as IPromptConcatPlugin, response: { requestId: 'test-request-345', content: 'Just a regular response without any tool calls', status: 'done' }, requestId: 'test-request', isFinal: true, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); await hooks.responseComplete.promise(context); // Context should not be modified expect(context.actions).toBeUndefined(); }); }); describe('Vector Search Functionality', () => { beforeEach(async () => { const { container } = await import('@services/container'); // Mock WikiEmbeddingService const mockWikiEmbeddingService = { searchSimilar: vi.fn(), }; // Replace the service in container if (container.isBound(serviceIdentifier.WikiEmbedding)) { await container.unbind(serviceIdentifier.WikiEmbedding); } container.bind(serviceIdentifier.WikiEmbedding).toConstantValue(mockWikiEmbeddingService); }); it('should execute vector search when searchType=vector', async () => { const { container } = await import('@services/container'); const wikiEmbeddingService = container.get(serviceIdentifier.WikiEmbedding); // Mock vector search results (wikiEmbeddingService as { searchSimilar: ReturnType }).searchSimilar.mockResolvedValue([ { record: { id: 1, workspaceId: 'test-workspace', tiddlerTitle: 'Vector Result 1', model: 'test-model', provider: 'test-provider', dimensions: 384, created: new Date(), modified: new Date(), }, similarity: 0.95, }, { record: { id: 2, workspaceId: 'test-workspace', tiddlerTitle: 'Vector Result 2', model: 'test-model', provider: 'test-provider', dimensions: 384, created: new Date(), modified: new Date(), }, similarity: 0.85, }, ]); // Mock wiki service to return full tiddler content const wikiService = container.get(serviceIdentifier.Wiki); vi.spyOn(wikiService, 'wikiOperationInServer').mockImplementation( ((...args: unknown[]) => { const channel = args[0] as WikiChannel; const opArgs = args[2] as string[] | undefined; if (channel === WikiChannel.getTiddlersAsJson && opArgs && opArgs.length > 0) { const title = opArgs[0]; return Promise.resolve([ { title, text: `Full content of ${title}`, tags: ['test'], }, ]); } return Promise.resolve([]); }) as unknown as IWikiService['wikiOperationInServer'], ); const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', aiApiConfig: { api: { provider: 'openai', model: 'gpt-4', embeddingModel: 'text-embedding-ada-002', }, modelParameters: {}, }, status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ { id: 'ai-vector-tool-call', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki 1", "searchType": "vector", "query": "How to use AI agents", "limit": 10, "threshold": 0.7}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; const response = { status: 'done' as const, content: '{"workspaceName": "Test Wiki 1", "searchType": "vector", "query": "How to use AI agents", "limit": 10, "threshold": 0.7}', requestId: 'test-request-vector', }; const context = { handlerContext, response, requestId: 'test-request-vector', isFinal: true, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, }, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: {} as ActionBag, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); await hooks.responseComplete.promise(context); // Verify vector search was called const mockService = wikiEmbeddingService as { searchSimilar: ReturnType }; expect(mockService.searchSimilar).toHaveBeenCalledWith( expect.any(String), // workspaceID 'How to use AI agents', expect.objectContaining({ api: expect.objectContaining({ provider: 'openai', model: 'gpt-4', }), }), 10, 0.7, ); // Verify results were processed expect(context.actions.yieldNextRoundTo).toBe('self'); expect(handlerContext.agent.messages.length).toBe(2); const toolResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(toolResultMessage.content).toContain(''); expect(toolResultMessage.content).toContain('Vector Result 1'); expect(toolResultMessage.content).toContain('Vector Result 2'); expect(toolResultMessage.content).toContain('Similarity:'); expect(toolResultMessage.content).toContain('95.0%'); expect(toolResultMessage.content).toContain('85.0%'); }); it('should handle vector search errors gracefully', async () => { const { container } = await import('@services/container'); const wikiEmbeddingService = container.get(serviceIdentifier.WikiEmbedding); // Mock vector search to throw error (wikiEmbeddingService as { searchSimilar: ReturnType }).searchSimilar.mockRejectedValue( new Error('Vector database not initialized'), ); const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', aiApiConfig: { api: { provider: 'openai', model: 'gpt-4', }, modelParameters: {}, }, status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ { id: 'ai-vector-error-call', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki 1", "searchType": "vector", "query": "test query", "limit": 10, "threshold": 0.7}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; const response = { status: 'done' as const, content: '{"workspaceName": "Test Wiki 1", "searchType": "vector", "query": "test query", "limit": 10, "threshold": 0.7}', requestId: 'test-request-vector-error', }; const context = { handlerContext, response, requestId: 'test-request-vector-error', isFinal: true, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, }, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: {} as ActionBag, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); await hooks.responseComplete.promise(context); // Should still set up next round with error message expect(context.actions.yieldNextRoundTo).toBe('self'); const errorResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(errorResultMessage.content).toContain('Error:'); // Error message contains i18n key or actual error expect(errorResultMessage.content).toMatch(/Vector database not initialized|Tool\.WikiSearch\.Error\.VectorSearchFailed/); expect(errorResultMessage.metadata?.isError).toBe(true); }); it('should require query parameter for vector search', async () => { const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', aiApiConfig: { api: { provider: 'openai', model: 'gpt-4', }, modelParameters: {}, }, status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ { id: 'ai-no-query', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki 1", "searchType": "vector", "limit": 10, "threshold": 0.7}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; const response = { status: 'done' as const, content: '{"workspaceName": "Test Wiki 1", "searchType": "vector", "limit": 10, "threshold": 0.7}', requestId: 'test-request-no-query', }; const context = { handlerContext, response, requestId: 'test-request-no-query', isFinal: true, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, }, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: {} as ActionBag, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); await hooks.responseComplete.promise(context); // Should return error about missing query const errorMessage = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(errorMessage.content).toContain('Error:'); // Error message contains i18n key or translated text expect(errorMessage.content).toMatch(/query|Tool\.WikiSearch\.Error\.VectorSearchRequiresQuery/); }); }); describe('Message Persistence Integration', () => { it('should work with messageManagementPlugin for complete persistence flow', async () => { // This test ensures wikiSearchPlugin works well with messageManagementPlugin const handlerContext = { agent: { id: 'test-agent', agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date(), }, created: new Date(), messages: [ { id: 'ai-tool-msg', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki 1", "filter": "[tag[test]]"}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, }, ], }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }; const response = { status: 'done' as const, content: '{"workspaceName": "Test Wiki 1", "filter": "[tag[test]]"}', requestId: 'test-request-integration', }; const context = { handlerContext, response, requestId: 'test-request-integration', isFinal: true, pluginConfig: { id: 'test-plugin', pluginId: 'wikiSearch' as const, forbidOverrides: false, }, prompts: [], messages: [], llmResponse: response.content, responses: [], actions: { yieldNextRoundTo: undefined, newUserMessage: undefined, }, }; const hooks = createHandlerHooks(); wikiSearchPlugin(hooks); messageManagementPlugin(hooks); await hooks.responseComplete.promise(context); // Verify integration works expect(context.actions.yieldNextRoundTo).toBe('self'); expect(handlerContext.agent.messages.length).toBe(2); // original + tool result const toolResultMessage = handlerContext.agent.messages[1] as AgentInstanceMessage; expect(toolResultMessage.metadata?.isToolResult).toBe(true); expect(toolResultMessage.metadata?.isPersisted).toBe(true); // Should be true after messageManagementPlugin processing }); it('should prevent regression: tool result not filtered in second round', async () => { // Root cause test to prevent regression of the original bug // Bug: Tool result messages were filtered out in second round due to duration=1 const messages: AgentInstanceMessage[] = [ { id: 'user-1', agentId: 'test', role: 'user', content: '搜索 wiki 中的 Index 条目并解释', modified: new Date(), duration: undefined, }, { id: 'ai-tool-1', agentId: 'test', role: 'assistant', content: '{workspaceName:"wiki", filter:"[title[Index]]"}', modified: new Date(), duration: 1, metadata: { containsToolCall: true, toolId: 'wiki-search' }, }, { id: 'tool-result-1', agentId: 'test', role: 'assistant', content: '\nTool: wiki-search\nResult: Found Index page...\n', modified: new Date(), duration: 1, metadata: { isToolResult: true, toolId: 'wiki-search' }, }, ]; // Test duration filtering - this is where the bug was const { filterMessagesByDuration } = await import('../../utilities/messageDurationFilter'); const filtered = filterMessagesByDuration(messages); // Root cause: Both tool call and tool result should be included for proper AI context expect(filtered.length).toBe(3); // user + tool call + tool result expect(filtered.some((m: AgentInstanceMessage) => m.metadata?.containsToolCall)).toBe(true); expect(filtered.some((m: AgentInstanceMessage) => m.metadata?.isToolResult)).toBe(true); }); }); });