onbuzz 3.6.1 → 3.6.3
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__/fileSystemTool.test.js +717 -0
- package/src/tools/__tests__/fileTreeTool.test.js +274 -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
- /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
|
@@ -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
|
+
});
|