/** * Tests for Full Replacement plugin duration mechanism * Tests that expired messages (with duration) are filtered out from AI context * Based on real configuration from defaultAgents.json */ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { AgentInstanceMessage } from '../../interface'; import type { IPromptConcatPlugin } from '../../promptConcat/promptConcatSchema'; import type { IPrompt } from '../../promptConcat/promptConcatSchema/prompts'; import { cloneDeep } from 'lodash'; import defaultAgents from '../../buildInAgentHandlers/defaultAgents.json'; import { createHandlerHooks, PromptConcatHookContext } from '../index'; import { fullReplacementPlugin } from '../promptPlugins'; // Use the real agent config const exampleAgent = defaultAgents[0]; const realHandlerConfig = exampleAgent.handlerConfig; describe('Full Replacement Plugin - Duration Mechanism', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('History Source Type with Duration Filtering', () => { it('should filter out expired messages (duration=1) from historyOfSession', async () => { // Find the real fullReplacement plugin for history from defaultAgents.json const historyPlugin = realHandlerConfig.plugins.find( p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession', ); expect(historyPlugin).toBeDefined(); expect(historyPlugin!.fullReplacementParam!.targetId).toBe('default-history'); // Real target ID // Use real prompts structure from defaultAgents.json const testPrompts = cloneDeep(realHandlerConfig.prompts) as IPrompt[]; const messages: AgentInstanceMessage[] = [ // Message 0: User message, no duration - should be included { id: 'user-msg-1', role: 'user' as const, content: 'Hello, help me search for something', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // No duration, should be included }, // Message 1: AI tool call message, duration=1 - should be filtered out { id: 'ai-tool-call-msg', role: 'assistant' as const, content: '{"workspaceName": "Test Wiki", "filter": "[tag[test]]"}', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: 1, // Should be filtered out because it's expired metadata: { containsToolCall: true, toolId: 'wiki-search', }, }, // Message 2: Tool result message, duration=1 - should be filtered out { id: 'tool-result-msg', role: 'user' as const, content: 'Tool: wiki-search\\nResult: Found some content', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: 1, // Should be filtered out because it's expired metadata: { isToolResult: true, toolId: 'wiki-search', }, }, // Message 3: AI response after tool - should be included { id: 'ai-response-msg', role: 'assistant' as const, content: 'Based on the search results, here is the information you requested...', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // No duration, should be included }, // Message 4: Latest user message - should be included but will be removed by fullReplacement { id: 'user-msg-2', role: 'user' as const, content: 'Can you tell me more about this?', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // No duration, should be included }, ]; const context: PromptConcatHookContext = { handlerContext: { agent: { id: 'test-agent', messages, agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date() }, created: new Date(), }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }, pluginConfig: historyPlugin! as unknown as IPromptConcatPlugin, // Type cast due to JSON import limitations prompts: testPrompts, messages, }; const hooks = createHandlerHooks(); fullReplacementPlugin(hooks); // Execute the processPrompts hook await hooks.processPrompts.promise(context); // Find the target prompt that should be replaced (using real target ID from config) const targetId = historyPlugin!.fullReplacementParam!.targetId; // 'default-history' const historyPrompt = testPrompts.find(p => p.id === 'history'); expect(historyPrompt).toBeDefined(); const targetPrompt = historyPrompt!.children?.find(child => child.id === targetId); expect(targetPrompt).toBeDefined(); // The fullReplacementPlugin puts filtered messages in children array // Note: fullReplacementPlugin removes the last message (current user message) const children = (targetPrompt as unknown as { children?: IPrompt[] }).children || []; expect(children.length).toBe(2); // Only non-expired messages (user1, ai-response), excluding last user message // Check the content of the children const childrenText = children.map((child: IPrompt) => child.text || '').join(' '); // Should include user messages without duration (except the last one which is removed) expect(childrenText).toContain('Hello, help me search for something'); // Note: "Can you tell me more about this?" is the last message and gets removed by fullReplacement // Should include AI response without duration expect(childrenText).toContain('Based on the search results, here is the information'); // Should NOT include expired messages (duration=1) expect(childrenText).not.toContain(''); expect(childrenText).not.toContain(''); }); it('should include messages with duration=0 (visible in current round)', async () => { const historyPlugin = realHandlerConfig.plugins.find( p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession', ); const messages: AgentInstanceMessage[] = [ { id: 'user-msg-1', role: 'user' as const, content: 'First message', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, }, { id: 'ai-msg-1', role: 'assistant' as const, content: 'AI response with duration 0', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: 0, // Excluded from AI context but still in current round }, { id: 'user-msg-2', role: 'user' as const, content: 'Latest message', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, }, ]; const testPrompts = cloneDeep(realHandlerConfig.prompts) as IPrompt[]; const context: PromptConcatHookContext = { handlerContext: { agent: { id: 'test-agent', messages, agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date() }, created: new Date(), }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }, pluginConfig: historyPlugin! as unknown as IPromptConcatPlugin, // Type cast for JSON import prompts: testPrompts, messages, }; const hooks = createHandlerHooks(); fullReplacementPlugin(hooks); await hooks.processPrompts.promise(context); const targetId = historyPlugin!.fullReplacementParam!.targetId; const historyPrompt = testPrompts.find(p => p.id === 'history'); const targetPrompt = historyPrompt!.children?.find(child => child.id === targetId); const children = (targetPrompt as unknown as { children?: IPrompt[] }).children || []; const childrenText = children.map((child: IPrompt) => child.text || '').join(' '); // Duration=0 messages are excluded from AI context by filterMessagesByDuration // Last message is also removed by fullReplacement expect(children.length).toBe(1); // Only user1 remains expect(childrenText).toContain('First message'); // AI response with duration=0 should be filtered out expect(childrenText).not.toContain('AI response with duration 0'); }); it('should handle mixed duration values correctly', async () => { const historyPlugin = realHandlerConfig.plugins.find( p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'historyOfSession', ); const messages: AgentInstanceMessage[] = [ { id: 'msg-1', role: 'user' as const, content: 'Message 1 - no duration', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // Should be included }, { id: 'msg-2', role: 'assistant' as const, content: 'Message 2 - duration 3', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: 3, // Should be included (roundsFromCurrent=2 < duration=3) }, { id: 'msg-3', role: 'user' as const, content: 'Message 3 - duration 1', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: 1, // Should be included since roundsFromCurrent(0) < duration(1) }, { id: 'msg-4', role: 'user' as const, // Changed to user so it gets removed by fullReplacement content: 'Message 4 - latest user message', agentId: 'test-agent', contentType: 'text/plain', modified: new Date(), duration: undefined, // Will be removed as last user message }, ]; const testPrompts = cloneDeep(realHandlerConfig.prompts) as IPrompt[]; const context: PromptConcatHookContext = { handlerContext: { agent: { id: 'test-agent', messages, agentDefId: 'test-agent-def', status: { state: 'working' as const, modified: new Date() }, created: new Date(), }, agentDef: { id: 'test-agent-def', name: 'test', handlerConfig: {} }, isCancelled: () => false, }, pluginConfig: historyPlugin! as unknown as IPromptConcatPlugin, // Type cast for JSON import prompts: testPrompts, messages, }; const hooks = createHandlerHooks(); fullReplacementPlugin(hooks); await hooks.processPrompts.promise(context); const targetId = historyPlugin!.fullReplacementParam!.targetId; const historyPrompt = testPrompts.find(p => p.id === 'history'); const targetPrompt = historyPrompt!.children?.find(child => child.id === targetId); const children = (targetPrompt as unknown as { children?: IPrompt[] }).children || []; const childrenText = children.map((child: IPrompt) => child.text || '').join(' '); // Should include messages without duration and with duration values that haven't expired // Note: last message (msg-4) is removed by fullReplacement expect(children.length).toBe(3); // msg-1, msg-2, msg-3 (all within their duration windows) expect(childrenText).toContain('Message 1 - no duration'); expect(childrenText).toContain('Message 2 - duration 3'); expect(childrenText).toContain('Message 3 - duration 1'); // roundsFromCurrent(0) < duration(1) // Last message should be removed by fullReplacement (only if it's a user message) expect(childrenText).not.toContain('Message 4 - latest user message'); }); }); describe('LLM Response Source Type', () => { it('should verify LLM response replacement config exists', () => { // Verify the real config has LLM response replacement const llmResponsePlugin = realHandlerConfig.plugins.find( p => p.pluginId === 'fullReplacement' && p.fullReplacementParam?.sourceType === 'llmResponse', ); expect(llmResponsePlugin).toBeDefined(); expect(llmResponsePlugin!.fullReplacementParam!.targetId).toBe('default-response'); }); }); });