onbuzz 3.6.1 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/package.json +1 -1
  2. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  3. package/src/__test-utils__/globalSetup.js +9 -0
  4. package/src/__test-utils__/globalTeardown.js +12 -0
  5. package/src/__test-utils__/mockFactories.js +101 -0
  6. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  7. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  8. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  9. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  10. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  11. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  12. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  13. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  14. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  15. package/src/core/__tests__/agentPool.test.js +601 -0
  16. package/src/core/__tests__/agentScheduler.test.js +576 -0
  17. package/src/core/__tests__/contextManager.test.js +252 -0
  18. package/src/core/__tests__/flowExecutor.test.js +262 -0
  19. package/src/core/__tests__/messageProcessor.test.js +627 -0
  20. package/src/core/__tests__/orchestrator.test.js +257 -0
  21. package/src/core/__tests__/stateManager.test.js +375 -0
  22. package/src/core/agentPool.js +11 -1
  23. package/src/index.js +25 -9
  24. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  25. package/src/services/__tests__/agentActivityService.test.js +319 -0
  26. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  27. package/src/services/__tests__/benchmarkService.test.js +184 -0
  28. package/src/services/__tests__/budgetService.test.js +211 -0
  29. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  30. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  31. package/src/services/__tests__/credentialVault.test.js +469 -0
  32. package/src/services/__tests__/errorHandler.test.js +314 -0
  33. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  34. package/src/services/__tests__/flowContextService.test.js +199 -0
  35. package/src/services/__tests__/memoryService.test.js +450 -0
  36. package/src/services/__tests__/modelRouterService.test.js +388 -0
  37. package/src/services/__tests__/modelsService.test.js +261 -0
  38. package/src/services/__tests__/portRegistry.test.js +123 -0
  39. package/src/services/__tests__/projectDetector.test.js +34 -0
  40. package/src/services/__tests__/promptService.test.js +242 -0
  41. package/src/services/__tests__/qualityInspector.test.js +97 -0
  42. package/src/services/__tests__/scheduleService.test.js +308 -0
  43. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  44. package/src/services/__tests__/skillsService.test.js +402 -0
  45. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  46. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  47. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  48. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  49. package/src/tools/__tests__/baseTool.test.js +420 -0
  50. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  51. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  52. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  53. package/src/tools/__tests__/filesystemTool.test.js +717 -0
  54. package/src/tools/__tests__/helpTool.test.js +204 -0
  55. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  56. package/src/tools/__tests__/memoryTool.test.js +297 -0
  57. package/src/tools/__tests__/seekTool.test.js +282 -0
  58. package/src/tools/__tests__/skillsTool.test.js +226 -0
  59. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  60. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  61. package/src/tools/__tests__/terminalTool.test.js +384 -0
  62. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  63. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  64. package/src/tools/webTool.js +6 -12
  65. package/src/types/__tests__/agent.test.js +499 -0
  66. package/src/types/__tests__/contextReference.test.js +606 -0
  67. package/src/types/__tests__/conversation.test.js +555 -0
  68. package/src/types/__tests__/toolCommand.test.js +584 -0
  69. package/src/types/contextReference.js +1 -1
  70. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  71. package/src/utilities/__tests__/configManager.test.js +397 -0
  72. package/src/utilities/__tests__/constants.test.js +49 -0
  73. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  74. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  75. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  76. package/src/utilities/__tests__/logger.test.js +129 -0
  77. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  78. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  79. package/src/utilities/__tests__/tagParser.test.js +887 -0
  80. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  81. package/src/utilities/tagParser.js +2 -2
  82. package/src/tools/browserTool.js +0 -897
  83. package/src/utilities/platformUtils.test.js +0 -98
