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,402 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock fs and userDataDir
5
+ const mockFs = {
6
+ readFile: jest.fn(),
7
+ writeFile: jest.fn().mockResolvedValue(undefined),
8
+ mkdir: jest.fn().mockResolvedValue(undefined),
9
+ rm: jest.fn().mockResolvedValue(undefined),
10
+ readdir: jest.fn().mockResolvedValue([]),
11
+ stat: jest.fn(),
12
+ access: jest.fn().mockResolvedValue(undefined),
13
+ copyFile: jest.fn().mockResolvedValue(undefined)
14
+ };
15
+
16
+ jest.unstable_mockModule('fs', () => ({
17
+ promises: mockFs
18
+ }));
19
+
20
+ jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
21
+ getUserDataPaths: jest.fn(() => ({
22
+ settings: '/fake/settings',
23
+ attachments: '/fake/attachments',
24
+ skills: '/fake/skills'
25
+ })),
26
+ ensureUserDataDirs: jest.fn(async () => {})
27
+ }));
28
+
29
+ const { SkillsService } = await import('../skillsService.js');
30
+
31
+ describe('SkillsService', () => {
32
+ let service;
33
+ let logger;
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ logger = createMockLogger();
38
+ service = new SkillsService(logger);
39
+ service.initialized = false;
40
+ service.indexCache = null;
41
+ });
42
+
43
+ test('constructor initializes with default state', () => {
44
+ expect(service.initialized).toBe(false);
45
+ expect(service.indexCache).toBeNull();
46
+ });
47
+
48
+ test('initialize sets up skillsDir and marks initialized', async () => {
49
+ await service.initialize();
50
+ expect(service.initialized).toBe(true);
51
+ expect(service.skillsDir).toBe('/fake/skills');
52
+ });
53
+
54
+ test('initialize only runs once', async () => {
55
+ await service.initialize();
56
+ const { ensureUserDataDirs } = await import('../../utilities/userDataDir.js');
57
+ const callCount = ensureUserDataDirs.mock.calls.length;
58
+ await service.initialize();
59
+ expect(ensureUserDataDirs.mock.calls.length).toBe(callCount);
60
+ });
61
+
62
+ test('initialize throws on failure', async () => {
63
+ const { ensureUserDataDirs } = await import('../../utilities/userDataDir.js');
64
+ service.initialized = false;
65
+ ensureUserDataDirs.mockRejectedValueOnce(new Error('disk error'));
66
+ await expect(service.initialize()).rejects.toThrow('disk error');
67
+ });
68
+
69
+ describe('validation', () => {
70
+ test('_validateSkillName rejects empty names', () => {
71
+ expect(() => service._validateSkillName('')).toThrow('required');
72
+ expect(() => service._validateSkillName(null)).toThrow('required');
73
+ });
74
+
75
+ test('_validateSkillName rejects too long names', () => {
76
+ expect(() => service._validateSkillName('a'.repeat(51))).toThrow('50 characters');
77
+ });
78
+
79
+ test('_validateSkillName rejects non-kebab-case', () => {
80
+ expect(() => service._validateSkillName('MySkill')).toThrow('kebab-case');
81
+ expect(() => service._validateSkillName('my_skill')).toThrow('kebab-case');
82
+ });
83
+
84
+ test('_validateSkillName accepts valid names', () => {
85
+ expect(() => service._validateSkillName('my-skill')).not.toThrow();
86
+ expect(() => service._validateSkillName('code-review')).not.toThrow();
87
+ expect(() => service._validateSkillName('a1-b2')).not.toThrow();
88
+ });
89
+
90
+ test('_validatePathSafe rejects traversal paths', () => {
91
+ service.skillsDir = '/fake/skills';
92
+ expect(() => service._validatePathSafe('my-skill', '../../etc/passwd')).toThrow('within the skill directory');
93
+ });
94
+
95
+ test('_validatePathSafe accepts valid paths', () => {
96
+ service.skillsDir = '/fake/skills';
97
+ const result = service._validatePathSafe('my-skill', 'subdir/file.txt');
98
+ expect(result).toContain('my-skill');
99
+ });
100
+ });
101
+
102
+ describe('content analysis', () => {
103
+ test('_extractDescription returns first non-heading line', () => {
104
+ const content = '# Title\n\nThis is the description.\nMore text.';
105
+ expect(service._extractDescription(content)).toBe('This is the description.');
106
+ });
107
+
108
+ test('_extractDescription returns empty for null', () => {
109
+ expect(service._extractDescription(null)).toBe('');
110
+ });
111
+
112
+ test('_extractDescription truncates long descriptions', () => {
113
+ const content = 'x'.repeat(250);
114
+ const desc = service._extractDescription(content);
115
+ expect(desc.length).toBeLessThanOrEqual(200);
116
+ expect(desc).toContain('...');
117
+ });
118
+
119
+ test('_extractSections finds ## headings', () => {
120
+ const content = '# Title\nIntro\n## Section A\nContent A\n## Section B\nContent B';
121
+ const sections = service._extractSections(content);
122
+ expect(sections).toHaveLength(2);
123
+ expect(sections[0].heading).toBe('## Section A');
124
+ expect(sections[1].heading).toBe('## Section B');
125
+ });
126
+
127
+ test('_extractSections returns empty for null', () => {
128
+ expect(service._extractSections(null)).toEqual([]);
129
+ });
130
+
131
+ test('_computeSize counts bytes and lines', () => {
132
+ const result = service._computeSize('line1\nline2\nline3');
133
+ expect(result.lineCount).toBe(3);
134
+ expect(result.sizeBytes).toBeGreaterThan(0);
135
+ });
136
+
137
+ test('_computeSize handles null', () => {
138
+ const result = service._computeSize(null);
139
+ expect(result.lineCount).toBe(0);
140
+ });
141
+ });
142
+
143
+ describe('CRUD operations', () => {
144
+ beforeEach(async () => {
145
+ // Pre-initialize
146
+ service.initialized = true;
147
+ service.skillsDir = '/fake/skills';
148
+ });
149
+
150
+ test('listSkills returns mapped skill summaries', async () => {
151
+ service.indexCache = {
152
+ skills: {
153
+ 'my-skill': {
154
+ name: 'my-skill',
155
+ description: 'A skill',
156
+ sections: ['## Setup'],
157
+ sizeBytes: 100,
158
+ lineCount: 10,
159
+ files: ['skill.md'],
160
+ createdAt: '2024-01-01',
161
+ updatedAt: '2024-01-02'
162
+ }
163
+ }
164
+ };
165
+ const list = await service.listSkills();
166
+ expect(list).toHaveLength(1);
167
+ expect(list[0].name).toBe('my-skill');
168
+ expect(list[0].fileCount).toBe(1);
169
+ });
170
+
171
+ test('listSkills loads index when cache empty', async () => {
172
+ service.indexCache = null;
173
+ mockFs.readFile.mockRejectedValueOnce(new Error('ENOENT'));
174
+ const list = await service.listSkills();
175
+ expect(list).toEqual([]);
176
+ });
177
+
178
+ test('describeSkill returns detailed info', async () => {
179
+ service.indexCache = {
180
+ skills: {
181
+ 'my-skill': {
182
+ name: 'my-skill',
183
+ description: 'Desc',
184
+ sections: ['## Setup'],
185
+ files: ['skill.md'],
186
+ createdAt: '2024-01-01',
187
+ updatedAt: '2024-01-02'
188
+ }
189
+ }
190
+ };
191
+ mockFs.readFile.mockResolvedValueOnce('# Title\n## Setup\nContent');
192
+
193
+ const info = await service.describeSkill('my-skill');
194
+ expect(info.name).toBe('my-skill');
195
+ expect(info.sections).toHaveLength(1);
196
+ });
197
+
198
+ test('describeSkill throws for unknown skill', async () => {
199
+ service.indexCache = { skills: {} };
200
+ await expect(service.describeSkill('unknown')).rejects.toThrow('not found');
201
+ });
202
+
203
+ test('readSkill returns content and files', async () => {
204
+ service.indexCache = { skills: { 'my-skill': { description: 'Desc' } } };
205
+ mockFs.readFile.mockResolvedValueOnce('# Skill content');
206
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
207
+
208
+ const result = await service.readSkill('my-skill');
209
+ expect(result.content).toBe('# Skill content');
210
+ expect(result.name).toBe('my-skill');
211
+ });
212
+
213
+ test('readSkill throws for unknown skill', async () => {
214
+ service.indexCache = { skills: {} };
215
+ await expect(service.readSkill('unknown')).rejects.toThrow('not found');
216
+ });
217
+
218
+ test('readSkillSection returns matching section', async () => {
219
+ service.indexCache = { skills: { 'my-skill': {} } };
220
+ mockFs.readFile.mockResolvedValueOnce('# Title\nIntro\n## Setup\nSetup content\n## Usage\nUsage content');
221
+
222
+ const result = await service.readSkillSection('my-skill', 'Setup');
223
+ expect(result.section).toBe('## Setup');
224
+ expect(result.content).toContain('Setup content');
225
+ });
226
+
227
+ test('readSkillSection throws for missing section', async () => {
228
+ service.indexCache = { skills: { 'my-skill': {} } };
229
+ mockFs.readFile.mockResolvedValueOnce('# Title\n## Setup\nContent');
230
+
231
+ await expect(service.readSkillSection('my-skill', 'Missing')).rejects.toThrow('Section not found');
232
+ });
233
+
234
+ test('readSkillFile reads a file within skill directory', async () => {
235
+ service.indexCache = { skills: { 'my-skill': {} } };
236
+ mockFs.readFile.mockResolvedValueOnce('file content');
237
+
238
+ const result = await service.readSkillFile('my-skill', 'data.json');
239
+ expect(result.content).toBe('file content');
240
+ });
241
+
242
+ test('readSkillFile throws for missing file', async () => {
243
+ service.indexCache = { skills: { 'my-skill': {} } };
244
+ mockFs.readFile.mockRejectedValueOnce(new Error('ENOENT'));
245
+
246
+ await expect(service.readSkillFile('my-skill', 'missing.txt')).rejects.toThrow('File not found');
247
+ });
248
+
249
+ test('createSkill creates directory and writes files', async () => {
250
+ service.indexCache = { skills: {} };
251
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
252
+
253
+ const entry = await service.createSkill('new-skill', '# New Skill\nDescription text');
254
+ expect(mockFs.mkdir).toHaveBeenCalled();
255
+ expect(mockFs.writeFile).toHaveBeenCalled();
256
+ expect(entry.name).toBe('new-skill');
257
+ });
258
+
259
+ test('createSkill throws for existing skill', async () => {
260
+ service.indexCache = { skills: { 'existing': {} } };
261
+ await expect(service.createSkill('existing', 'content')).rejects.toThrow('already exists');
262
+ });
263
+
264
+ test('createSkill handles additional files', async () => {
265
+ service.indexCache = { skills: {} };
266
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
267
+
268
+ await service.createSkill('my-skill', '# Skill', [
269
+ { path: 'data.json', content: '{}' }
270
+ ]);
271
+ // writeFile called for skill.md, data.json, and index
272
+ expect(mockFs.writeFile.mock.calls.length).toBeGreaterThanOrEqual(3);
273
+ });
274
+
275
+ test('updateSkill updates content', async () => {
276
+ service.indexCache = {
277
+ skills: {
278
+ 'my-skill': { name: 'my-skill', createdAt: '2024-01-01', description: 'Old' }
279
+ }
280
+ };
281
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
282
+
283
+ const entry = await service.updateSkill('my-skill', '# Updated');
284
+ expect(entry.createdAt).toBe('2024-01-01'); // Preserved
285
+ });
286
+
287
+ test('updateSkill reads existing content when not provided', async () => {
288
+ service.indexCache = { skills: { 'my-skill': { createdAt: '2024-01-01', description: 'D' } } };
289
+ mockFs.readFile.mockResolvedValueOnce('# Existing content');
290
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
291
+
292
+ await service.updateSkill('my-skill');
293
+ expect(mockFs.readFile).toHaveBeenCalled();
294
+ });
295
+
296
+ test('updateSkill throws for unknown skill', async () => {
297
+ service.indexCache = { skills: {} };
298
+ await expect(service.updateSkill('unknown', 'content')).rejects.toThrow('not found');
299
+ });
300
+
301
+ test('deleteSkill removes directory and index entry', async () => {
302
+ service.indexCache = { skills: { 'my-skill': {} } };
303
+ await service.deleteSkill('my-skill');
304
+ expect(mockFs.rm).toHaveBeenCalled();
305
+ expect(service.indexCache.skills['my-skill']).toBeUndefined();
306
+ });
307
+
308
+ test('deleteSkill throws for unknown skill', async () => {
309
+ service.indexCache = { skills: {} };
310
+ await expect(service.deleteSkill('unknown')).rejects.toThrow('not found');
311
+ });
312
+
313
+ test('getSkillSummaries returns matching summaries', async () => {
314
+ service.indexCache = {
315
+ skills: {
316
+ 'skill-a': { name: 'skill-a', description: 'A', sections: ['## S1'], lineCount: 5 },
317
+ 'skill-b': { name: 'skill-b', description: 'B', sections: [], lineCount: 10 }
318
+ }
319
+ };
320
+
321
+ const summaries = await service.getSkillSummaries(['skill-a', 'skill-c']);
322
+ expect(summaries).toHaveLength(1);
323
+ expect(summaries[0].name).toBe('skill-a');
324
+ });
325
+ });
326
+
327
+ describe('importSkill', () => {
328
+ beforeEach(() => {
329
+ service.initialized = true;
330
+ service.skillsDir = '/fake/skills';
331
+ });
332
+
333
+ test('imports a single file as skill.md', async () => {
334
+ service.indexCache = { skills: {} };
335
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => false });
336
+ mockFs.readFile.mockResolvedValueOnce('# Imported skill content');
337
+ // second readFile for _buildIndexEntry
338
+ mockFs.readFile.mockResolvedValueOnce('# Imported skill content');
339
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
340
+
341
+ const entry = await service.importSkill('/path/to/my-file.md', 'imported-skill');
342
+ expect(entry.name).toBe('imported-skill');
343
+ });
344
+
345
+ test('throws for non-existent source', async () => {
346
+ service.indexCache = { skills: {} };
347
+ mockFs.stat.mockRejectedValueOnce(new Error('ENOENT'));
348
+
349
+ await expect(service.importSkill('/missing/path')).rejects.toThrow('not found');
350
+ });
351
+
352
+ test('throws for duplicate skill name', async () => {
353
+ service.indexCache = { skills: { 'existing': {} } };
354
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => false });
355
+
356
+ await expect(service.importSkill('/path/file.md', 'existing')).rejects.toThrow('already exists');
357
+ });
358
+
359
+ test('derives skill name from source path', async () => {
360
+ service.indexCache = { skills: {} };
361
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => false });
362
+ mockFs.readFile.mockResolvedValueOnce('content');
363
+ mockFs.readFile.mockResolvedValueOnce('content');
364
+ mockFs.readdir.mockResolvedValueOnce([{ name: 'skill.md', isFile: () => true }]);
365
+
366
+ const entry = await service.importSkill('/path/to/My Cool Skill.md');
367
+ // Should be kebab-cased
368
+ expect(entry.name).toMatch(/^[a-z0-9-]+$/);
369
+ });
370
+
371
+ test('imports directory with skill.md', async () => {
372
+ service.indexCache = { skills: {} };
373
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => true });
374
+ // _copyDir
375
+ mockFs.readdir.mockResolvedValueOnce([
376
+ { name: 'skill.md', isFile: () => true, isDirectory: () => false },
377
+ { name: 'data.json', isFile: () => true, isDirectory: () => false }
378
+ ]);
379
+ // access check
380
+ mockFs.access.mockResolvedValueOnce(undefined);
381
+ // readFile for _buildIndexEntry
382
+ mockFs.readFile.mockResolvedValueOnce('# Dir skill');
383
+ // _listSkillFiles
384
+ mockFs.readdir.mockResolvedValueOnce([
385
+ { name: 'skill.md', isFile: () => true },
386
+ { name: 'data.json', isFile: () => true }
387
+ ]);
388
+
389
+ const entry = await service.importSkill('/path/skill-dir', 'dir-skill');
390
+ expect(entry.name).toBe('dir-skill');
391
+ });
392
+
393
+ test('imports directory without skill.md throws', async () => {
394
+ service.indexCache = { skills: {} };
395
+ mockFs.stat.mockResolvedValueOnce({ isDirectory: () => true });
396
+ mockFs.readdir.mockResolvedValueOnce([]);
397
+ mockFs.access.mockRejectedValueOnce(new Error('ENOENT'));
398
+
399
+ await expect(service.importSkill('/path/no-skill', 'bad-import')).rejects.toThrow('must contain a skill.md');
400
+ });
401
+ });
402
+ });
@@ -0,0 +1,48 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+ import TokenCountingService from '../tokenCountingService.js';
4
+
5
+ describe('TokenCountingService', () => {
6
+ let logger;
7
+
8
+ beforeEach(() => {
9
+ logger = createMockLogger();
10
+ });
11
+
12
+ test('constructor creates instance', () => {
13
+ const service = new TokenCountingService(logger);
14
+ expect(service).toBeInstanceOf(TokenCountingService);
15
+ expect(service.logger).toBe(logger);
16
+ });
17
+
18
+ test('getModelContextWindow returns positive number for known model', () => {
19
+ const service = new TokenCountingService(logger);
20
+ // Use a fallback model name that exists in the hardcoded map
21
+ const contextWindow = service.getModelContextWindow('gpt-4');
22
+ expect(typeof contextWindow).toBe('number');
23
+ expect(contextWindow).toBeGreaterThan(0);
24
+ });
25
+
26
+ test('shouldTriggerCompaction returns true when near limit', () => {
27
+ const service = new TokenCountingService(logger);
28
+ // currentTokens + maxOutputTokens >= threshold * contextWindow
29
+ // 90000 + 8192 = 98192 >= 0.7 * 128000 = 89600 => true
30
+ const result = service.shouldTriggerCompaction(90000, 8192, 128000);
31
+ expect(result).toBe(true);
32
+ });
33
+
34
+ test('shouldTriggerCompaction returns false when well under limit', () => {
35
+ const service = new TokenCountingService(logger);
36
+ // 10000 + 8192 = 18192 < 0.7 * 128000 = 89600 => false
37
+ const result = service.shouldTriggerCompaction(10000, 8192, 128000);
38
+ expect(result).toBe(false);
39
+ });
40
+
41
+ test('calculateTargetTokenCount returns positive number', () => {
42
+ const service = new TokenCountingService(logger);
43
+ const target = service.calculateTargetTokenCount(128000);
44
+ expect(typeof target).toBe('number');
45
+ expect(target).toBeGreaterThan(0);
46
+ expect(target).toBeLessThanOrEqual(128000);
47
+ });
48
+ });