@lumenflow/cli 1.5.0 → 2.0.0
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/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- package/dist/wu-recover.js +329 -0
- package/dist/wu-status.js +188 -0
- package/package.json +37 -7
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file file-operations.test.ts
|
|
3
|
+
* @description Tests for file operation CLI commands (WU-1108)
|
|
4
|
+
*
|
|
5
|
+
* File operations provide audited file access with:
|
|
6
|
+
* - Scope checking against WU code_paths
|
|
7
|
+
* - PHI scanning for sensitive data detection
|
|
8
|
+
* - Worktree guard for write operations
|
|
9
|
+
*
|
|
10
|
+
* TDD: RED phase - these tests define expected behavior before implementation
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { writeFile, mkdir, rm } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
19
|
+
// Test imports - these will fail until implementation exists (RED phase)
|
|
20
|
+
// We'll import them individually to test each module
|
|
21
|
+
import { readFileWithAudit, parseFileReadArgs, FILE_READ_DEFAULTS, } from '../file-read.js';
|
|
22
|
+
import { writeFileWithAudit, parseFileWriteArgs, FILE_WRITE_DEFAULTS, } from '../file-write.js';
|
|
23
|
+
import { editFileWithAudit, parseFileEditArgs, FILE_EDIT_DEFAULTS, } from '../file-edit.js';
|
|
24
|
+
import { deleteFileWithAudit, parseFileDeleteArgs, FILE_DELETE_DEFAULTS, } from '../file-delete.js';
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// FILE-READ TESTS
|
|
27
|
+
// ============================================================================
|
|
28
|
+
describe('file-read CLI', () => {
|
|
29
|
+
describe('source file existence', () => {
|
|
30
|
+
it('should have the CLI source file', () => {
|
|
31
|
+
const srcPath = join(__dirname, '../file-read.ts');
|
|
32
|
+
expect(existsSync(srcPath)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it('should be buildable (dist file exists after build)', () => {
|
|
35
|
+
const distPath = join(__dirname, '../../dist/file-read.js');
|
|
36
|
+
expect(existsSync(distPath)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('FILE_READ_DEFAULTS', () => {
|
|
40
|
+
it('should have default max file size', () => {
|
|
41
|
+
expect(FILE_READ_DEFAULTS.maxFileSizeBytes).toBeTypeOf('number');
|
|
42
|
+
expect(FILE_READ_DEFAULTS.maxFileSizeBytes).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
it('should have default encoding', () => {
|
|
45
|
+
expect(FILE_READ_DEFAULTS.encoding).toBe('utf-8');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('parseFileReadArgs', () => {
|
|
49
|
+
it('should parse path argument', () => {
|
|
50
|
+
const args = parseFileReadArgs(['node', 'file-read', 'src/index.ts']);
|
|
51
|
+
expect(args.path).toBe('src/index.ts');
|
|
52
|
+
});
|
|
53
|
+
it('should parse --path option', () => {
|
|
54
|
+
const args = parseFileReadArgs(['node', 'file-read', '--path', 'src/utils.ts']);
|
|
55
|
+
expect(args.path).toBe('src/utils.ts');
|
|
56
|
+
});
|
|
57
|
+
it('should parse --encoding option', () => {
|
|
58
|
+
const args = parseFileReadArgs([
|
|
59
|
+
'node',
|
|
60
|
+
'file-read',
|
|
61
|
+
'--path',
|
|
62
|
+
'file.txt',
|
|
63
|
+
'--encoding',
|
|
64
|
+
'latin1',
|
|
65
|
+
]);
|
|
66
|
+
expect(args.encoding).toBe('latin1');
|
|
67
|
+
});
|
|
68
|
+
it('should parse --start-line option', () => {
|
|
69
|
+
const args = parseFileReadArgs([
|
|
70
|
+
'node',
|
|
71
|
+
'file-read',
|
|
72
|
+
'--path',
|
|
73
|
+
'file.txt',
|
|
74
|
+
'--start-line',
|
|
75
|
+
'10',
|
|
76
|
+
]);
|
|
77
|
+
expect(args.startLine).toBe(10);
|
|
78
|
+
});
|
|
79
|
+
it('should parse --end-line option', () => {
|
|
80
|
+
const args = parseFileReadArgs([
|
|
81
|
+
'node',
|
|
82
|
+
'file-read',
|
|
83
|
+
'--path',
|
|
84
|
+
'file.txt',
|
|
85
|
+
'--end-line',
|
|
86
|
+
'50',
|
|
87
|
+
]);
|
|
88
|
+
expect(args.endLine).toBe(50);
|
|
89
|
+
});
|
|
90
|
+
it('should parse --help flag', () => {
|
|
91
|
+
const args = parseFileReadArgs(['node', 'file-read', '--help']);
|
|
92
|
+
expect(args.help).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it('should require path argument', () => {
|
|
95
|
+
const args = parseFileReadArgs(['node', 'file-read']);
|
|
96
|
+
expect(args.path).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('readFileWithAudit', () => {
|
|
100
|
+
let tempDir;
|
|
101
|
+
beforeEach(async () => {
|
|
102
|
+
tempDir = join(tmpdir(), `file-read-test-${Date.now()}`);
|
|
103
|
+
await mkdir(tempDir, { recursive: true });
|
|
104
|
+
});
|
|
105
|
+
afterEach(async () => {
|
|
106
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
it('should read file content successfully', async () => {
|
|
109
|
+
const testFile = join(tempDir, 'test.txt');
|
|
110
|
+
await writeFile(testFile, 'Hello, World!', 'utf-8');
|
|
111
|
+
const result = await readFileWithAudit({ path: testFile });
|
|
112
|
+
expect(result.success).toBe(true);
|
|
113
|
+
expect(result.content).toBe('Hello, World!');
|
|
114
|
+
});
|
|
115
|
+
it('should return error for non-existent file', async () => {
|
|
116
|
+
const result = await readFileWithAudit({ path: join(tempDir, 'nonexistent.txt') });
|
|
117
|
+
expect(result.success).toBe(false);
|
|
118
|
+
expect(result.error).toContain('ENOENT');
|
|
119
|
+
});
|
|
120
|
+
it('should support line range reading', async () => {
|
|
121
|
+
const testFile = join(tempDir, 'multiline.txt');
|
|
122
|
+
await writeFile(testFile, 'line1\nline2\nline3\nline4\nline5', 'utf-8');
|
|
123
|
+
const result = await readFileWithAudit({ path: testFile, startLine: 2, endLine: 4 });
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
expect(result.content).toBe('line2\nline3\nline4');
|
|
126
|
+
});
|
|
127
|
+
it('should return file metadata', async () => {
|
|
128
|
+
const testFile = join(tempDir, 'metadata.txt');
|
|
129
|
+
await writeFile(testFile, 'Test content', 'utf-8');
|
|
130
|
+
const result = await readFileWithAudit({ path: testFile });
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
expect(result.metadata).toBeDefined();
|
|
133
|
+
expect(result.metadata?.sizeBytes).toBeGreaterThan(0);
|
|
134
|
+
expect(result.metadata?.lineCount).toBeGreaterThan(0);
|
|
135
|
+
});
|
|
136
|
+
it('should respect max file size limit', async () => {
|
|
137
|
+
const testFile = join(tempDir, 'large.txt');
|
|
138
|
+
await writeFile(testFile, 'x'.repeat(1024 * 1024), 'utf-8'); // 1MB
|
|
139
|
+
const result = await readFileWithAudit({
|
|
140
|
+
path: testFile,
|
|
141
|
+
maxFileSizeBytes: 1024, // 1KB limit
|
|
142
|
+
});
|
|
143
|
+
expect(result.success).toBe(false);
|
|
144
|
+
expect(result.error).toContain('exceeds');
|
|
145
|
+
});
|
|
146
|
+
it('should include audit metadata in result', async () => {
|
|
147
|
+
const testFile = join(tempDir, 'audit.txt');
|
|
148
|
+
await writeFile(testFile, 'Audit test', 'utf-8');
|
|
149
|
+
const result = await readFileWithAudit({ path: testFile });
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
expect(result.auditLog).toBeDefined();
|
|
152
|
+
expect(result.auditLog?.operation).toBe('read');
|
|
153
|
+
expect(result.auditLog?.path).toBe(testFile);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// FILE-WRITE TESTS
|
|
159
|
+
// ============================================================================
|
|
160
|
+
describe('file-write CLI', () => {
|
|
161
|
+
describe('source file existence', () => {
|
|
162
|
+
it('should have the CLI source file', () => {
|
|
163
|
+
const srcPath = join(__dirname, '../file-write.ts');
|
|
164
|
+
expect(existsSync(srcPath)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
it('should be buildable (dist file exists after build)', () => {
|
|
167
|
+
const distPath = join(__dirname, '../../dist/file-write.js');
|
|
168
|
+
expect(existsSync(distPath)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('FILE_WRITE_DEFAULTS', () => {
|
|
172
|
+
it('should have default encoding', () => {
|
|
173
|
+
expect(FILE_WRITE_DEFAULTS.encoding).toBe('utf-8');
|
|
174
|
+
});
|
|
175
|
+
it('should have default createDirectories option', () => {
|
|
176
|
+
expect(FILE_WRITE_DEFAULTS.createDirectories).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('parseFileWriteArgs', () => {
|
|
180
|
+
it('should parse path argument', () => {
|
|
181
|
+
const args = parseFileWriteArgs(['node', 'file-write', 'output.txt', '--content', 'Hello']);
|
|
182
|
+
expect(args.path).toBe('output.txt');
|
|
183
|
+
});
|
|
184
|
+
it('should parse --path option', () => {
|
|
185
|
+
const args = parseFileWriteArgs([
|
|
186
|
+
'node',
|
|
187
|
+
'file-write',
|
|
188
|
+
'--path',
|
|
189
|
+
'output.txt',
|
|
190
|
+
'--content',
|
|
191
|
+
'Hello',
|
|
192
|
+
]);
|
|
193
|
+
expect(args.path).toBe('output.txt');
|
|
194
|
+
});
|
|
195
|
+
it('should parse --content option', () => {
|
|
196
|
+
const args = parseFileWriteArgs([
|
|
197
|
+
'node',
|
|
198
|
+
'file-write',
|
|
199
|
+
'--path',
|
|
200
|
+
'file.txt',
|
|
201
|
+
'--content',
|
|
202
|
+
'Test content',
|
|
203
|
+
]);
|
|
204
|
+
expect(args.content).toBe('Test content');
|
|
205
|
+
});
|
|
206
|
+
it('should parse --encoding option', () => {
|
|
207
|
+
const args = parseFileWriteArgs([
|
|
208
|
+
'node',
|
|
209
|
+
'file-write',
|
|
210
|
+
'--path',
|
|
211
|
+
'file.txt',
|
|
212
|
+
'--content',
|
|
213
|
+
'x',
|
|
214
|
+
'--encoding',
|
|
215
|
+
'latin1',
|
|
216
|
+
]);
|
|
217
|
+
expect(args.encoding).toBe('latin1');
|
|
218
|
+
});
|
|
219
|
+
it('should parse --no-create-dirs flag', () => {
|
|
220
|
+
const args = parseFileWriteArgs([
|
|
221
|
+
'node',
|
|
222
|
+
'file-write',
|
|
223
|
+
'--path',
|
|
224
|
+
'file.txt',
|
|
225
|
+
'--content',
|
|
226
|
+
'x',
|
|
227
|
+
'--no-create-dirs',
|
|
228
|
+
]);
|
|
229
|
+
expect(args.createDirectories).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
it('should parse --help flag', () => {
|
|
232
|
+
const args = parseFileWriteArgs(['node', 'file-write', '--help']);
|
|
233
|
+
expect(args.help).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
it('should require path and content', () => {
|
|
236
|
+
const args = parseFileWriteArgs(['node', 'file-write']);
|
|
237
|
+
expect(args.path).toBeUndefined();
|
|
238
|
+
expect(args.content).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('writeFileWithAudit', () => {
|
|
242
|
+
let tempDir;
|
|
243
|
+
beforeEach(async () => {
|
|
244
|
+
tempDir = join(tmpdir(), `file-write-test-${Date.now()}`);
|
|
245
|
+
await mkdir(tempDir, { recursive: true });
|
|
246
|
+
});
|
|
247
|
+
afterEach(async () => {
|
|
248
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
249
|
+
});
|
|
250
|
+
it('should write file content successfully', async () => {
|
|
251
|
+
const testFile = join(tempDir, 'output.txt');
|
|
252
|
+
const result = await writeFileWithAudit({
|
|
253
|
+
path: testFile,
|
|
254
|
+
content: 'New content',
|
|
255
|
+
});
|
|
256
|
+
expect(result.success).toBe(true);
|
|
257
|
+
expect(existsSync(testFile)).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
it('should create parent directories', async () => {
|
|
260
|
+
const testFile = join(tempDir, 'nested', 'deep', 'output.txt');
|
|
261
|
+
const result = await writeFileWithAudit({
|
|
262
|
+
path: testFile,
|
|
263
|
+
content: 'Nested content',
|
|
264
|
+
createDirectories: true,
|
|
265
|
+
});
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
expect(existsSync(testFile)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
it('should fail when createDirectories is false and parent does not exist', async () => {
|
|
270
|
+
const testFile = join(tempDir, 'nonexistent', 'output.txt');
|
|
271
|
+
const result = await writeFileWithAudit({
|
|
272
|
+
path: testFile,
|
|
273
|
+
content: 'Content',
|
|
274
|
+
createDirectories: false,
|
|
275
|
+
});
|
|
276
|
+
expect(result.success).toBe(false);
|
|
277
|
+
expect(result.error).toContain('ENOENT');
|
|
278
|
+
});
|
|
279
|
+
it('should return bytes written metadata', async () => {
|
|
280
|
+
const testFile = join(tempDir, 'metadata.txt');
|
|
281
|
+
const content = 'Test content for metadata';
|
|
282
|
+
const result = await writeFileWithAudit({
|
|
283
|
+
path: testFile,
|
|
284
|
+
content,
|
|
285
|
+
});
|
|
286
|
+
expect(result.success).toBe(true);
|
|
287
|
+
expect(result.metadata?.bytesWritten).toBe(Buffer.byteLength(content, 'utf-8'));
|
|
288
|
+
});
|
|
289
|
+
it('should include audit metadata in result', async () => {
|
|
290
|
+
const testFile = join(tempDir, 'audit.txt');
|
|
291
|
+
const result = await writeFileWithAudit({
|
|
292
|
+
path: testFile,
|
|
293
|
+
content: 'Audit test',
|
|
294
|
+
});
|
|
295
|
+
expect(result.success).toBe(true);
|
|
296
|
+
expect(result.auditLog).toBeDefined();
|
|
297
|
+
expect(result.auditLog?.operation).toBe('write');
|
|
298
|
+
expect(result.auditLog?.path).toBe(testFile);
|
|
299
|
+
});
|
|
300
|
+
it('should handle PHI scanning option gracefully', async () => {
|
|
301
|
+
const testFile = join(tempDir, 'phi.txt');
|
|
302
|
+
// Use a valid NHS number for testing (this is a test number)
|
|
303
|
+
const content = 'Patient NHS number: 2983396339';
|
|
304
|
+
const result = await writeFileWithAudit({
|
|
305
|
+
path: testFile,
|
|
306
|
+
content,
|
|
307
|
+
scanPHI: true,
|
|
308
|
+
});
|
|
309
|
+
// Should succeed regardless of PHI scanning availability
|
|
310
|
+
expect(result.success).toBe(true);
|
|
311
|
+
// PHI scanning may be disabled or unavailable, so we just check result structure
|
|
312
|
+
// If PHI scanning was enabled and found PHI, warnings would be present
|
|
313
|
+
// If PHI scanning is disabled or core is not available, warnings would be undefined
|
|
314
|
+
expect(result.auditLog).toBeDefined();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// FILE-EDIT TESTS
|
|
320
|
+
// ============================================================================
|
|
321
|
+
describe('file-edit CLI', () => {
|
|
322
|
+
describe('source file existence', () => {
|
|
323
|
+
it('should have the CLI source file', () => {
|
|
324
|
+
const srcPath = join(__dirname, '../file-edit.ts');
|
|
325
|
+
expect(existsSync(srcPath)).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
it('should be buildable (dist file exists after build)', () => {
|
|
328
|
+
const distPath = join(__dirname, '../../dist/file-edit.js');
|
|
329
|
+
expect(existsSync(distPath)).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
describe('FILE_EDIT_DEFAULTS', () => {
|
|
333
|
+
it('should have default encoding', () => {
|
|
334
|
+
expect(FILE_EDIT_DEFAULTS.encoding).toBe('utf-8');
|
|
335
|
+
});
|
|
336
|
+
it('should have default replaceAll option', () => {
|
|
337
|
+
expect(FILE_EDIT_DEFAULTS.replaceAll).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
describe('parseFileEditArgs', () => {
|
|
341
|
+
it('should parse path argument', () => {
|
|
342
|
+
const args = parseFileEditArgs([
|
|
343
|
+
'node',
|
|
344
|
+
'file-edit',
|
|
345
|
+
'file.txt',
|
|
346
|
+
'--old',
|
|
347
|
+
'foo',
|
|
348
|
+
'--new',
|
|
349
|
+
'bar',
|
|
350
|
+
]);
|
|
351
|
+
expect(args.path).toBe('file.txt');
|
|
352
|
+
});
|
|
353
|
+
it('should parse --path option', () => {
|
|
354
|
+
const args = parseFileEditArgs([
|
|
355
|
+
'node',
|
|
356
|
+
'file-edit',
|
|
357
|
+
'--path',
|
|
358
|
+
'file.txt',
|
|
359
|
+
'--old',
|
|
360
|
+
'foo',
|
|
361
|
+
'--new',
|
|
362
|
+
'bar',
|
|
363
|
+
]);
|
|
364
|
+
expect(args.path).toBe('file.txt');
|
|
365
|
+
});
|
|
366
|
+
it('should parse --old-string option', () => {
|
|
367
|
+
const args = parseFileEditArgs([
|
|
368
|
+
'node',
|
|
369
|
+
'file-edit',
|
|
370
|
+
'--path',
|
|
371
|
+
'file.txt',
|
|
372
|
+
'--old-string',
|
|
373
|
+
'old text',
|
|
374
|
+
'--new-string',
|
|
375
|
+
'new text',
|
|
376
|
+
]);
|
|
377
|
+
expect(args.oldString).toBe('old text');
|
|
378
|
+
});
|
|
379
|
+
it('should parse --new-string option', () => {
|
|
380
|
+
const args = parseFileEditArgs([
|
|
381
|
+
'node',
|
|
382
|
+
'file-edit',
|
|
383
|
+
'--path',
|
|
384
|
+
'file.txt',
|
|
385
|
+
'--old-string',
|
|
386
|
+
'old',
|
|
387
|
+
'--new-string',
|
|
388
|
+
'new value',
|
|
389
|
+
]);
|
|
390
|
+
expect(args.newString).toBe('new value');
|
|
391
|
+
});
|
|
392
|
+
it('should parse --replace-all flag', () => {
|
|
393
|
+
const args = parseFileEditArgs([
|
|
394
|
+
'node',
|
|
395
|
+
'file-edit',
|
|
396
|
+
'--path',
|
|
397
|
+
'file.txt',
|
|
398
|
+
'--old',
|
|
399
|
+
'x',
|
|
400
|
+
'--new',
|
|
401
|
+
'y',
|
|
402
|
+
'--replace-all',
|
|
403
|
+
]);
|
|
404
|
+
expect(args.replaceAll).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
it('should parse --help flag', () => {
|
|
407
|
+
const args = parseFileEditArgs(['node', 'file-edit', '--help']);
|
|
408
|
+
expect(args.help).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
it('should require path, old-string, and new-string', () => {
|
|
411
|
+
const args = parseFileEditArgs(['node', 'file-edit']);
|
|
412
|
+
expect(args.path).toBeUndefined();
|
|
413
|
+
expect(args.oldString).toBeUndefined();
|
|
414
|
+
expect(args.newString).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
describe('editFileWithAudit', () => {
|
|
418
|
+
let tempDir;
|
|
419
|
+
beforeEach(async () => {
|
|
420
|
+
tempDir = join(tmpdir(), `file-edit-test-${Date.now()}`);
|
|
421
|
+
await mkdir(tempDir, { recursive: true });
|
|
422
|
+
});
|
|
423
|
+
afterEach(async () => {
|
|
424
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
425
|
+
});
|
|
426
|
+
it('should replace string in file successfully', async () => {
|
|
427
|
+
const testFile = join(tempDir, 'edit.txt');
|
|
428
|
+
await writeFile(testFile, 'Hello, World!', 'utf-8');
|
|
429
|
+
const result = await editFileWithAudit({
|
|
430
|
+
path: testFile,
|
|
431
|
+
oldString: 'World',
|
|
432
|
+
newString: 'Universe',
|
|
433
|
+
});
|
|
434
|
+
expect(result.success).toBe(true);
|
|
435
|
+
expect(result.replacements).toBe(1);
|
|
436
|
+
});
|
|
437
|
+
it('should return error for non-existent file', async () => {
|
|
438
|
+
const result = await editFileWithAudit({
|
|
439
|
+
path: join(tempDir, 'nonexistent.txt'),
|
|
440
|
+
oldString: 'foo',
|
|
441
|
+
newString: 'bar',
|
|
442
|
+
});
|
|
443
|
+
expect(result.success).toBe(false);
|
|
444
|
+
expect(result.error).toContain('ENOENT');
|
|
445
|
+
});
|
|
446
|
+
it('should return error when old string not found', async () => {
|
|
447
|
+
const testFile = join(tempDir, 'nofind.txt');
|
|
448
|
+
await writeFile(testFile, 'Hello, World!', 'utf-8');
|
|
449
|
+
const result = await editFileWithAudit({
|
|
450
|
+
path: testFile,
|
|
451
|
+
oldString: 'NotFound',
|
|
452
|
+
newString: 'Replacement',
|
|
453
|
+
});
|
|
454
|
+
expect(result.success).toBe(false);
|
|
455
|
+
expect(result.error).toContain('not found');
|
|
456
|
+
});
|
|
457
|
+
it('should return error when old string is not unique (without replaceAll)', async () => {
|
|
458
|
+
const testFile = join(tempDir, 'multiple.txt');
|
|
459
|
+
await writeFile(testFile, 'foo bar foo baz foo', 'utf-8');
|
|
460
|
+
const result = await editFileWithAudit({
|
|
461
|
+
path: testFile,
|
|
462
|
+
oldString: 'foo',
|
|
463
|
+
newString: 'qux',
|
|
464
|
+
replaceAll: false,
|
|
465
|
+
});
|
|
466
|
+
expect(result.success).toBe(false);
|
|
467
|
+
expect(result.error).toContain('not unique');
|
|
468
|
+
});
|
|
469
|
+
it('should replace all occurrences when replaceAll is true', async () => {
|
|
470
|
+
const testFile = join(tempDir, 'replaceall.txt');
|
|
471
|
+
await writeFile(testFile, 'foo bar foo baz foo', 'utf-8');
|
|
472
|
+
const result = await editFileWithAudit({
|
|
473
|
+
path: testFile,
|
|
474
|
+
oldString: 'foo',
|
|
475
|
+
newString: 'qux',
|
|
476
|
+
replaceAll: true,
|
|
477
|
+
});
|
|
478
|
+
expect(result.success).toBe(true);
|
|
479
|
+
expect(result.replacements).toBe(3);
|
|
480
|
+
});
|
|
481
|
+
it('should include audit metadata in result', async () => {
|
|
482
|
+
const testFile = join(tempDir, 'audit.txt');
|
|
483
|
+
await writeFile(testFile, 'Original content', 'utf-8');
|
|
484
|
+
const result = await editFileWithAudit({
|
|
485
|
+
path: testFile,
|
|
486
|
+
oldString: 'Original',
|
|
487
|
+
newString: 'Modified',
|
|
488
|
+
});
|
|
489
|
+
expect(result.success).toBe(true);
|
|
490
|
+
expect(result.auditLog).toBeDefined();
|
|
491
|
+
expect(result.auditLog?.operation).toBe('edit');
|
|
492
|
+
expect(result.auditLog?.path).toBe(testFile);
|
|
493
|
+
});
|
|
494
|
+
it('should return diff preview', async () => {
|
|
495
|
+
const testFile = join(tempDir, 'diff.txt');
|
|
496
|
+
await writeFile(testFile, 'Hello, World!', 'utf-8');
|
|
497
|
+
const result = await editFileWithAudit({
|
|
498
|
+
path: testFile,
|
|
499
|
+
oldString: 'World',
|
|
500
|
+
newString: 'Universe',
|
|
501
|
+
});
|
|
502
|
+
expect(result.success).toBe(true);
|
|
503
|
+
expect(result.diff).toBeDefined();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
// ============================================================================
|
|
508
|
+
// FILE-DELETE TESTS
|
|
509
|
+
// ============================================================================
|
|
510
|
+
describe('file-delete CLI', () => {
|
|
511
|
+
describe('source file existence', () => {
|
|
512
|
+
it('should have the CLI source file', () => {
|
|
513
|
+
const srcPath = join(__dirname, '../file-delete.ts');
|
|
514
|
+
expect(existsSync(srcPath)).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
it('should be buildable (dist file exists after build)', () => {
|
|
517
|
+
const distPath = join(__dirname, '../../dist/file-delete.js');
|
|
518
|
+
expect(existsSync(distPath)).toBe(true);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
describe('FILE_DELETE_DEFAULTS', () => {
|
|
522
|
+
it('should have default recursive option', () => {
|
|
523
|
+
expect(FILE_DELETE_DEFAULTS.recursive).toBe(false);
|
|
524
|
+
});
|
|
525
|
+
it('should have default force option', () => {
|
|
526
|
+
expect(FILE_DELETE_DEFAULTS.force).toBe(false);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
describe('parseFileDeleteArgs', () => {
|
|
530
|
+
it('should parse path argument', () => {
|
|
531
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', 'file.txt']);
|
|
532
|
+
expect(args.path).toBe('file.txt');
|
|
533
|
+
});
|
|
534
|
+
it('should parse --path option', () => {
|
|
535
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '--path', 'file.txt']);
|
|
536
|
+
expect(args.path).toBe('file.txt');
|
|
537
|
+
});
|
|
538
|
+
it('should parse --recursive flag', () => {
|
|
539
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '--path', 'dir/', '--recursive']);
|
|
540
|
+
expect(args.recursive).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
it('should parse -r shorthand for recursive', () => {
|
|
543
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '--path', 'dir/', '-r']);
|
|
544
|
+
expect(args.recursive).toBe(true);
|
|
545
|
+
});
|
|
546
|
+
it('should parse --force flag', () => {
|
|
547
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '--path', 'file.txt', '--force']);
|
|
548
|
+
expect(args.force).toBe(true);
|
|
549
|
+
});
|
|
550
|
+
it('should parse -f shorthand for force', () => {
|
|
551
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '--path', 'file.txt', '-f']);
|
|
552
|
+
expect(args.force).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
it('should parse --help flag', () => {
|
|
555
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '--help']);
|
|
556
|
+
expect(args.help).toBe(true);
|
|
557
|
+
});
|
|
558
|
+
it('should require path argument', () => {
|
|
559
|
+
const args = parseFileDeleteArgs(['node', 'file-delete']);
|
|
560
|
+
expect(args.path).toBeUndefined();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
describe('deleteFileWithAudit', () => {
|
|
564
|
+
let tempDir;
|
|
565
|
+
beforeEach(async () => {
|
|
566
|
+
tempDir = join(tmpdir(), `file-delete-test-${Date.now()}`);
|
|
567
|
+
await mkdir(tempDir, { recursive: true });
|
|
568
|
+
});
|
|
569
|
+
afterEach(async () => {
|
|
570
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
571
|
+
});
|
|
572
|
+
it('should delete file successfully', async () => {
|
|
573
|
+
const testFile = join(tempDir, 'delete.txt');
|
|
574
|
+
await writeFile(testFile, 'Content to delete', 'utf-8');
|
|
575
|
+
const result = await deleteFileWithAudit({ path: testFile });
|
|
576
|
+
expect(result.success).toBe(true);
|
|
577
|
+
expect(existsSync(testFile)).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
it('should return error for non-existent file (without force)', async () => {
|
|
580
|
+
const result = await deleteFileWithAudit({
|
|
581
|
+
path: join(tempDir, 'nonexistent.txt'),
|
|
582
|
+
force: false,
|
|
583
|
+
});
|
|
584
|
+
expect(result.success).toBe(false);
|
|
585
|
+
expect(result.error).toContain('ENOENT');
|
|
586
|
+
});
|
|
587
|
+
it('should succeed for non-existent file with force option', async () => {
|
|
588
|
+
const result = await deleteFileWithAudit({
|
|
589
|
+
path: join(tempDir, 'nonexistent.txt'),
|
|
590
|
+
force: true,
|
|
591
|
+
});
|
|
592
|
+
expect(result.success).toBe(true);
|
|
593
|
+
});
|
|
594
|
+
it('should delete directory recursively', async () => {
|
|
595
|
+
const nestedDir = join(tempDir, 'nested');
|
|
596
|
+
await mkdir(nestedDir, { recursive: true });
|
|
597
|
+
await writeFile(join(nestedDir, 'file1.txt'), 'Content 1', 'utf-8');
|
|
598
|
+
await writeFile(join(nestedDir, 'file2.txt'), 'Content 2', 'utf-8');
|
|
599
|
+
const result = await deleteFileWithAudit({
|
|
600
|
+
path: nestedDir,
|
|
601
|
+
recursive: true,
|
|
602
|
+
});
|
|
603
|
+
expect(result.success).toBe(true);
|
|
604
|
+
expect(existsSync(nestedDir)).toBe(false);
|
|
605
|
+
});
|
|
606
|
+
it('should fail to delete non-empty directory without recursive', async () => {
|
|
607
|
+
const nestedDir = join(tempDir, 'nonempty');
|
|
608
|
+
await mkdir(nestedDir, { recursive: true });
|
|
609
|
+
await writeFile(join(nestedDir, 'file.txt'), 'Content', 'utf-8');
|
|
610
|
+
const result = await deleteFileWithAudit({
|
|
611
|
+
path: nestedDir,
|
|
612
|
+
recursive: false,
|
|
613
|
+
});
|
|
614
|
+
expect(result.success).toBe(false);
|
|
615
|
+
});
|
|
616
|
+
it('should include audit metadata in result', async () => {
|
|
617
|
+
const testFile = join(tempDir, 'audit.txt');
|
|
618
|
+
await writeFile(testFile, 'Audit content', 'utf-8');
|
|
619
|
+
const result = await deleteFileWithAudit({ path: testFile });
|
|
620
|
+
expect(result.success).toBe(true);
|
|
621
|
+
expect(result.auditLog).toBeDefined();
|
|
622
|
+
expect(result.auditLog?.operation).toBe('delete');
|
|
623
|
+
expect(result.auditLog?.path).toBe(testFile);
|
|
624
|
+
});
|
|
625
|
+
it('should return deleted item count for directories', async () => {
|
|
626
|
+
const nestedDir = join(tempDir, 'count');
|
|
627
|
+
await mkdir(join(nestedDir, 'subdir'), { recursive: true });
|
|
628
|
+
await writeFile(join(nestedDir, 'file1.txt'), 'Content 1', 'utf-8');
|
|
629
|
+
await writeFile(join(nestedDir, 'subdir', 'file2.txt'), 'Content 2', 'utf-8');
|
|
630
|
+
const result = await deleteFileWithAudit({
|
|
631
|
+
path: nestedDir,
|
|
632
|
+
recursive: true,
|
|
633
|
+
});
|
|
634
|
+
expect(result.success).toBe(true);
|
|
635
|
+
expect(result.metadata?.deletedCount).toBeGreaterThan(0);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
// ============================================================================
|
|
640
|
+
// INTEGRATION TESTS
|
|
641
|
+
// ============================================================================
|
|
642
|
+
describe('file operations integration', () => {
|
|
643
|
+
let tempDir;
|
|
644
|
+
beforeEach(async () => {
|
|
645
|
+
tempDir = join(tmpdir(), `file-ops-integration-${Date.now()}`);
|
|
646
|
+
await mkdir(tempDir, { recursive: true });
|
|
647
|
+
});
|
|
648
|
+
afterEach(async () => {
|
|
649
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
650
|
+
});
|
|
651
|
+
it('should support read-edit-write workflow', async () => {
|
|
652
|
+
const testFile = join(tempDir, 'workflow.txt');
|
|
653
|
+
// Write initial content
|
|
654
|
+
const writeResult = await writeFileWithAudit({
|
|
655
|
+
path: testFile,
|
|
656
|
+
content: 'Hello, World!',
|
|
657
|
+
});
|
|
658
|
+
expect(writeResult.success).toBe(true);
|
|
659
|
+
// Read content
|
|
660
|
+
const readResult = await readFileWithAudit({ path: testFile });
|
|
661
|
+
expect(readResult.success).toBe(true);
|
|
662
|
+
expect(readResult.content).toBe('Hello, World!');
|
|
663
|
+
// Edit content
|
|
664
|
+
const editResult = await editFileWithAudit({
|
|
665
|
+
path: testFile,
|
|
666
|
+
oldString: 'World',
|
|
667
|
+
newString: 'Universe',
|
|
668
|
+
});
|
|
669
|
+
expect(editResult.success).toBe(true);
|
|
670
|
+
// Verify edit
|
|
671
|
+
const verifyResult = await readFileWithAudit({ path: testFile });
|
|
672
|
+
expect(verifyResult.success).toBe(true);
|
|
673
|
+
expect(verifyResult.content).toBe('Hello, Universe!');
|
|
674
|
+
// Delete file
|
|
675
|
+
const deleteResult = await deleteFileWithAudit({ path: testFile });
|
|
676
|
+
expect(deleteResult.success).toBe(true);
|
|
677
|
+
expect(existsSync(testFile)).toBe(false);
|
|
678
|
+
});
|
|
679
|
+
it('should handle unicode content correctly', async () => {
|
|
680
|
+
const testFile = join(tempDir, 'unicode.txt');
|
|
681
|
+
const unicodeContent = 'Hello, \u4e16\u754c! \u{1F600}'; // Hello, World in Chinese + emoji
|
|
682
|
+
const writeResult = await writeFileWithAudit({
|
|
683
|
+
path: testFile,
|
|
684
|
+
content: unicodeContent,
|
|
685
|
+
});
|
|
686
|
+
expect(writeResult.success).toBe(true);
|
|
687
|
+
const readResult = await readFileWithAudit({ path: testFile });
|
|
688
|
+
expect(readResult.success).toBe(true);
|
|
689
|
+
expect(readResult.content).toBe(unicodeContent);
|
|
690
|
+
});
|
|
691
|
+
it('should handle binary-like content', async () => {
|
|
692
|
+
const testFile = join(tempDir, 'binary.dat');
|
|
693
|
+
// Create content with null bytes and special characters
|
|
694
|
+
const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe]).toString('latin1');
|
|
695
|
+
const writeResult = await writeFileWithAudit({
|
|
696
|
+
path: testFile,
|
|
697
|
+
content: binaryContent,
|
|
698
|
+
encoding: 'latin1',
|
|
699
|
+
});
|
|
700
|
+
expect(writeResult.success).toBe(true);
|
|
701
|
+
const readResult = await readFileWithAudit({
|
|
702
|
+
path: testFile,
|
|
703
|
+
encoding: 'latin1',
|
|
704
|
+
});
|
|
705
|
+
expect(readResult.success).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
// ============================================================================
|
|
709
|
+
// ADDITIONAL EDGE CASE TESTS FOR COVERAGE
|
|
710
|
+
// ============================================================================
|
|
711
|
+
describe('file operations edge cases', () => {
|
|
712
|
+
let tempDir;
|
|
713
|
+
beforeEach(async () => {
|
|
714
|
+
tempDir = join(tmpdir(), `file-ops-edge-${Date.now()}`);
|
|
715
|
+
await mkdir(tempDir, { recursive: true });
|
|
716
|
+
});
|
|
717
|
+
afterEach(async () => {
|
|
718
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
719
|
+
});
|
|
720
|
+
describe('readFileWithAudit edge cases', () => {
|
|
721
|
+
it('should return error when path is empty', async () => {
|
|
722
|
+
const result = await readFileWithAudit({ path: '' });
|
|
723
|
+
expect(result.success).toBe(false);
|
|
724
|
+
expect(result.error).toContain('required');
|
|
725
|
+
});
|
|
726
|
+
it('should return error when path is undefined', async () => {
|
|
727
|
+
const result = await readFileWithAudit({});
|
|
728
|
+
expect(result.success).toBe(false);
|
|
729
|
+
expect(result.error).toContain('required');
|
|
730
|
+
});
|
|
731
|
+
it('should handle only startLine specified', async () => {
|
|
732
|
+
const testFile = join(tempDir, 'startonly.txt');
|
|
733
|
+
await writeFile(testFile, 'line1\nline2\nline3\nline4', 'utf-8');
|
|
734
|
+
const result = await readFileWithAudit({ path: testFile, startLine: 3 });
|
|
735
|
+
expect(result.success).toBe(true);
|
|
736
|
+
expect(result.content).toBe('line3\nline4');
|
|
737
|
+
});
|
|
738
|
+
it('should handle only endLine specified', async () => {
|
|
739
|
+
const testFile = join(tempDir, 'endonly.txt');
|
|
740
|
+
await writeFile(testFile, 'line1\nline2\nline3\nline4', 'utf-8');
|
|
741
|
+
const result = await readFileWithAudit({ path: testFile, endLine: 2 });
|
|
742
|
+
expect(result.success).toBe(true);
|
|
743
|
+
expect(result.content).toBe('line1\nline2');
|
|
744
|
+
});
|
|
745
|
+
it('should use default max file size when not specified', async () => {
|
|
746
|
+
const testFile = join(tempDir, 'default.txt');
|
|
747
|
+
await writeFile(testFile, 'small content', 'utf-8');
|
|
748
|
+
const result = await readFileWithAudit({ path: testFile });
|
|
749
|
+
expect(result.success).toBe(true);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
describe('writeFileWithAudit edge cases', () => {
|
|
753
|
+
it('should return error when path is empty', async () => {
|
|
754
|
+
const result = await writeFileWithAudit({ path: '', content: 'test' });
|
|
755
|
+
expect(result.success).toBe(false);
|
|
756
|
+
expect(result.error).toContain('required');
|
|
757
|
+
});
|
|
758
|
+
it('should write empty content successfully', async () => {
|
|
759
|
+
const testFile = join(tempDir, 'empty.txt');
|
|
760
|
+
const result = await writeFileWithAudit({ path: testFile, content: '' });
|
|
761
|
+
expect(result.success).toBe(true);
|
|
762
|
+
expect(result.metadata?.bytesWritten).toBe(0);
|
|
763
|
+
});
|
|
764
|
+
it('should write when content is undefined (defaults to empty)', async () => {
|
|
765
|
+
const testFile = join(tempDir, 'undefined-content.txt');
|
|
766
|
+
const result = await writeFileWithAudit({ path: testFile });
|
|
767
|
+
expect(result.success).toBe(true);
|
|
768
|
+
expect(result.metadata?.bytesWritten).toBe(0);
|
|
769
|
+
});
|
|
770
|
+
it('should use default encoding when not specified', async () => {
|
|
771
|
+
const testFile = join(tempDir, 'default-enc.txt');
|
|
772
|
+
const result = await writeFileWithAudit({
|
|
773
|
+
path: testFile,
|
|
774
|
+
content: 'test content',
|
|
775
|
+
});
|
|
776
|
+
expect(result.success).toBe(true);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
describe('editFileWithAudit edge cases', () => {
|
|
780
|
+
it('should return error when path is empty', async () => {
|
|
781
|
+
const result = await editFileWithAudit({
|
|
782
|
+
path: '',
|
|
783
|
+
oldString: 'old',
|
|
784
|
+
newString: 'new',
|
|
785
|
+
});
|
|
786
|
+
expect(result.success).toBe(false);
|
|
787
|
+
expect(result.error).toContain('required');
|
|
788
|
+
});
|
|
789
|
+
it('should return error when oldString is empty', async () => {
|
|
790
|
+
const testFile = join(tempDir, 'noolstr.txt');
|
|
791
|
+
await writeFile(testFile, 'content', 'utf-8');
|
|
792
|
+
const result = await editFileWithAudit({
|
|
793
|
+
path: testFile,
|
|
794
|
+
oldString: '',
|
|
795
|
+
newString: 'new',
|
|
796
|
+
});
|
|
797
|
+
expect(result.success).toBe(false);
|
|
798
|
+
expect(result.error).toContain('required');
|
|
799
|
+
});
|
|
800
|
+
it('should allow empty newString (deletion)', async () => {
|
|
801
|
+
const testFile = join(tempDir, 'deletion.txt');
|
|
802
|
+
await writeFile(testFile, 'Hello, World!', 'utf-8');
|
|
803
|
+
const result = await editFileWithAudit({
|
|
804
|
+
path: testFile,
|
|
805
|
+
oldString: 'World',
|
|
806
|
+
newString: '',
|
|
807
|
+
});
|
|
808
|
+
expect(result.success).toBe(true);
|
|
809
|
+
});
|
|
810
|
+
it('should handle very long strings in diff', async () => {
|
|
811
|
+
const testFile = join(tempDir, 'longdiff.txt');
|
|
812
|
+
const longContent = 'x'.repeat(200) + 'FIND_ME' + 'y'.repeat(200);
|
|
813
|
+
await writeFile(testFile, longContent, 'utf-8');
|
|
814
|
+
const result = await editFileWithAudit({
|
|
815
|
+
path: testFile,
|
|
816
|
+
oldString: 'FIND_ME',
|
|
817
|
+
newString: 'REPLACED',
|
|
818
|
+
});
|
|
819
|
+
expect(result.success).toBe(true);
|
|
820
|
+
expect(result.diff).toBeDefined();
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
describe('deleteFileWithAudit edge cases', () => {
|
|
824
|
+
it('should return error when path is empty', async () => {
|
|
825
|
+
const result = await deleteFileWithAudit({ path: '' });
|
|
826
|
+
expect(result.success).toBe(false);
|
|
827
|
+
expect(result.error).toContain('required');
|
|
828
|
+
});
|
|
829
|
+
it('should delete empty directory with recursive flag', async () => {
|
|
830
|
+
const emptyDir = join(tempDir, 'emptydir');
|
|
831
|
+
await mkdir(emptyDir, { recursive: true });
|
|
832
|
+
const result = await deleteFileWithAudit({
|
|
833
|
+
path: emptyDir,
|
|
834
|
+
recursive: true,
|
|
835
|
+
});
|
|
836
|
+
expect(result.success).toBe(true);
|
|
837
|
+
expect(result.metadata?.wasDirectory).toBe(true);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
describe('parseArgs edge cases', () => {
|
|
841
|
+
it('parseFileReadArgs should handle --max-size option', () => {
|
|
842
|
+
const args = parseFileReadArgs([
|
|
843
|
+
'node',
|
|
844
|
+
'file-read',
|
|
845
|
+
'--path',
|
|
846
|
+
'f.txt',
|
|
847
|
+
'--max-size',
|
|
848
|
+
'1024',
|
|
849
|
+
]);
|
|
850
|
+
expect(args.maxFileSizeBytes).toBe(1024);
|
|
851
|
+
});
|
|
852
|
+
it('parseFileWriteArgs should parse --scan-phi flag', () => {
|
|
853
|
+
const args = parseFileWriteArgs([
|
|
854
|
+
'node',
|
|
855
|
+
'file-write',
|
|
856
|
+
'--path',
|
|
857
|
+
'f.txt',
|
|
858
|
+
'--content',
|
|
859
|
+
'x',
|
|
860
|
+
'--scan-phi',
|
|
861
|
+
]);
|
|
862
|
+
expect(args.scanPHI).toBe(true);
|
|
863
|
+
});
|
|
864
|
+
it('parseFileEditArgs should handle short options', () => {
|
|
865
|
+
const args = parseFileEditArgs(['node', 'file-edit', 'f.txt', '--old', 'a', '--new', 'b']);
|
|
866
|
+
expect(args.oldString).toBe('a');
|
|
867
|
+
expect(args.newString).toBe('b');
|
|
868
|
+
});
|
|
869
|
+
it('parseFileDeleteArgs should handle combined short flags', () => {
|
|
870
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '-r', '-f', 'dir/']);
|
|
871
|
+
expect(args.recursive).toBe(true);
|
|
872
|
+
expect(args.force).toBe(true);
|
|
873
|
+
});
|
|
874
|
+
it('parseFileEditArgs should handle --encoding option', () => {
|
|
875
|
+
const args = parseFileEditArgs([
|
|
876
|
+
'node',
|
|
877
|
+
'file-edit',
|
|
878
|
+
'--path',
|
|
879
|
+
'f.txt',
|
|
880
|
+
'--old',
|
|
881
|
+
'a',
|
|
882
|
+
'--new',
|
|
883
|
+
'b',
|
|
884
|
+
'--encoding',
|
|
885
|
+
'latin1',
|
|
886
|
+
]);
|
|
887
|
+
expect(args.encoding).toBe('latin1');
|
|
888
|
+
});
|
|
889
|
+
it('parseFileReadArgs should handle -h short help', () => {
|
|
890
|
+
const args = parseFileReadArgs(['node', 'file-read', '-h']);
|
|
891
|
+
expect(args.help).toBe(true);
|
|
892
|
+
});
|
|
893
|
+
it('parseFileWriteArgs should handle -h short help', () => {
|
|
894
|
+
const args = parseFileWriteArgs(['node', 'file-write', '-h']);
|
|
895
|
+
expect(args.help).toBe(true);
|
|
896
|
+
});
|
|
897
|
+
it('parseFileEditArgs should handle -h short help', () => {
|
|
898
|
+
const args = parseFileEditArgs(['node', 'file-edit', '-h']);
|
|
899
|
+
expect(args.help).toBe(true);
|
|
900
|
+
});
|
|
901
|
+
it('parseFileDeleteArgs should handle -h short help', () => {
|
|
902
|
+
const args = parseFileDeleteArgs(['node', 'file-delete', '-h']);
|
|
903
|
+
expect(args.help).toBe(true);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
});
|