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.
- package/package.json +1 -1
- package/src/__test-utils__/fixtures/malformedJson.js +31 -0
- package/src/__test-utils__/globalSetup.js +9 -0
- package/src/__test-utils__/globalTeardown.js +12 -0
- package/src/__test-utils__/mockFactories.js +101 -0
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
- package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
- package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
- package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
- package/src/core/__tests__/agentPool.test.js +601 -0
- package/src/core/__tests__/agentScheduler.test.js +576 -0
- package/src/core/__tests__/contextManager.test.js +252 -0
- package/src/core/__tests__/flowExecutor.test.js +262 -0
- package/src/core/__tests__/messageProcessor.test.js +627 -0
- package/src/core/__tests__/orchestrator.test.js +257 -0
- package/src/core/__tests__/stateManager.test.js +375 -0
- package/src/core/agentPool.js +11 -1
- package/src/index.js +25 -9
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
- package/src/services/__tests__/agentActivityService.test.js +319 -0
- package/src/services/__tests__/apiKeyManager.test.js +206 -0
- package/src/services/__tests__/benchmarkService.test.js +184 -0
- package/src/services/__tests__/budgetService.test.js +211 -0
- package/src/services/__tests__/contextInjectionService.test.js +205 -0
- package/src/services/__tests__/conversationCompactionService.test.js +280 -0
- package/src/services/__tests__/credentialVault.test.js +469 -0
- package/src/services/__tests__/errorHandler.test.js +314 -0
- package/src/services/__tests__/fileAttachmentService.test.js +278 -0
- package/src/services/__tests__/flowContextService.test.js +199 -0
- package/src/services/__tests__/memoryService.test.js +450 -0
- package/src/services/__tests__/modelRouterService.test.js +388 -0
- package/src/services/__tests__/modelsService.test.js +261 -0
- package/src/services/__tests__/portRegistry.test.js +123 -0
- package/src/services/__tests__/projectDetector.test.js +34 -0
- package/src/services/__tests__/promptService.test.js +242 -0
- package/src/services/__tests__/qualityInspector.test.js +97 -0
- package/src/services/__tests__/scheduleService.test.js +308 -0
- package/src/services/__tests__/serviceRegistry.test.js +74 -0
- package/src/services/__tests__/skillsService.test.js +402 -0
- package/src/services/__tests__/tokenCountingService.test.js +48 -0
- package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
- package/src/tools/__tests__/agentDelayTool.test.js +342 -0
- package/src/tools/__tests__/asyncToolManager.test.js +344 -0
- package/src/tools/__tests__/baseTool.test.js +420 -0
- package/src/tools/__tests__/codeMapTool.test.js +348 -0
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
- package/src/tools/__tests__/fileTreeTool.test.js +274 -0
- package/src/tools/__tests__/filesystemTool.test.js +717 -0
- package/src/tools/__tests__/helpTool.test.js +204 -0
- package/src/tools/__tests__/jobDoneTool.test.js +296 -0
- package/src/tools/__tests__/memoryTool.test.js +297 -0
- package/src/tools/__tests__/seekTool.test.js +282 -0
- package/src/tools/__tests__/skillsTool.test.js +226 -0
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
- package/src/tools/__tests__/taskManagerTool.test.js +725 -0
- package/src/tools/__tests__/terminalTool.test.js +384 -0
- package/src/tools/__tests__/userPromptTool.test.js +297 -0
- package/src/tools/__tests__/webTool.e2e.test.js +25 -11
- package/src/tools/webTool.js +6 -12
- package/src/types/__tests__/agent.test.js +499 -0
- package/src/types/__tests__/contextReference.test.js +606 -0
- package/src/types/__tests__/conversation.test.js +555 -0
- package/src/types/__tests__/toolCommand.test.js +584 -0
- package/src/types/contextReference.js +1 -1
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
- package/src/utilities/__tests__/configManager.test.js +397 -0
- package/src/utilities/__tests__/constants.test.js +49 -0
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
- package/src/utilities/__tests__/fileProcessor.test.js +104 -0
- package/src/utilities/__tests__/jsonRepair.test.js +104 -0
- package/src/utilities/__tests__/logger.test.js +129 -0
- package/src/utilities/__tests__/platformUtils.test.js +87 -0
- package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
- package/src/utilities/__tests__/tagParser.test.js +887 -0
- package/src/utilities/__tests__/toolConstants.test.js +94 -0
- package/src/utilities/tagParser.js +2 -2
- package/src/tools/browserTool.js +0 -897
- 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
|
+
});
|