|
1 | 1 | import { describe, it, expect } from 'vitest'; |
| 2 | +import { stripMemoryTags, extractRecallQuery } from './index.js'; |
2 | 3 |
|
3 | | -/** |
4 | | - * Unit tests for the memory feedback loop fix. |
5 | | - * Verifies that <hindsight_memories> and <relevant_memories> tags |
6 | | - * are stripped from content before RETAIN to prevent duplicates. |
7 | | - */ |
8 | | -describe('Memory Tag Stripping', () => { |
9 | | - /** |
10 | | - * Simulates the tag stripping logic from agent_end hook |
11 | | - */ |
12 | | - function stripMemoryTags(content: string): string { |
13 | | - // Strip plugin-injected memory tags to prevent feedback loop |
14 | | - content = content.replace(/<hindsight_memories>[\s\S]*?<\/hindsight_memories>/g, ''); |
15 | | - content = content.replace(/<relevant_memories>[\s\S]*?<\/relevant_memories>/g, ''); |
16 | | - return content; |
17 | | - } |
18 | | - |
19 | | - it('should strip simple hindsight_memories tags', () => { |
20 | | - const input = 'User: Hello\n<hindsight_memories>\nRelevant memories here...\n</hindsight_memories>\nAssistant: How can I help?'; |
21 | | - const expected = 'User: Hello\n\nAssistant: How can I help?'; |
22 | | - const result = stripMemoryTags(input); |
23 | | - expect(result).toBe(expected); |
| 4 | +// --------------------------------------------------------------------------- |
| 5 | +// stripMemoryTags |
| 6 | +// --------------------------------------------------------------------------- |
| 7 | + |
| 8 | +describe('stripMemoryTags', () => { |
| 9 | + it('strips simple hindsight_memories tags', () => { |
| 10 | + const input = |
| 11 | + 'User: Hello\n<hindsight_memories>\nRelevant memories here...\n</hindsight_memories>\nAssistant: How can I help?'; |
| 12 | + expect(stripMemoryTags(input)).toBe('User: Hello\n\nAssistant: How can I help?'); |
24 | 13 | }); |
25 | 14 |
|
26 | | - it('should strip relevant_memories tags', () => { |
| 15 | + it('strips relevant_memories tags', () => { |
27 | 16 | const input = 'Before\n<relevant_memories>\nSome data\n</relevant_memories>\nAfter'; |
28 | | - const expected = 'Before\n\nAfter'; |
29 | | - const result = stripMemoryTags(input); |
30 | | - expect(result).toBe(expected); |
| 17 | + expect(stripMemoryTags(input)).toBe('Before\n\nAfter'); |
31 | 18 | }); |
32 | 19 |
|
33 | | - it('should strip multiple hindsight_memories blocks', () => { |
34 | | - const input = 'Start\n<hindsight_memories>\nBlock 1\n</hindsight_memories>\nMiddle\n<hindsight_memories>\nBlock 2\n</hindsight_memories>\nEnd'; |
35 | | - const expected = 'Start\n\nMiddle\n\nEnd'; |
36 | | - const result = stripMemoryTags(input); |
37 | | - expect(result).toBe(expected); |
| 20 | + it('strips multiple hindsight_memories blocks', () => { |
| 21 | + const input = |
| 22 | + 'Start\n<hindsight_memories>\nBlock 1\n</hindsight_memories>\nMiddle\n<hindsight_memories>\nBlock 2\n</hindsight_memories>\nEnd'; |
| 23 | + expect(stripMemoryTags(input)).toBe('Start\n\nMiddle\n\nEnd'); |
38 | 24 | }); |
39 | 25 |
|
40 | | - it('should handle multiline memory blocks with JSON', () => { |
41 | | - const input = 'User: What is the weather?\n<hindsight_memories>\nRelevant memories:\n{\n "memory": "User likes sunny weather"\n}\n</hindsight_memories>\nAssistant: Let me check'; |
42 | | - const expected = 'User: What is the weather?\n\nAssistant: Let me check'; |
| 26 | + it('handles multiline memory blocks with JSON', () => { |
| 27 | + const input = |
| 28 | + 'User: What is the weather?\n<hindsight_memories>\n[\n {"memory": "User likes sunny weather"}\n]\n</hindsight_memories>\nAssistant: Let me check'; |
43 | 29 | const result = stripMemoryTags(input); |
44 | | - expect(result).toBe(expected); |
| 30 | + expect(result).toBe('User: What is the weather?\n\nAssistant: Let me check'); |
45 | 31 | }); |
46 | 32 |
|
47 | | - it('should preserve content without memory tags', () => { |
| 33 | + it('preserves content without memory tags', () => { |
48 | 34 | const input = 'User: Hello\nAssistant: Hi there!'; |
49 | | - const expected = 'User: Hello\nAssistant: Hi there!'; |
50 | | - const result = stripMemoryTags(input); |
51 | | - expect(result).toBe(expected); |
| 35 | + expect(stripMemoryTags(input)).toBe(input); |
52 | 36 | }); |
53 | 37 |
|
54 | | - it('should handle nested-like content without actual nesting', () => { |
55 | | - const input = '<hindsight_memories>Outer start\n</hindsight_memories>\nSafe content\n<hindsight_memories>\nOuter end</hindsight_memories>'; |
56 | | - const expected = '\nSafe content\n'; |
57 | | - const result = stripMemoryTags(input); |
58 | | - expect(result).toBe(expected); |
59 | | - }); |
60 | | - |
61 | | - it('should strip both tag types in same content', () => { |
62 | | - const input = 'A\n<hindsight_memories>\nH mem\n</hindsight_memories>\nB\n<relevant_memories>\nR mem\n</relevant_memories>\nC'; |
63 | | - const expected = 'A\n\nB\n\nC'; |
64 | | - const result = stripMemoryTags(input); |
65 | | - expect(result).toBe(expected); |
| 38 | + it('strips both tag types in same content', () => { |
| 39 | + const input = |
| 40 | + 'A\n<hindsight_memories>\nH mem\n</hindsight_memories>\nB\n<relevant_memories>\nR mem\n</relevant_memories>\nC'; |
| 41 | + expect(stripMemoryTags(input)).toBe('A\n\nB\n\nC'); |
66 | 42 | }); |
67 | 43 |
|
68 | | - it('should handle real-world agent conversation with injected memories', () => { |
69 | | - const input = '[role: system]\n<hindsight_memories>\nRelevant memories from past conversations (score 1=highest, prioritize recent when conflicting):\n[\n {\n "content": "User prefers dark mode",\n "relevance_score": 0.95\n }\n]\n\nUser message: How do I enable dark mode?\n</hindsight_memories>\n[system:end]\n\n[role: user]\nHow do I enable dark mode?\n[user:end]\n\n[role: assistant]\nBased on your previous preference, let me help you enable dark mode.\n[assistant:end]'; |
| 44 | + it('strips tags from a real-world agent conversation with injected memories', () => { |
| 45 | + const input = |
| 46 | + '[role: system]\n<hindsight_memories>\nRelevant memories:\n[{"text": "User prefers dark mode"}]\nUser message: How do I enable dark mode?\n</hindsight_memories>\n[system:end]\n\n[role: user]\nHow do I enable dark mode?\n[user:end]\n\n[role: assistant]\nLet me help you enable dark mode.\n[assistant:end]'; |
70 | 47 |
|
71 | 48 | const result = stripMemoryTags(input); |
72 | 49 |
|
73 | | - // Should not contain the memory tags |
74 | 50 | expect(result).not.toContain('<hindsight_memories>'); |
75 | 51 | expect(result).not.toContain('</hindsight_memories>'); |
76 | | - expect(result).not.toContain('Relevant memories from past conversations'); |
77 | | - |
78 | | - // Should still contain the actual conversation |
| 52 | + expect(result).not.toContain('User prefers dark mode'); |
79 | 53 | expect(result).toContain('[role: user]'); |
80 | 54 | expect(result).toContain('How do I enable dark mode?'); |
81 | 55 | expect(result).toContain('[role: assistant]'); |
82 | 56 | }); |
83 | 57 | }); |
| 58 | + |
| 59 | +// --------------------------------------------------------------------------- |
| 60 | +// extractRecallQuery |
| 61 | +// --------------------------------------------------------------------------- |
| 62 | + |
| 63 | +describe('extractRecallQuery', () => { |
| 64 | + it('returns rawMessage when it is long enough', () => { |
| 65 | + expect(extractRecallQuery('What is my favorite food?', undefined)).toBe( |
| 66 | + 'What is my favorite food?', |
| 67 | + ); |
| 68 | + }); |
| 69 | + |
| 70 | + it('returns null when rawMessage is too short and prompt is absent', () => { |
| 71 | + expect(extractRecallQuery('Hi', undefined)).toBeNull(); |
| 72 | + expect(extractRecallQuery('', '')).toBeNull(); |
| 73 | + expect(extractRecallQuery(undefined, undefined)).toBeNull(); |
| 74 | + }); |
| 75 | + |
| 76 | + it('returns null when both rawMessage and prompt are too short', () => { |
| 77 | + expect(extractRecallQuery('Hey', 'Hey')).toBeNull(); |
| 78 | + }); |
| 79 | + |
| 80 | + it('falls back to prompt when rawMessage is absent', () => { |
| 81 | + const result = extractRecallQuery(undefined, 'What programming language do I prefer?'); |
| 82 | + expect(result).toBe('What programming language do I prefer?'); |
| 83 | + }); |
| 84 | + |
| 85 | + it('strips leading System: lines from prompt', () => { |
| 86 | + const prompt = 'System: You are an agent.\nSystem: Use tools wisely.\n\nWhat is my name?'; |
| 87 | + const result = extractRecallQuery(undefined, prompt); |
| 88 | + expect(result).not.toContain('System:'); |
| 89 | + expect(result).toContain('What is my name?'); |
| 90 | + }); |
| 91 | + |
| 92 | + it('strips [Channel] envelope header and returns inner message', () => { |
| 93 | + const prompt = '[Telegram Chat]\nWhat is my favorite hobby?'; |
| 94 | + const result = extractRecallQuery(undefined, prompt); |
| 95 | + expect(result).toBe('What is my favorite hobby?'); |
| 96 | + }); |
| 97 | + |
| 98 | + it('strips [from: SenderName] footer from group chat prompts', () => { |
| 99 | + const prompt = '[Slack Channel #general]\nWhat should I eat for lunch?\n[from: Alice]'; |
| 100 | + const result = extractRecallQuery(undefined, prompt); |
| 101 | + expect(result).not.toContain('[from: Alice]'); |
| 102 | + expect(result).toContain('What should I eat for lunch?'); |
| 103 | + }); |
| 104 | + |
| 105 | + it('handles full envelope with System lines, channel header, and from footer', () => { |
| 106 | + const prompt = |
| 107 | + 'System: You are a helpful agent.\n\n[Discord Server]\nRemind me what I said about Python?\n[from: Bob]'; |
| 108 | + const result = extractRecallQuery(undefined, prompt); |
| 109 | + expect(result).not.toContain('System:'); |
| 110 | + expect(result).not.toContain('[Discord'); |
| 111 | + expect(result).not.toContain('[from: Bob]'); |
| 112 | + expect(result).toContain('Remind me what I said about Python?'); |
| 113 | + }); |
| 114 | + |
| 115 | + it('strips session abort hint from prompt', () => { |
| 116 | + const prompt = |
| 117 | + 'Note: The previous agent run was aborted by the user\n\n[Telegram]\nWhat is my cat\'s name?'; |
| 118 | + const result = extractRecallQuery(undefined, prompt); |
| 119 | + expect(result).not.toContain('Note: The previous agent run was aborted'); |
| 120 | + expect(result).toContain("What is my cat's name?"); |
| 121 | + }); |
| 122 | + |
| 123 | + it('returns null when prompt reduces to < 5 chars after stripping', () => { |
| 124 | + // Envelope with almost-empty inner message |
| 125 | + const prompt = '[Telegram Chat]\nHi'; |
| 126 | + const result = extractRecallQuery(undefined, prompt); |
| 127 | + expect(result).toBeNull(); |
| 128 | + }); |
| 129 | + |
| 130 | + it('prefers rawMessage over prompt even when prompt is longer', () => { |
| 131 | + const rawMessage = 'What do I like to eat?'; |
| 132 | + const prompt = '[Telegram]\nWhat do I like to eat?\n[from: Alice]'; |
| 133 | + const result = extractRecallQuery(rawMessage, prompt); |
| 134 | + // Should return the clean rawMessage verbatim |
| 135 | + expect(result).toBe(rawMessage); |
| 136 | + expect(result).not.toContain('[from: Alice]'); |
| 137 | + }); |
| 138 | + |
| 139 | + it('trims whitespace from result', () => { |
| 140 | + const result = extractRecallQuery(' What is my job? ', undefined); |
| 141 | + expect(result).toBe('What is my job?'); |
| 142 | + }); |
| 143 | +}); |
0 commit comments