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,388 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import DirectoryAccessManager from '../directoryAccessManager.js';
5
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
6
+
7
+ describe('DirectoryAccessManager', () => {
8
+ let dam;
9
+ let logger;
10
+ const projectDir = path.resolve('/tmp/test-project');
11
+
12
+ beforeEach(() => {
13
+ logger = createMockLogger();
14
+ dam = new DirectoryAccessManager({}, logger);
15
+ });
16
+
17
+ test('createDirectoryAccess returns config with working directory', () => {
18
+ const access = dam.createDirectoryAccess({
19
+ workingDirectory: projectDir
20
+ });
21
+ expect(access).toHaveProperty('workingDirectory');
22
+ expect(access.workingDirectory).toBe(projectDir);
23
+ expect(access).toHaveProperty('readOnlyDirectories');
24
+ expect(access).toHaveProperty('writeEnabledDirectories');
25
+ expect(access).toHaveProperty('restrictToProject');
26
+ expect(access).toHaveProperty('version', '1.0');
27
+ expect(access).toHaveProperty('createdAt');
28
+ });
29
+
30
+ test('createDirectoryAccess includes workingDir in readOnly when restrictToProject', () => {
31
+ const access = dam.createDirectoryAccess({
32
+ workingDirectory: projectDir,
33
+ restrictToProject: true
34
+ });
35
+ expect(access.readOnlyDirectories).toContain(projectDir);
36
+ });
37
+
38
+ test('createDirectoryAccess filters writeEnabled dirs outside project when restricted', () => {
39
+ const access = dam.createDirectoryAccess({
40
+ workingDirectory: projectDir,
41
+ restrictToProject: true,
42
+ writeEnabledDirectories: ['/tmp/other-project']
43
+ });
44
+ // /tmp/other-project is outside projectDir, should be filtered
45
+ expect(access.writeEnabledDirectories).not.toContain(path.resolve('/tmp/other-project'));
46
+ });
47
+
48
+ test('createDirectoryAccess resolves custom restrictions to absolute paths', () => {
49
+ const access = dam.createDirectoryAccess({
50
+ workingDirectory: projectDir,
51
+ customRestrictions: ['sensitive']
52
+ });
53
+ expect(access.customRestrictions[0]).toBe(path.resolve('sensitive'));
54
+ });
55
+
56
+ // ─── validateReadAccess ────────────────────────────────────────
57
+
58
+ test('validateReadAccess allows path within project', () => {
59
+ const access = dam.createDirectoryAccess({
60
+ workingDirectory: projectDir
61
+ });
62
+ const filePath = path.join(projectDir, 'src', 'index.js');
63
+ const result = dam.validateReadAccess(filePath, access);
64
+ expect(result.allowed).toBe(true);
65
+ expect(result.category).toBe('allowed');
66
+ });
67
+
68
+ test('validateReadAccess denies system paths', () => {
69
+ const access = dam.createDirectoryAccess({
70
+ workingDirectory: projectDir
71
+ });
72
+ const systemPath = process.platform === 'win32'
73
+ ? 'C:\\Windows\\System32\\config'
74
+ : '/etc/passwd';
75
+ const result = dam.validateReadAccess(systemPath, access);
76
+ expect(result.allowed).toBe(false);
77
+ expect(result.category).toBe('system_restricted');
78
+ });
79
+
80
+ test('validateReadAccess allows system paths when allowSystemAccess is true', () => {
81
+ const sshPath = path.join(os.homedir(), '.ssh', 'known_hosts');
82
+ const access = dam.createDirectoryAccess({
83
+ workingDirectory: projectDir,
84
+ allowSystemAccess: true,
85
+ restrictToProject: false,
86
+ readOnlyDirectories: [os.homedir()]
87
+ });
88
+ const result = dam.validateReadAccess(sshPath, access);
89
+ expect(result.allowed).toBe(true);
90
+ });
91
+
92
+ test('validateReadAccess denies custom restricted paths', () => {
93
+ const restrictedDir = path.join(projectDir, 'secrets');
94
+ const access = dam.createDirectoryAccess({
95
+ workingDirectory: projectDir,
96
+ customRestrictions: [restrictedDir]
97
+ });
98
+ const result = dam.validateReadAccess(path.join(restrictedDir, 'key.pem'), access);
99
+ expect(result.allowed).toBe(false);
100
+ expect(result.category).toBe('custom_restricted');
101
+ });
102
+
103
+ test('validateReadAccess denies path outside project scope', () => {
104
+ const access = dam.createDirectoryAccess({
105
+ workingDirectory: projectDir,
106
+ restrictToProject: true
107
+ });
108
+ const result = dam.validateReadAccess('/tmp/other-project/file.js', access);
109
+ expect(result.allowed).toBe(false);
110
+ expect(result.category).toBe('project_restricted');
111
+ });
112
+
113
+ test('validateReadAccess handles validation errors gracefully', () => {
114
+ const access = dam.createDirectoryAccess({
115
+ workingDirectory: projectDir
116
+ });
117
+ // Pass an object instead of string to trigger error
118
+ const result = dam.validateReadAccess(null, access);
119
+ expect(result.allowed).toBe(false);
120
+ expect(result.category).toBe('validation_error');
121
+ });
122
+
123
+ // ─── validateWriteAccess ───────────────────────────────────────
124
+
125
+ test('validateWriteAccess allows within write-enabled directory', () => {
126
+ const access = dam.createDirectoryAccess({
127
+ workingDirectory: projectDir,
128
+ writeEnabledDirectories: [projectDir]
129
+ });
130
+ const filePath = path.join(projectDir, 'output.txt');
131
+ const result = dam.validateWriteAccess(filePath, access);
132
+ expect(result.allowed).toBe(true);
133
+ expect(result.writeAllowed).toBe(true);
134
+ expect(result.category).toBe('write_allowed');
135
+ });
136
+
137
+ test('validateWriteAccess denies write to read-only directory', () => {
138
+ const readOnlyDir = path.join(projectDir, 'readonly');
139
+ const access = dam.createDirectoryAccess({
140
+ workingDirectory: projectDir,
141
+ readOnlyDirectories: [readOnlyDir],
142
+ writeEnabledDirectories: [path.join(projectDir, 'writable')],
143
+ restrictToProject: false
144
+ });
145
+ const result = dam.validateWriteAccess(path.join(readOnlyDir, 'file.txt'), access);
146
+ expect(result.allowed).toBe(false);
147
+ expect(result.writeAllowed).toBe(false);
148
+ expect(result.category).toBe('read_only_restricted');
149
+ });
150
+
151
+ test('validateWriteAccess denies write outside write-enabled directories', () => {
152
+ const writeDir = path.join(projectDir, 'output');
153
+ const access = dam.createDirectoryAccess({
154
+ workingDirectory: projectDir,
155
+ writeEnabledDirectories: [writeDir],
156
+ restrictToProject: false
157
+ });
158
+ const otherPath = path.join(projectDir, 'src', 'file.js');
159
+ const result = dam.validateWriteAccess(otherPath, access);
160
+ expect(result.allowed).toBe(false);
161
+ expect(result.writeAllowed).toBe(false);
162
+ expect(result.category).toBe('write_restricted');
163
+ });
164
+
165
+ test('validateWriteAccess falls back to workingDirectory when no writeEnabled dirs', () => {
166
+ const access = dam.createDirectoryAccess({
167
+ workingDirectory: projectDir,
168
+ writeEnabledDirectories: [],
169
+ restrictToProject: true
170
+ });
171
+ // Manually clear writeEnabledDirectories that createDirectoryAccess may have filtered
172
+ access.writeEnabledDirectories = [];
173
+ const filePath = path.join(projectDir, 'output.txt');
174
+ const result = dam.validateWriteAccess(filePath, access);
175
+ expect(result.allowed).toBe(true);
176
+ expect(result.writeAllowed).toBe(true);
177
+ });
178
+
179
+ test('validateWriteAccess propagates read-access denial', () => {
180
+ const access = dam.createDirectoryAccess({
181
+ workingDirectory: projectDir
182
+ });
183
+ const systemPath = process.platform === 'win32'
184
+ ? 'C:\\Windows\\System32\\config\\test.txt'
185
+ : '/etc/shadow';
186
+ const result = dam.validateWriteAccess(systemPath, access);
187
+ expect(result.allowed).toBe(false);
188
+ expect(result.writeAllowed).toBe(false);
189
+ });
190
+
191
+ // ─── getWorkingDirectory ───────────────────────────────────────
192
+
193
+ test('getWorkingDirectory returns workingDirectory from config', () => {
194
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
195
+ expect(dam.getWorkingDirectory(access)).toBe(projectDir);
196
+ });
197
+
198
+ test('getWorkingDirectory falls back to cwd when no workingDirectory', () => {
199
+ expect(dam.getWorkingDirectory({})).toBe(process.cwd());
200
+ });
201
+
202
+ // ─── getAccessibleDirectories ──────────────────────────────────
203
+
204
+ test('getAccessibleDirectories returns directory listing', () => {
205
+ const access = dam.createDirectoryAccess({
206
+ workingDirectory: projectDir,
207
+ readOnlyDirectories: [path.join(projectDir, 'docs')],
208
+ writeEnabledDirectories: [path.join(projectDir, 'src')]
209
+ });
210
+ const result = dam.getAccessibleDirectories(access);
211
+ expect(result.workingDirectory).toBe(projectDir);
212
+ expect(result.readOnly).toContain(projectDir); // workingDir added
213
+ expect(result.projectRestricted).toBe(true);
214
+ expect(result.systemAccessAllowed).toBe(false);
215
+ expect(typeof result.totalDirectories).toBe('number');
216
+ });
217
+
218
+ // ─── updateDirectoryAccess ─────────────────────────────────────
219
+
220
+ test('updateDirectoryAccess updates working directory', () => {
221
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
222
+ const updated = dam.updateDirectoryAccess(access, {
223
+ workingDirectory: '/tmp/new-project'
224
+ });
225
+ expect(updated.workingDirectory).toBe(path.resolve('/tmp/new-project'));
226
+ expect(updated.updatedAt).toBeDefined();
227
+ });
228
+
229
+ test('updateDirectoryAccess updates readOnlyDirectories', () => {
230
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
231
+ const updated = dam.updateDirectoryAccess(access, {
232
+ readOnlyDirectories: ['/tmp/docs']
233
+ });
234
+ // The stored path may be normalized differently per platform
235
+ const hasDocsPath = updated.readOnlyDirectories.some(d => d.includes('tmp') && d.includes('docs'));
236
+ expect(hasDocsPath).toBe(true);
237
+ });
238
+
239
+ test('updateDirectoryAccess updates writeEnabledDirectories', () => {
240
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
241
+ const updated = dam.updateDirectoryAccess(access, {
242
+ writeEnabledDirectories: [projectDir]
243
+ });
244
+ expect(updated.writeEnabledDirectories).toContain(projectDir);
245
+ });
246
+
247
+ test('updateDirectoryAccess updates boolean flags', () => {
248
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
249
+ const updated = dam.updateDirectoryAccess(access, {
250
+ restrictToProject: false,
251
+ allowSystemAccess: true
252
+ });
253
+ expect(updated.restrictToProject).toBe(false);
254
+ expect(updated.allowSystemAccess).toBe(true);
255
+ });
256
+
257
+ test('updateDirectoryAccess updates customRestrictions', () => {
258
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
259
+ const updated = dam.updateDirectoryAccess(access, {
260
+ customRestrictions: ['/tmp/restricted']
261
+ });
262
+ expect(updated.customRestrictions).toContain(path.resolve('/tmp/restricted'));
263
+ });
264
+
265
+ test('updateDirectoryAccess preserves version', () => {
266
+ const access = dam.createDirectoryAccess({ workingDirectory: projectDir });
267
+ const updated = dam.updateDirectoryAccess(access, {});
268
+ expect(updated.version).toBe('1.0');
269
+ });
270
+
271
+ // ─── validateAccessConfiguration ──────────────────────────────
272
+
273
+ test('validateAccessConfiguration validates valid config', () => {
274
+ const access = dam.createDirectoryAccess({
275
+ workingDirectory: projectDir,
276
+ writeEnabledDirectories: [projectDir]
277
+ });
278
+ const result = dam.validateAccessConfiguration(access);
279
+ expect(result.valid).toBe(true);
280
+ expect(result.errors).toHaveLength(0);
281
+ expect(result.summary).toBeDefined();
282
+ });
283
+
284
+ test('validateAccessConfiguration errors on missing workingDirectory', () => {
285
+ const config = {
286
+ readOnlyDirectories: [],
287
+ writeEnabledDirectories: []
288
+ };
289
+ const result = dam.validateAccessConfiguration(config);
290
+ expect(result.valid).toBe(false);
291
+ expect(result.errors.some(e => e.includes('Working directory'))).toBe(true);
292
+ });
293
+
294
+ test('validateAccessConfiguration errors on non-array directories', () => {
295
+ const result = dam.validateAccessConfiguration({
296
+ workingDirectory: projectDir,
297
+ readOnlyDirectories: 'not-array',
298
+ writeEnabledDirectories: 'not-array'
299
+ });
300
+ expect(result.errors.some(e => e.includes('readOnlyDirectories must be an array'))).toBe(true);
301
+ expect(result.errors.some(e => e.includes('writeEnabledDirectories must be an array'))).toBe(true);
302
+ });
303
+
304
+ test('validateAccessConfiguration warns on overlapping directories', () => {
305
+ const result = dam.validateAccessConfiguration({
306
+ workingDirectory: projectDir,
307
+ readOnlyDirectories: [projectDir],
308
+ writeEnabledDirectories: [projectDir],
309
+ allowSystemAccess: false,
310
+ restrictToProject: true
311
+ });
312
+ expect(result.warnings.some(w => w.includes('Overlapping'))).toBe(true);
313
+ });
314
+
315
+ test('validateAccessConfiguration warns on system access enabled', () => {
316
+ const result = dam.validateAccessConfiguration({
317
+ workingDirectory: projectDir,
318
+ readOnlyDirectories: [],
319
+ writeEnabledDirectories: [],
320
+ allowSystemAccess: true
321
+ });
322
+ expect(result.warnings.some(w => w.includes('System path access'))).toBe(true);
323
+ });
324
+
325
+ // ─── createRelativePath ────────────────────────────────────────
326
+
327
+ test('createRelativePath converts absolute to relative', () => {
328
+ const access = dam.createDirectoryAccess({
329
+ workingDirectory: projectDir,
330
+ writeEnabledDirectories: [projectDir]
331
+ });
332
+ const absPath = path.join(projectDir, 'src', 'index.js');
333
+ const result = dam.createRelativePath(absPath, access);
334
+ expect(result).toBe(path.join('src', 'index.js'));
335
+ });
336
+
337
+ test('createRelativePath returns original when not within any directory', () => {
338
+ const access = dam.createDirectoryAccess({
339
+ workingDirectory: projectDir,
340
+ readOnlyDirectories: [],
341
+ writeEnabledDirectories: []
342
+ });
343
+ // Clear auto-added dirs
344
+ access.readOnlyDirectories = [];
345
+ access.writeEnabledDirectories = [];
346
+ access.workingDirectory = '/nonexistent';
347
+ const absPath = '/completely/different/path.js';
348
+ const result = dam.createRelativePath(absPath, access);
349
+ expect(result).toBe(absPath);
350
+ });
351
+
352
+ // ─── getAccessSummary ──────────────────────────────────────────
353
+
354
+ test('getAccessSummary returns summary object', () => {
355
+ const access = dam.createDirectoryAccess({
356
+ workingDirectory: projectDir,
357
+ writeEnabledDirectories: [projectDir]
358
+ });
359
+ const summary = dam.getAccessSummary(access);
360
+ expect(summary.workingDirectory).toBe(projectDir);
361
+ expect(typeof summary.readOnlyCount).toBe('number');
362
+ expect(typeof summary.writeEnabledCount).toBe('number');
363
+ expect(typeof summary.projectRestricted).toBe('boolean');
364
+ expect(typeof summary.systemAccessAllowed).toBe('boolean');
365
+ expect(typeof summary.customRestrictionsCount).toBe('number');
366
+ expect(summary.configVersion).toBe('1.0');
367
+ expect(summary.lastUpdated).toBeDefined();
368
+ });
369
+
370
+ // ─── Static methods ───────────────────────────────────────────
371
+
372
+ test('createProjectDefaults returns config for given directory', () => {
373
+ const defaults = DirectoryAccessManager.createProjectDefaults(projectDir);
374
+ expect(defaults.workingDirectory).toBe(projectDir);
375
+ expect(defaults.readOnlyDirectories).toContain(projectDir);
376
+ expect(defaults.writeEnabledDirectories).toContain(projectDir);
377
+ expect(defaults.restrictToProject).toBe(true);
378
+ expect(defaults.allowSystemAccess).toBe(false);
379
+ });
380
+
381
+ test('createPermissiveDefaults returns permissive config', () => {
382
+ const defaults = DirectoryAccessManager.createPermissiveDefaults(projectDir);
383
+ expect(defaults.workingDirectory).toBe(projectDir);
384
+ expect(defaults.restrictToProject).toBe(false);
385
+ expect(defaults.allowSystemAccess).toBe(false);
386
+ expect(defaults.writeEnabledDirectories).toContain(projectDir);
387
+ });
388
+ });
@@ -0,0 +1,104 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
3
+
4
+ const fsMock = {
5
+ readFile: jest.fn(),
6
+ writeFile: jest.fn().mockResolvedValue(undefined),
7
+ unlink: jest.fn().mockResolvedValue(undefined),
8
+ mkdir: jest.fn().mockResolvedValue(undefined),
9
+ stat: jest.fn(),
10
+ access: jest.fn(),
11
+ copyFile: jest.fn().mockResolvedValue(undefined),
12
+ readdir: jest.fn().mockResolvedValue(['a.js', 'b.txt']),
13
+ rm: jest.fn().mockResolvedValue(undefined),
14
+ rename: jest.fn().mockResolvedValue(undefined)
15
+ };
16
+
17
+ jest.unstable_mockModule('fs/promises', () => ({ default: fsMock, ...fsMock }));
18
+
19
+ const { default: FileProcessor } = await import('../fileProcessor.js');
20
+
21
+ describe('FileProcessor', () => {
22
+ let fp;
23
+ let logger;
24
+
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ logger = createMockLogger();
28
+ fp = new FileProcessor({}, logger);
29
+ });
30
+
31
+ test('readFile calls fs.readFile and returns content', async () => {
32
+ fsMock.readFile.mockResolvedValue('hello world');
33
+ const result = await fp.readFile('/tmp/test.txt');
34
+ expect(fsMock.readFile).toHaveBeenCalledWith('/tmp/test.txt', 'utf8');
35
+ expect(result).toBe('hello world');
36
+ });
37
+
38
+ test('readFile propagates errors', async () => {
39
+ fsMock.readFile.mockRejectedValue(new Error('ENOENT'));
40
+ await expect(fp.readFile('/missing.txt')).rejects.toThrow('Failed to read file');
41
+ });
42
+
43
+ test('imageToBase64 returns data URI string starting with data:image/', async () => {
44
+ const buf = Buffer.from('fakepng');
45
+ fsMock.readFile.mockResolvedValue(buf);
46
+ const result = await fp.imageToBase64('/tmp/photo.png');
47
+ expect(result).toMatch(/^data:image\//);
48
+ expect(result).toContain(';base64,');
49
+ });
50
+
51
+ test('imageToBase64 maps .png to image/png and .jpg to image/jpeg', async () => {
52
+ const buf = Buffer.from('img');
53
+ fsMock.readFile.mockResolvedValue(buf);
54
+
55
+ const png = await fp.imageToBase64('/tmp/a.png');
56
+ expect(png).toMatch(/^data:image\/png;base64,/);
57
+
58
+ const jpg = await fp.imageToBase64('/tmp/b.jpg');
59
+ expect(jpg).toMatch(/^data:image\/jpeg;base64,/);
60
+ });
61
+
62
+ test('estimateTokens returns ~length/4 for regular text', () => {
63
+ const text = 'a'.repeat(100);
64
+ const tokens = fp.estimateTokens(text);
65
+ expect(tokens).toBe(Math.ceil(100 / 4));
66
+ });
67
+
68
+ test('estimateTokens returns ~length/1.5 for base64 images', () => {
69
+ const base64Part = 'A'.repeat(300);
70
+ const dataUri = `data:image/png;base64,${base64Part}`;
71
+ const tokens = fp.estimateTokens(dataUri);
72
+ expect(tokens).toBe(Math.ceil(300 / 1.5));
73
+ });
74
+
75
+ test('estimateTokens returns 0 for empty/null input', () => {
76
+ expect(fp.estimateTokens(null)).toBe(0);
77
+ expect(fp.estimateTokens('')).toBe(0);
78
+ expect(fp.estimateTokens(undefined)).toBe(0);
79
+ });
80
+
81
+ test('fileExists returns true when access succeeds', async () => {
82
+ fsMock.access.mockResolvedValue(undefined);
83
+ const result = await fp.fileExists('/tmp/exists.txt');
84
+ expect(result).toBe(true);
85
+ });
86
+
87
+ test('fileExists returns false when access throws', async () => {
88
+ fsMock.access.mockRejectedValue(new Error('ENOENT'));
89
+ const result = await fp.fileExists('/tmp/nope.txt');
90
+ expect(result).toBe(false);
91
+ });
92
+
93
+ test('writeFile creates parent directory before writing', async () => {
94
+ await fp.writeFile('/tmp/sub/dir/file.txt', 'content');
95
+
96
+ // mkdir should be called before writeFile
97
+ expect(fsMock.mkdir).toHaveBeenCalled();
98
+ expect(fsMock.writeFile).toHaveBeenCalled();
99
+
100
+ const mkdirCall = fsMock.mkdir.mock.invocationCallOrder[0];
101
+ const writeCall = fsMock.writeFile.mock.invocationCallOrder[0];
102
+ expect(mkdirCall).toBeLessThan(writeCall);
103
+ });
104
+ });
@@ -0,0 +1,104 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import {
3
+ parseJSONWithRepair,
4
+ looksLikeTruncatedJSON,
5
+ createTruncationNotice,
6
+ getFileExtension
7
+ } from '../jsonRepair.js';
8
+ import {
9
+ validJson,
10
+ trailingCommaObject,
11
+ truncatedObject,
12
+ truncatedString,
13
+ plainText,
14
+ emptyString
15
+ } from '../../__test-utils__/fixtures/malformedJson.js';
16
+
17
+ describe('jsonRepair', () => {
18
+ beforeEach(() => {
19
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
20
+ jest.spyOn(console, 'log').mockImplementation(() => {});
21
+ });
22
+
23
+ describe('parseJSONWithRepair', () => {
24
+ test('valid JSON returns data with wasRepaired=false', () => {
25
+ const result = parseJSONWithRepair(validJson);
26
+ expect(result.data).toEqual({ name: 'test', value: 42 });
27
+ expect(result.wasRepaired).toBe(false);
28
+ expect(result.wasTruncated).toBe(false);
29
+ expect(result.error).toBeNull();
30
+ });
31
+
32
+ test('trailing comma repairs successfully with wasRepaired=true', () => {
33
+ const result = parseJSONWithRepair(trailingCommaObject, { silent: true });
34
+ expect(result.data).toEqual({ a: 1, b: 2 });
35
+ expect(result.wasRepaired).toBe(true);
36
+ expect(result.error).toBeNull();
37
+ });
38
+
39
+ test('truncated JSON sets wasTruncated=true', () => {
40
+ const result = parseJSONWithRepair(truncatedObject, { silent: true });
41
+ expect(result.wasRepaired).toBe(true);
42
+ expect(result.wasTruncated).toBe(true);
43
+ expect(result.data).not.toBeNull();
44
+ expect(result.error).toBeNull();
45
+ });
46
+
47
+ test('completely invalid input returns error or repaired result', () => {
48
+ // jsonrepair may be able to "repair" some plain text by treating it as a value
49
+ const result = parseJSONWithRepair(plainText, { silent: true });
50
+ // Either it was repaired successfully or it returned an error
51
+ if (result.error) {
52
+ expect(result.data).toBeNull();
53
+ expect(result.error).toHaveProperty('originalError');
54
+ } else {
55
+ // jsonrepair managed to parse it somehow
56
+ expect(result.data).not.toBeUndefined();
57
+ }
58
+ });
59
+
60
+ test('null/undefined input handles gracefully', () => {
61
+ // JSON.parse(null) returns null, so parseJSONWithRepair(null) succeeds with data=null
62
+ const nullResult = parseJSONWithRepair(null, { silent: true });
63
+ // Should not throw — returns a result object
64
+ expect(nullResult).toHaveProperty('wasRepaired');
65
+ expect(nullResult).toHaveProperty('error');
66
+ });
67
+ });
68
+
69
+ describe('looksLikeTruncatedJSON', () => {
70
+ test('unclosed bracket returns true', () => {
71
+ expect(looksLikeTruncatedJSON('{"key": "value"')).toBe(true);
72
+ expect(looksLikeTruncatedJSON('[1, 2, 3')).toBe(true);
73
+ expect(looksLikeTruncatedJSON(truncatedString)).toBe(true);
74
+ });
75
+
76
+ test('complete JSON returns false', () => {
77
+ expect(looksLikeTruncatedJSON('{"key": "value"}')).toBe(false);
78
+ expect(looksLikeTruncatedJSON('[1, 2, 3]')).toBe(false);
79
+ expect(looksLikeTruncatedJSON('{}')).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe('createTruncationNotice', () => {
84
+ test('returns appropriate comment for each file type', () => {
85
+ expect(createTruncationNotice('js')).toContain('//');
86
+ expect(createTruncationNotice('css')).toContain('/*');
87
+ expect(createTruncationNotice('html')).toContain('<!--');
88
+ expect(createTruncationNotice('py')).toContain('#');
89
+ // Note: json returns '' which is falsy, so || falls through to default
90
+ expect(createTruncationNotice('json')).toContain('[CONTENT TRUNCATED]');
91
+ expect(createTruncationNotice('unknown')).toContain('[CONTENT TRUNCATED]');
92
+ });
93
+ });
94
+
95
+ describe('getFileExtension', () => {
96
+ test('extracts extension correctly for various paths', () => {
97
+ expect(getFileExtension('file.js')).toBe('js');
98
+ expect(getFileExtension('path/to/file.test.ts')).toBe('ts');
99
+ expect(getFileExtension('document.PDF')).toBe('pdf');
100
+ expect(getFileExtension('noext')).toBe('');
101
+ expect(getFileExtension('/some/path/file.json')).toBe('json');
102
+ });
103
+ });
104
+ });