@amirdaraee/namewise 0.3.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/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
- package/.github/workflows/auto-release.yml +78 -0
- package/.github/workflows/ci.yml +78 -0
- package/.github/workflows/publish.yml +43 -0
- package/.github/workflows/test.yml +37 -0
- package/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +19 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/rename.d.ts +2 -0
- package/dist/cli/rename.d.ts.map +1 -0
- package/dist/cli/rename.js +136 -0
- package/dist/cli/rename.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/excel-parser.d.ts +6 -0
- package/dist/parsers/excel-parser.d.ts.map +1 -0
- package/dist/parsers/excel-parser.js +42 -0
- package/dist/parsers/excel-parser.js.map +1 -0
- package/dist/parsers/factory.d.ts +7 -0
- package/dist/parsers/factory.d.ts.map +1 -0
- package/dist/parsers/factory.js +29 -0
- package/dist/parsers/factory.js.map +1 -0
- package/dist/parsers/pdf-parser.d.ts +7 -0
- package/dist/parsers/pdf-parser.d.ts.map +1 -0
- package/dist/parsers/pdf-parser.js +67 -0
- package/dist/parsers/pdf-parser.js.map +1 -0
- package/dist/parsers/text-parser.d.ts +6 -0
- package/dist/parsers/text-parser.d.ts.map +1 -0
- package/dist/parsers/text-parser.js +39 -0
- package/dist/parsers/text-parser.js.map +1 -0
- package/dist/parsers/word-parser.d.ts +6 -0
- package/dist/parsers/word-parser.d.ts.map +1 -0
- package/dist/parsers/word-parser.js +44 -0
- package/dist/parsers/word-parser.js.map +1 -0
- package/dist/services/ai-factory.d.ts +5 -0
- package/dist/services/ai-factory.d.ts.map +1 -0
- package/dist/services/ai-factory.js +15 -0
- package/dist/services/ai-factory.js.map +1 -0
- package/dist/services/claude-service.d.ts +9 -0
- package/dist/services/claude-service.d.ts.map +1 -0
- package/dist/services/claude-service.js +113 -0
- package/dist/services/claude-service.js.map +1 -0
- package/dist/services/file-renamer.d.ts +12 -0
- package/dist/services/file-renamer.d.ts.map +1 -0
- package/dist/services/file-renamer.js +99 -0
- package/dist/services/file-renamer.js.map +1 -0
- package/dist/services/openai-service.d.ts +9 -0
- package/dist/services/openai-service.d.ts.map +1 -0
- package/dist/services/openai-service.js +112 -0
- package/dist/services/openai-service.js.map +1 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-templates.d.ts +18 -0
- package/dist/utils/file-templates.d.ts.map +1 -0
- package/dist/utils/file-templates.js +232 -0
- package/dist/utils/file-templates.js.map +1 -0
- package/dist/utils/naming-conventions.d.ts +4 -0
- package/dist/utils/naming-conventions.d.ts.map +1 -0
- package/dist/utils/naming-conventions.js +55 -0
- package/dist/utils/naming-conventions.js.map +1 -0
- package/package.json +75 -0
- package/src/cli/commands.ts +20 -0
- package/src/cli/rename.ts +157 -0
- package/src/index.ts +17 -0
- package/src/parsers/excel-parser.ts +49 -0
- package/src/parsers/factory.ts +34 -0
- package/src/parsers/pdf-parser.ts +78 -0
- package/src/parsers/text-parser.ts +43 -0
- package/src/parsers/word-parser.ts +50 -0
- package/src/services/ai-factory.ts +16 -0
- package/src/services/claude-service.ts +114 -0
- package/src/services/file-renamer.ts +123 -0
- package/src/services/openai-service.ts +113 -0
- package/src/types/index.ts +71 -0
- package/src/types/pdf-extraction.d.ts +7 -0
- package/src/utils/file-templates.ts +275 -0
- package/src/utils/naming-conventions.ts +67 -0
- package/tests/data/empty-file.txt +0 -0
- package/tests/data/sample-markdown.md +9 -0
- package/tests/data/sample-pdf.pdf +0 -0
- package/tests/data/sample-text.txt +25 -0
- package/tests/integration/end-to-end.test.ts +209 -0
- package/tests/integration/workflow.test.ts +336 -0
- package/tests/mocks/mock-ai-service.ts +58 -0
- package/tests/unit/cli/commands.test.ts +163 -0
- package/tests/unit/parsers/factory.test.ts +100 -0
- package/tests/unit/parsers/pdf-parser.test.ts +63 -0
- package/tests/unit/parsers/text-parser.test.ts +85 -0
- package/tests/unit/services/ai-factory.test.ts +37 -0
- package/tests/unit/services/claude-service.test.ts +188 -0
- package/tests/unit/services/file-renamer.test.ts +299 -0
- package/tests/unit/services/openai-service.test.ts +196 -0
- package/tests/unit/utils/file-templates.test.ts +199 -0
- package/tests/unit/utils/naming-conventions.test.ts +88 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +30 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { DocumentParserFactory } from '../../src/parsers/factory.js';
|
|
5
|
+
import { FileRenamer } from '../../src/services/file-renamer.js';
|
|
6
|
+
import { MockAIService } from '../mocks/mock-ai-service.js';
|
|
7
|
+
import { Config, FileInfo } from '../../src/types/index.js';
|
|
8
|
+
|
|
9
|
+
// Mock file system operations
|
|
10
|
+
vi.mock('fs', async () => {
|
|
11
|
+
const actual = await vi.importActual('fs');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
promises: {
|
|
15
|
+
...actual.promises,
|
|
16
|
+
rename: vi.fn(),
|
|
17
|
+
access: vi.fn(),
|
|
18
|
+
stat: vi.fn(),
|
|
19
|
+
readdir: vi.fn()
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('Workflow Integration Tests', () => {
|
|
25
|
+
let mockAIService: MockAIService;
|
|
26
|
+
let parserFactory: DocumentParserFactory;
|
|
27
|
+
let fileRenamer: FileRenamer;
|
|
28
|
+
let config: Config;
|
|
29
|
+
const testDataDir = path.join(process.cwd(), 'tests/data');
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
mockAIService = new MockAIService();
|
|
33
|
+
parserFactory = new DocumentParserFactory();
|
|
34
|
+
config = {
|
|
35
|
+
aiProvider: 'claude',
|
|
36
|
+
apiKey: 'test-key',
|
|
37
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
38
|
+
supportedExtensions: ['.txt', '.pdf', '.docx', '.xlsx', '.md'],
|
|
39
|
+
dryRun: false,
|
|
40
|
+
namingConvention: 'kebab-case',
|
|
41
|
+
templateOptions: {
|
|
42
|
+
category: 'general',
|
|
43
|
+
personalName: undefined,
|
|
44
|
+
dateFormat: 'none'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fileRenamer = new FileRenamer(parserFactory, mockAIService, config);
|
|
49
|
+
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
mockAIService.resetCallCount();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Complete File Processing Workflow', () => {
|
|
59
|
+
it('should process mixed file types successfully', async () => {
|
|
60
|
+
const testFiles: FileInfo[] = [
|
|
61
|
+
{
|
|
62
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
63
|
+
name: 'sample-text.txt',
|
|
64
|
+
extension: '.txt',
|
|
65
|
+
size: 1000
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
path: path.join(testDataDir, 'sample-markdown.md'),
|
|
69
|
+
name: 'sample-markdown.md',
|
|
70
|
+
extension: '.md',
|
|
71
|
+
size: 500
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Mock successful file operations
|
|
76
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
|
|
77
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
78
|
+
|
|
79
|
+
// Set up different AI responses for different content types
|
|
80
|
+
mockAIService.setMockResponse('default', 'project-requirements-document');
|
|
81
|
+
mockAIService.setMockResponse('meeting', 'team-meeting-notes-march-2024');
|
|
82
|
+
|
|
83
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
84
|
+
|
|
85
|
+
expect(results).toHaveLength(2);
|
|
86
|
+
expect(results.every(r => r.success)).toBe(true);
|
|
87
|
+
expect(mockAIService.getCallCount()).toBe(2);
|
|
88
|
+
expect(fs.rename).toHaveBeenCalledTimes(2);
|
|
89
|
+
|
|
90
|
+
// Verify different filenames were generated
|
|
91
|
+
expect(results[0].suggestedName).toContain('project-requirements-document');
|
|
92
|
+
expect(results[1].suggestedName).toContain('team-meeting-notes');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle mixed success and failure scenarios', async () => {
|
|
96
|
+
const testFiles: FileInfo[] = [
|
|
97
|
+
{
|
|
98
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
99
|
+
name: 'sample-text.txt',
|
|
100
|
+
extension: '.txt',
|
|
101
|
+
size: 1000
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
path: path.join(testDataDir, 'large-file.txt'),
|
|
105
|
+
name: 'large-file.txt',
|
|
106
|
+
extension: '.txt',
|
|
107
|
+
size: 20 * 1024 * 1024 // Exceeds limit
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: path.join(testDataDir, 'empty-file.txt'),
|
|
111
|
+
name: 'empty-file.txt',
|
|
112
|
+
extension: '.txt',
|
|
113
|
+
size: 0
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
|
|
118
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
119
|
+
|
|
120
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
121
|
+
|
|
122
|
+
expect(results).toHaveLength(3);
|
|
123
|
+
expect(results[0].success).toBe(true); // Normal file
|
|
124
|
+
expect(results[1].success).toBe(false); // Too large
|
|
125
|
+
expect(results[2].success).toBe(false); // Empty file
|
|
126
|
+
|
|
127
|
+
expect(results[1].error).toContain('File size');
|
|
128
|
+
expect(results[2].error).toContain('No content could be extracted');
|
|
129
|
+
|
|
130
|
+
// Only the successful file should trigger AI call and rename
|
|
131
|
+
expect(mockAIService.getCallCount()).toBe(1);
|
|
132
|
+
expect(fs.rename).toHaveBeenCalledOnce();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should respect dry-run mode across all files', async () => {
|
|
136
|
+
config.dryRun = true;
|
|
137
|
+
fileRenamer = new FileRenamer(parserFactory, mockAIService, config);
|
|
138
|
+
|
|
139
|
+
const testFiles: FileInfo[] = [
|
|
140
|
+
{
|
|
141
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
142
|
+
name: 'sample-text.txt',
|
|
143
|
+
extension: '.txt',
|
|
144
|
+
size: 1000
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
path: path.join(testDataDir, 'sample-markdown.md'),
|
|
148
|
+
name: 'sample-markdown.md',
|
|
149
|
+
extension: '.md',
|
|
150
|
+
size: 500
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
|
|
155
|
+
|
|
156
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
157
|
+
|
|
158
|
+
expect(results).toHaveLength(2);
|
|
159
|
+
expect(results.every(r => r.success)).toBe(true);
|
|
160
|
+
expect(mockAIService.getCallCount()).toBe(2); // AI should still be called
|
|
161
|
+
expect(fs.rename).not.toHaveBeenCalled(); // But no actual renaming
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should handle file conflicts appropriately', async () => {
|
|
165
|
+
const testFiles: FileInfo[] = [
|
|
166
|
+
{
|
|
167
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
168
|
+
name: 'sample-text.txt',
|
|
169
|
+
extension: '.txt',
|
|
170
|
+
size: 1000
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// Simulate that the target filename already exists
|
|
175
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
176
|
+
|
|
177
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
178
|
+
|
|
179
|
+
expect(results).toHaveLength(1);
|
|
180
|
+
expect(results[0].success).toBe(false);
|
|
181
|
+
expect(results[0].error).toContain('Target filename already exists');
|
|
182
|
+
|
|
183
|
+
// AI should still be called, but no renaming should occur
|
|
184
|
+
expect(mockAIService.getCallCount()).toBe(1);
|
|
185
|
+
expect(fs.rename).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Parser Integration', () => {
|
|
190
|
+
it('should use correct parser for each file type', async () => {
|
|
191
|
+
const testFiles: FileInfo[] = [
|
|
192
|
+
{
|
|
193
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
194
|
+
name: 'sample-text.txt',
|
|
195
|
+
extension: '.txt',
|
|
196
|
+
size: 1000
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
path: path.join(testDataDir, 'sample-pdf.pdf'),
|
|
200
|
+
name: 'sample-pdf.pdf',
|
|
201
|
+
extension: '.pdf',
|
|
202
|
+
size: 2000
|
|
203
|
+
}
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
|
|
207
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
208
|
+
|
|
209
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
210
|
+
|
|
211
|
+
expect(results).toHaveLength(2);
|
|
212
|
+
expect(results.every(r => r.success)).toBe(true);
|
|
213
|
+
|
|
214
|
+
// Both files should be processed successfully using their respective parsers
|
|
215
|
+
expect(mockAIService.getCallCount()).toBe(2);
|
|
216
|
+
expect(fs.rename).toHaveBeenCalledTimes(2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should reject unsupported file types', async () => {
|
|
220
|
+
const testFiles: FileInfo[] = [
|
|
221
|
+
{
|
|
222
|
+
path: path.join(testDataDir, 'unsupported.xyz'),
|
|
223
|
+
name: 'unsupported.xyz',
|
|
224
|
+
extension: '.xyz',
|
|
225
|
+
size: 1000
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
230
|
+
|
|
231
|
+
expect(results).toHaveLength(1);
|
|
232
|
+
expect(results[0].success).toBe(false);
|
|
233
|
+
expect(results[0].error).toContain('No parser available');
|
|
234
|
+
|
|
235
|
+
expect(mockAIService.getCallCount()).toBe(0);
|
|
236
|
+
expect(fs.rename).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('AI Service Integration', () => {
|
|
241
|
+
it('should handle AI service failures gracefully', async () => {
|
|
242
|
+
mockAIService.setShouldFail(true);
|
|
243
|
+
|
|
244
|
+
const testFiles: FileInfo[] = [
|
|
245
|
+
{
|
|
246
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
247
|
+
name: 'sample-text.txt',
|
|
248
|
+
extension: '.txt',
|
|
249
|
+
size: 1000
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
254
|
+
|
|
255
|
+
expect(results).toHaveLength(1);
|
|
256
|
+
expect(results[0].success).toBe(false);
|
|
257
|
+
expect(results[0].error).toContain('Mock AI service failed');
|
|
258
|
+
|
|
259
|
+
expect(mockAIService.getCallCount()).toBe(1);
|
|
260
|
+
expect(fs.rename).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should generate contextually appropriate filenames', async () => {
|
|
264
|
+
const testFiles: FileInfo[] = [
|
|
265
|
+
{
|
|
266
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
267
|
+
name: 'sample-text.txt',
|
|
268
|
+
extension: '.txt',
|
|
269
|
+
size: 1000
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
path: path.join(testDataDir, 'sample-markdown.md'),
|
|
273
|
+
name: 'sample-markdown.md',
|
|
274
|
+
extension: '.md',
|
|
275
|
+
size: 500
|
|
276
|
+
}
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
|
|
280
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
281
|
+
|
|
282
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
283
|
+
|
|
284
|
+
expect(results).toHaveLength(2);
|
|
285
|
+
expect(results.every(r => r.success)).toBe(true);
|
|
286
|
+
|
|
287
|
+
// Verify that different content generates different filenames
|
|
288
|
+
expect(results[0].suggestedName).not.toBe(results[1].suggestedName);
|
|
289
|
+
|
|
290
|
+
// Filenames should reflect content
|
|
291
|
+
expect(results[0].suggestedName).toContain('project-requirements-document');
|
|
292
|
+
expect(results[1].suggestedName).toContain('team-meeting-notes');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('Error Recovery and Resilience', () => {
|
|
297
|
+
it('should continue processing after individual file failures', async () => {
|
|
298
|
+
const testFiles: FileInfo[] = [
|
|
299
|
+
{
|
|
300
|
+
path: path.join(testDataDir, 'sample-text.txt'),
|
|
301
|
+
name: 'sample-text.txt',
|
|
302
|
+
extension: '.txt',
|
|
303
|
+
size: 1000
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
path: path.join(testDataDir, 'non-existent.txt'),
|
|
307
|
+
name: 'non-existent.txt',
|
|
308
|
+
extension: '.txt',
|
|
309
|
+
size: 1000
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
path: path.join(testDataDir, 'sample-markdown.md'),
|
|
313
|
+
name: 'sample-markdown.md',
|
|
314
|
+
extension: '.md',
|
|
315
|
+
size: 500
|
|
316
|
+
}
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
|
|
320
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
321
|
+
|
|
322
|
+
const results = await fileRenamer.renameFiles(testFiles);
|
|
323
|
+
|
|
324
|
+
expect(results).toHaveLength(3);
|
|
325
|
+
expect(results[0].success).toBe(true); // First file succeeds
|
|
326
|
+
expect(results[1].success).toBe(false); // Second file fails
|
|
327
|
+
expect(results[2].success).toBe(true); // Third file still processes
|
|
328
|
+
|
|
329
|
+
expect(results[1].error).toContain('Failed to parse text file');
|
|
330
|
+
|
|
331
|
+
// Two successful files should generate AI calls and renames
|
|
332
|
+
expect(mockAIService.getCallCount()).toBe(2);
|
|
333
|
+
expect(fs.rename).toHaveBeenCalledTimes(2);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AIProvider } from '../../src/types/index.js';
|
|
2
|
+
|
|
3
|
+
export class MockAIService implements AIProvider {
|
|
4
|
+
name = 'MockAI';
|
|
5
|
+
private mockResponses: Map<string, string> = new Map();
|
|
6
|
+
private shouldFail = false;
|
|
7
|
+
private callCount = 0;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
// Default mock responses
|
|
11
|
+
this.mockResponses.set('default', 'project-requirements-document');
|
|
12
|
+
this.mockResponses.set('meeting', 'team-meeting-notes-march-2024');
|
|
13
|
+
this.mockResponses.set('report', 'quarterly-sales-report-q1-2024');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setMockResponse(key: string, response: string): void {
|
|
17
|
+
this.mockResponses.set(key, response);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setShouldFail(shouldFail: boolean): void {
|
|
21
|
+
this.shouldFail = shouldFail;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getCallCount(): number {
|
|
25
|
+
return this.callCount;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resetCallCount(): void {
|
|
29
|
+
this.callCount = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async generateFileName(content: string, originalName: string, namingConvention?: string, category?: string): Promise<string> {
|
|
33
|
+
this.callCount++;
|
|
34
|
+
|
|
35
|
+
if (this.shouldFail) {
|
|
36
|
+
throw new Error('Mock AI service failed');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Simple logic to return different responses based on content
|
|
40
|
+
const contentLower = content.toLowerCase();
|
|
41
|
+
|
|
42
|
+
if (contentLower.includes('meeting') || contentLower.includes('attendees')) {
|
|
43
|
+
return this.mockResponses.get('meeting') || 'meeting-notes';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (contentLower.includes('requirements') || contentLower.includes('project')) {
|
|
47
|
+
return this.mockResponses.get('default') || 'project-document';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (contentLower.includes('report') || contentLower.includes('sales')) {
|
|
51
|
+
return this.mockResponses.get('report') || 'business-report';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback to a generic name based on original filename
|
|
55
|
+
const baseName = originalName.replace(/\.[^/.]+$/, '');
|
|
56
|
+
return `renamed-${baseName}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { setupCommands } from '../../../src/cli/commands.js';
|
|
4
|
+
|
|
5
|
+
// Mock the rename function to avoid actual execution
|
|
6
|
+
vi.mock('../../../src/cli/rename.js', () => ({
|
|
7
|
+
renameFiles: vi.fn()
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('CLI Commands', () => {
|
|
11
|
+
let program: Command;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
program = new Command();
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('setupCommands()', () => {
|
|
19
|
+
it('should set up rename command', () => {
|
|
20
|
+
setupCommands(program);
|
|
21
|
+
|
|
22
|
+
const renameCommand = program.commands.find(cmd => cmd.name() === 'rename');
|
|
23
|
+
expect(renameCommand).toBeDefined();
|
|
24
|
+
expect(renameCommand?.description()).toBe('Rename files in a directory based on their content');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should configure rename command with correct arguments', () => {
|
|
28
|
+
setupCommands(program);
|
|
29
|
+
|
|
30
|
+
const renameCommand = program.commands.find(cmd => cmd.name() === 'rename');
|
|
31
|
+
expect(renameCommand).toBeDefined();
|
|
32
|
+
expect(renameCommand?.description()).toContain('Rename files in a directory');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should configure rename command with correct options', () => {
|
|
36
|
+
setupCommands(program);
|
|
37
|
+
|
|
38
|
+
const renameCommand = program.commands.find(cmd => cmd.name() === 'rename');
|
|
39
|
+
expect(renameCommand).toBeDefined();
|
|
40
|
+
|
|
41
|
+
const options = renameCommand?.options;
|
|
42
|
+
expect(options).toBeDefined();
|
|
43
|
+
|
|
44
|
+
// Check provider option
|
|
45
|
+
const providerOption = options?.find(opt => opt.long === '--provider');
|
|
46
|
+
expect(providerOption).toBeDefined();
|
|
47
|
+
expect(providerOption?.description).toBe('AI provider (claude|openai)');
|
|
48
|
+
expect(providerOption?.defaultValue).toBe('claude');
|
|
49
|
+
|
|
50
|
+
// Check api-key option
|
|
51
|
+
const apiKeyOption = options?.find(opt => opt.long === '--api-key');
|
|
52
|
+
expect(apiKeyOption).toBeDefined();
|
|
53
|
+
expect(apiKeyOption?.description).toBe('API key for the AI provider');
|
|
54
|
+
|
|
55
|
+
// Check dry-run option
|
|
56
|
+
const dryRunOption = options?.find(opt => opt.long === '--dry-run');
|
|
57
|
+
expect(dryRunOption).toBeDefined();
|
|
58
|
+
expect(dryRunOption?.description).toBe('Preview changes without renaming files');
|
|
59
|
+
expect(dryRunOption?.defaultValue).toBe(false);
|
|
60
|
+
|
|
61
|
+
// Check max-size option
|
|
62
|
+
const maxSizeOption = options?.find(opt => opt.long === '--max-size');
|
|
63
|
+
expect(maxSizeOption).toBeDefined();
|
|
64
|
+
expect(maxSizeOption?.description).toBe('Maximum file size in MB');
|
|
65
|
+
expect(maxSizeOption?.defaultValue).toBe('10');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should have short option aliases', () => {
|
|
69
|
+
setupCommands(program);
|
|
70
|
+
|
|
71
|
+
const renameCommand = program.commands.find(cmd => cmd.name() === 'rename');
|
|
72
|
+
const options = renameCommand?.options;
|
|
73
|
+
|
|
74
|
+
// Check provider has -p alias
|
|
75
|
+
const providerOption = options?.find(opt => opt.long === '--provider');
|
|
76
|
+
expect(providerOption?.short).toBe('-p');
|
|
77
|
+
|
|
78
|
+
// Check api-key has -k alias
|
|
79
|
+
const apiKeyOption = options?.find(opt => opt.long === '--api-key');
|
|
80
|
+
expect(apiKeyOption?.short).toBe('-k');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Command parsing', () => {
|
|
85
|
+
it('should parse directory argument correctly', async () => {
|
|
86
|
+
const { renameFiles } = await import('../../../src/cli/rename.js');
|
|
87
|
+
|
|
88
|
+
setupCommands(program);
|
|
89
|
+
|
|
90
|
+
await program.parseAsync(['node', 'test', 'rename', '/test/directory'], { from: 'node' });
|
|
91
|
+
|
|
92
|
+
expect(renameFiles).toHaveBeenCalledWith('/test/directory', expect.any(Object));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should parse options correctly', async () => {
|
|
96
|
+
const { renameFiles } = await import('../../../src/cli/rename.js');
|
|
97
|
+
|
|
98
|
+
setupCommands(program);
|
|
99
|
+
|
|
100
|
+
await program.parseAsync([
|
|
101
|
+
'node', 'test', 'rename', '/test/directory',
|
|
102
|
+
'--provider', 'openai',
|
|
103
|
+
'--api-key', 'test-key',
|
|
104
|
+
'--dry-run',
|
|
105
|
+
'--max-size', '20'
|
|
106
|
+
], { from: 'node' });
|
|
107
|
+
|
|
108
|
+
expect(renameFiles).toHaveBeenCalledWith('/test/directory', {
|
|
109
|
+
provider: 'openai',
|
|
110
|
+
apiKey: 'test-key',
|
|
111
|
+
dryRun: true,
|
|
112
|
+
maxSize: '20',
|
|
113
|
+
case: 'kebab-case',
|
|
114
|
+
template: 'general',
|
|
115
|
+
name: undefined,
|
|
116
|
+
date: 'none'
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should use default values for options', async () => {
|
|
121
|
+
const { renameFiles } = await import('../../../src/cli/rename.js');
|
|
122
|
+
|
|
123
|
+
setupCommands(program);
|
|
124
|
+
|
|
125
|
+
await program.parseAsync(['node', 'test', 'rename', '/test/directory'], { from: 'node' });
|
|
126
|
+
|
|
127
|
+
const callArgs = vi.mocked(renameFiles).mock.calls[0];
|
|
128
|
+
expect(callArgs[1]).toEqual({
|
|
129
|
+
provider: 'claude',
|
|
130
|
+
apiKey: undefined,
|
|
131
|
+
dryRun: false,
|
|
132
|
+
maxSize: '10',
|
|
133
|
+
case: 'kebab-case',
|
|
134
|
+
template: 'general',
|
|
135
|
+
name: undefined,
|
|
136
|
+
date: 'none'
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should handle short option aliases', async () => {
|
|
141
|
+
const { renameFiles } = await import('../../../src/cli/rename.js');
|
|
142
|
+
|
|
143
|
+
setupCommands(program);
|
|
144
|
+
|
|
145
|
+
await program.parseAsync([
|
|
146
|
+
'node', 'test', 'rename', '/test/directory',
|
|
147
|
+
'-p', 'openai',
|
|
148
|
+
'-k', 'test-key'
|
|
149
|
+
], { from: 'node' });
|
|
150
|
+
|
|
151
|
+
expect(renameFiles).toHaveBeenCalledWith('/test/directory', {
|
|
152
|
+
provider: 'openai',
|
|
153
|
+
apiKey: 'test-key',
|
|
154
|
+
dryRun: false,
|
|
155
|
+
maxSize: '10',
|
|
156
|
+
case: 'kebab-case',
|
|
157
|
+
template: 'general',
|
|
158
|
+
name: undefined,
|
|
159
|
+
date: 'none'
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { DocumentParserFactory } from '../../../src/parsers/factory.js';
|
|
3
|
+
|
|
4
|
+
describe('DocumentParserFactory', () => {
|
|
5
|
+
let factory: DocumentParserFactory;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
factory = new DocumentParserFactory();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('getParser()', () => {
|
|
12
|
+
it('should return PDFParser for PDF files', () => {
|
|
13
|
+
const parser = factory.getParser('test.pdf');
|
|
14
|
+
expect(parser).toBeDefined();
|
|
15
|
+
expect(parser?.supports('test.pdf')).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return WordParser for Word documents', () => {
|
|
19
|
+
const parser1 = factory.getParser('test.docx');
|
|
20
|
+
const parser2 = factory.getParser('test.doc');
|
|
21
|
+
|
|
22
|
+
expect(parser1).toBeDefined();
|
|
23
|
+
expect(parser2).toBeDefined();
|
|
24
|
+
expect(parser1?.supports('test.docx')).toBe(true);
|
|
25
|
+
expect(parser2?.supports('test.doc')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return ExcelParser for Excel files', () => {
|
|
29
|
+
const parser1 = factory.getParser('test.xlsx');
|
|
30
|
+
const parser2 = factory.getParser('test.xls');
|
|
31
|
+
|
|
32
|
+
expect(parser1).toBeDefined();
|
|
33
|
+
expect(parser2).toBeDefined();
|
|
34
|
+
expect(parser1?.supports('test.xlsx')).toBe(true);
|
|
35
|
+
expect(parser2?.supports('test.xls')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return TextParser for text files', () => {
|
|
39
|
+
const parser1 = factory.getParser('test.txt');
|
|
40
|
+
const parser2 = factory.getParser('test.md');
|
|
41
|
+
const parser3 = factory.getParser('test.rtf');
|
|
42
|
+
|
|
43
|
+
expect(parser1).toBeDefined();
|
|
44
|
+
expect(parser2).toBeDefined();
|
|
45
|
+
expect(parser3).toBeDefined();
|
|
46
|
+
expect(parser1?.supports('test.txt')).toBe(true);
|
|
47
|
+
expect(parser2?.supports('test.md')).toBe(true);
|
|
48
|
+
expect(parser3?.supports('test.rtf')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return null for unsupported file types', () => {
|
|
52
|
+
const parser1 = factory.getParser('test.png');
|
|
53
|
+
const parser2 = factory.getParser('test.jpg');
|
|
54
|
+
const parser3 = factory.getParser('test.mp4');
|
|
55
|
+
const parser4 = factory.getParser('test.zip');
|
|
56
|
+
|
|
57
|
+
expect(parser1).toBeNull();
|
|
58
|
+
expect(parser2).toBeNull();
|
|
59
|
+
expect(parser3).toBeNull();
|
|
60
|
+
expect(parser4).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should be case insensitive', () => {
|
|
64
|
+
const parser1 = factory.getParser('test.PDF');
|
|
65
|
+
const parser2 = factory.getParser('test.DOCX');
|
|
66
|
+
const parser3 = factory.getParser('test.TXT');
|
|
67
|
+
|
|
68
|
+
expect(parser1).toBeDefined();
|
|
69
|
+
expect(parser2).toBeDefined();
|
|
70
|
+
expect(parser3).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('getSupportedExtensions()', () => {
|
|
75
|
+
it('should return all supported extensions', () => {
|
|
76
|
+
const extensions = factory.getSupportedExtensions();
|
|
77
|
+
|
|
78
|
+
expect(extensions).toContain('.pdf');
|
|
79
|
+
expect(extensions).toContain('.docx');
|
|
80
|
+
expect(extensions).toContain('.doc');
|
|
81
|
+
expect(extensions).toContain('.xlsx');
|
|
82
|
+
expect(extensions).toContain('.xls');
|
|
83
|
+
expect(extensions).toContain('.txt');
|
|
84
|
+
expect(extensions).toContain('.md');
|
|
85
|
+
expect(extensions).toContain('.rtf');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return unique extensions', () => {
|
|
89
|
+
const extensions = factory.getSupportedExtensions();
|
|
90
|
+
const uniqueExtensions = [...new Set(extensions)];
|
|
91
|
+
|
|
92
|
+
expect(extensions.length).toBe(uniqueExtensions.length);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should include at least 8 extensions', () => {
|
|
96
|
+
const extensions = factory.getSupportedExtensions();
|
|
97
|
+
expect(extensions.length).toBeGreaterThanOrEqual(8);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|