@@ -0,0 +1,280 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockAiService } from '../../__test-utils__/mockFactories.js';
3
+ import ConversationCompactionService from '../conversationCompactionService.js';
4
+
5
+ describe('ConversationCompactionService', () => {
6
+ let logger;
7
+ let aiService;
8
+ let tokenCountingService;
9
+ let service;
10
+
11
+ beforeEach(() => {
12
+ logger = createMockLogger();
13
+ aiService = createMockAiService();
14
+
15
+ tokenCountingService = {
16
+ getConversationTokenCount: jest.fn().mockReturnValue(50000),
17
+ getModelContextWindow: jest.fn().mockReturnValue(128000),
18
+ getModelMaxOutputTokens: jest.fn().mockReturnValue(8192),
19
+ shouldTriggerCompaction: jest.fn().mockReturnValue(false),
20
+ calculateTargetTokenCount: jest.fn().mockReturnValue(100000)
21
+ };
22
+
23
+ service = new ConversationCompactionService(tokenCountingService, aiService, logger);
24
+ });
25
+
26
+ // ─── Constructor ─────────────────────────────────────────────────────
27
+
28
+ test('constructor creates instance with dependencies', () => {
29
+ expect(service).toBeInstanceOf(ConversationCompactionService);
30
+ expect(service.tokenCountingService).toBe(tokenCountingService);
31
+ expect(service.aiService).toBe(aiService);
32
+ expect(service.logger).toBe(logger);
33
+ expect(service.modelsService).toBeNull();
34
+ expect(service.compactionModelIndex).toBe(0);
35
+ });
36
+
37
+ // ─── compactConversation ─────────────────────────────────────────────
38
+
39
+ test('compactConversation throws on empty messages array', async () => {
40
+ await expect(service.compactConversation([], 'model-a', 'model-a'))
41
+ .rejects.toThrow('Messages array is required and cannot be empty');
42
+ });
43
+
44
+ test('compactConversation with fewer than MIN_MESSAGES returns skipped result', async () => {
45
+ // MIN_MESSAGES_FOR_COMPACTION = 10, send only 3 short messages
46
+ const messages = [
47
+ { role: 'user', content: 'hi' },
48
+ { role: 'assistant', content: 'hello' },
49
+ { role: 'user', content: 'bye' }
50
+ ];
51
+
52
+ const result = await service.compactConversation(messages, 'model-a', 'model-a');
53
+ expect(result.skipped).toBe(true);
54
+ expect(result.reason).toBe('Too few messages');
55
+ expect(result.compactedMessages).toBe(messages);
56
+ });
57
+
58
+ // ─── _splitOversizedMessages ─────────────────────────────────────────
59
+
60
+ test('_splitOversizedMessages does not split short messages', () => {
61
+ const messages = [
62
+ { role: 'user', content: 'short message' },
63
+ { role: 'assistant', content: 'another short one' }
64
+ ];
65
+ const result = service._splitOversizedMessages(messages);
66
+ expect(result.wasSplit).toBe(false);
67
+ expect(result.messages).toHaveLength(2);
68
+ expect(result.messages[0].content).toBe('short message');
69
+ });
70
+
71
+ test('_splitOversizedMessages splits messages exceeding threshold into chunks', () => {
72
+ // OVERSIZED_MESSAGE_THRESHOLD = 50000, MAX_CHUNK_SIZE = 30000
73
+ const longContent = 'A'.repeat(60000);
74
+ const messages = [
75
+ { role: 'user', content: longContent }
76
+ ];
77
+ const result = service._splitOversizedMessages(messages);
78
+ expect(result.wasSplit).toBe(true);
79
+ expect(result.messages.length).toBeGreaterThan(1);
80
+ // Each chunk should have Part metadata
81
+ expect(result.messages[0].content).toContain('[Part 1/');
82
+ expect(result.messages[0]._splitMetadata).toBeDefined();
83
+ expect(result.messages[0]._splitMetadata.chunkIndex).toBe(0);
84
+ });
85
+
86
+ // ─── _splitContentIntoChunks ─────────────────────────────────────────
87
+
88
+ test('_splitContentIntoChunks splits at paragraph boundary (double newline)', () => {
89
+ // Build content with paragraphs that exceeds maxChunk
90
+ const para1 = 'X'.repeat(200);
91
+ const para2 = 'Y'.repeat(200);
92
+ const content = para1 + '\n\n' + para2;
93
+ // Use a small maxChunk that forces a split within content
94
+ const maxChunk = 250;
95
+ const chunks = service._splitContentIntoChunks(content, maxChunk);
96
+ expect(chunks.length).toBeGreaterThan(1);
97
+ // First chunk should end at the double-newline boundary
98
+ expect(chunks[0].endsWith('\n\n')).toBe(true);
99
+ });
100
+
101
+ test('_splitContentIntoChunks splits at sentence boundary when no paragraphs', () => {
102
+ // Content with sentences but no newlines
103
+ const sentence1 = 'A'.repeat(200) + '. ';
104
+ const sentence2 = 'B'.repeat(200);
105
+ const content = sentence1 + sentence2;
106
+ const maxChunk = 250;
107
+ const chunks = service._splitContentIntoChunks(content, maxChunk);
108
+ expect(chunks.length).toBeGreaterThan(1);
109
+ // First chunk should end at sentence boundary
110
+ expect(chunks[0].endsWith('. ')).toBe(true);
111
+ });
112
+
113
+ test('_splitContentIntoChunks hard-cuts when no boundaries found', () => {
114
+ // Single continuous string with no newlines, no periods, no spaces
115
+ const content = 'X'.repeat(500);
116
+ const maxChunk = 200;
117
+ const chunks = service._splitContentIntoChunks(content, maxChunk);
118
+ expect(chunks.length).toBe(3); // 200 + 200 + 100
119
+ expect(chunks[0]).toHaveLength(200);
120
+ expect(chunks[1]).toHaveLength(200);
121
+ expect(chunks[2]).toHaveLength(100);
122
+ });
123
+
124
+ test('_splitContentIntoChunks returns single chunk for short content', () => {
125
+ const chunks = service._splitContentIntoChunks('short', 1000);
126
+ expect(chunks).toEqual(['short']);
127
+ });
128
+
129
+ // ─── _identifySegments ──────────────────────────────────────────────
130
+
131
+ test('_identifySegments with <=4 messages returns correct split', () => {
132
+ const messages = [
133
+ { role: 'user', content: 'first' },
134
+ { role: 'assistant', content: 'second' },
135
+ { role: 'user', content: 'third' },
136
+ { role: 'assistant', content: 'fourth' }
137
+ ];
138
+ const segments = service._identifySegments(messages);
139
+ // With <=4 messages, beginning is empty, middle is all except last, end is last
140
+ expect(segments.beginning).toEqual([]);
141
+ expect(segments.middle).toHaveLength(3);
142
+ expect(segments.end).toHaveLength(1);
143
+ expect(segments.end[0].content).toBe('fourth');
144
+ });
145
+
146
+ test('_identifySegments with many messages preserves tail', () => {
147
+ const messages = [];
148
+ for (let i = 0; i < 20; i++) {
149
+ messages.push({ role: i % 2 === 0 ? 'user' : 'assistant', content: `msg-${i}` });
150
+ }
151
+ const segments = service._identifySegments(messages);
152
+ // End should contain the most recent messages (tail)
153
+ expect(segments.end.length).toBeGreaterThan(0);
154
+ // Last message should be in the end segment
155
+ const lastMsg = messages[messages.length - 1];
156
+ expect(segments.end[segments.end.length - 1]).toBe(lastMsg);
157
+ // Middle should contain the earliest messages
158
+ expect(segments.middle.length).toBeGreaterThan(0);
159
+ expect(segments.middle[0]).toBe(messages[0]);
160
+ });
161
+
162
+ // ─── _performFallbackCompaction ──────────────────────────────────────
163
+
164
+ test('_performFallbackCompaction filters out tool results and extra system messages', () => {
165
+ const mainSystem = { role: 'system', content: 'You are an assistant' };
166
+ const messages = [
167
+ mainSystem,
168
+ { role: 'user', content: 'Do something' },
169
+ { role: 'assistant', content: 'Sure' },
170
+ { role: 'tool', content: 'tool result data' },
171
+ { role: 'system', content: 'extra system message' },
172
+ { role: 'user', content: 'Next question' },
173
+ { role: 'assistant', content: 'Answer', type: 'tool_result' },
174
+ { role: 'user', content: 'Thanks' },
175
+ { role: 'assistant', content: 'Welcome' },
176
+ { role: 'user', content: 'Bye' },
177
+ { role: 'assistant', content: 'Goodbye' }
178
+ ];
179
+
180
+ const result = service._performFallbackCompaction(messages);
181
+ const compacted = result.compactedMessages;
182
+
183
+ // Should NOT contain the extra system message or tool results
184
+ const hasExtraSystem = compacted.some(m => m.content === 'extra system message');
185
+ const hasToolRole = compacted.some(m => m.role === 'tool');
186
+ const hasToolResult = compacted.some(m => m.type === 'tool_result');
187
+ expect(hasExtraSystem).toBe(false);
188
+ expect(hasToolRole).toBe(false);
189
+ expect(hasToolResult).toBe(false);
190
+ });
191
+
192
+ test('_performFallbackCompaction preserves main system message', () => {
193
+ const mainSystem = { role: 'system', content: 'Main system prompt' };
194
+ const messages = [
195
+ mainSystem,
196
+ { role: 'user', content: 'Hello' },
197
+ { role: 'assistant', content: 'Hi' },
198
+ { role: 'user', content: 'Question' },
199
+ { role: 'assistant', content: 'Answer' },
200
+ { role: 'user', content: 'Another' },
201
+ { role: 'assistant', content: 'Response' },
202
+ { role: 'user', content: 'More' },
203
+ { role: 'assistant', content: 'Info' },
204
+ { role: 'user', content: 'Last' },
205
+ { role: 'assistant', content: 'End' }
206
+ ];
207
+
208
+ const result = service._performFallbackCompaction(messages);
209
+ // Fallback compaction should produce compacted messages
210
+ expect(result.compactedMessages.length).toBeGreaterThan(0);
211
+ expect(result.compactedMessages.length).toBeLessThan(messages.length);
212
+ // Should have a summary message
213
+ const hasSummary = result.compactedMessages.some(m =>
214
+ m.content && m.content.includes('CONVERSATION SUMMARY')
215
+ );
216
+ expect(hasSummary).toBe(true);
217
+ });
218
+
219
+ // ─── Full compactConversation with AI ────────────────────────────────
220
+
221
+ test('compactConversation calls AI for summarization on sufficient messages', async () => {
222
+ // Build enough messages to exceed MIN_MESSAGES_FOR_COMPACTION (10)
223
+ const messages = [];
224
+ for (let i = 0; i < 15; i++) {
225
+ messages.push({
226
+ role: i % 2 === 0 ? 'user' : 'assistant',
227
+ content: `Message content number ${i} with some padding text to make it realistic.`
228
+ });
229
+ }
230
+
231
+ aiService.sendMessage.mockResolvedValue({
232
+ content: 'Summary of the conversation covering key decisions and outcomes.',
233
+ tokenUsage: { prompt_tokens: 500, completion_tokens: 100, total_tokens: 600 }
234
+ });
235
+
236
+ const result = await service.compactConversation(messages, 'model-a', 'model-a');
237
+
238
+ expect(result.skipped).toBeFalsy();
239
+ expect(result.compactedMessages).toBeDefined();
240
+ expect(result.strategy).toBeDefined();
241
+ expect(aiService.sendMessage).toHaveBeenCalled();
242
+ // The summary message should appear in compacted output
243
+ const summaryMsg = result.compactedMessages.find(m => m.type === 'summary');
244
+ expect(summaryMsg).toBeDefined();
245
+ expect(summaryMsg.content).toContain('[CONVERSATION SUMMARY');
246
+ });
247
+
248
+ test('compactConversation handles AI failure by using fallback compaction', async () => {
249
+ const messages = [];
250
+ for (let i = 0; i < 15; i++) {
251
+ messages.push({
252
+ role: i % 2 === 0 ? 'user' : 'assistant',
253
+ content: `Message number ${i} with some text.`
254
+ });
255
+ }
256
+
257
+ // Make ALL AI calls fail to trigger fallback
258
+ aiService.sendMessage.mockRejectedValue(new Error('API 429 rate limit exceeded'));
259
+
260
+ const result = await service.compactConversation(messages, 'model-a', 'model-a');
261
+
262
+ // Should have fallen back to structural compaction, not thrown
263
+ expect(result.strategy).toBe('structural_fallback');
264
+ expect(result.compactedMessages).toBeDefined();
265
+ expect(result.compactedMessages.length).toBeGreaterThan(0);
266
+ // Should contain a summary message from fallback
267
+ const summaryMsg = result.compactedMessages.find(m => m.type === 'summary');
268
+ expect(summaryMsg).toBeDefined();
269
+ expect(summaryMsg.content).toContain('structural fallback');
270
+ });
271
+
272
+ // ─── getCompactionStats ──────────────────────────────────────────────
273
+
274
+ test('setModelsService injects models service', () => {
275
+ const mockModelsService = { getModels: jest.fn(), getAvailableModelNames: jest.fn() };
276
+ service.setModelsService(mockModelsService);
277
+ expect(service.modelsService).toBe(mockModelsService);
278
+ expect(logger.info).toHaveBeenCalledWith('ModelsService injected into compaction service');
279
+ });
280
+ });