@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.
Files changed (105) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
  3. package/.github/workflows/auto-release.yml +78 -0
  4. package/.github/workflows/ci.yml +78 -0
  5. package/.github/workflows/publish.yml +43 -0
  6. package/.github/workflows/test.yml +37 -0
  7. package/CHANGELOG.md +128 -0
  8. package/LICENSE +21 -0
  9. package/README.md +251 -0
  10. package/dist/cli/commands.d.ts +3 -0
  11. package/dist/cli/commands.d.ts.map +1 -0
  12. package/dist/cli/commands.js +19 -0
  13. package/dist/cli/commands.js.map +1 -0
  14. package/dist/cli/rename.d.ts +2 -0
  15. package/dist/cli/rename.d.ts.map +1 -0
  16. package/dist/cli/rename.js +136 -0
  17. package/dist/cli/rename.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +13 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/parsers/excel-parser.d.ts +6 -0
  23. package/dist/parsers/excel-parser.d.ts.map +1 -0
  24. package/dist/parsers/excel-parser.js +42 -0
  25. package/dist/parsers/excel-parser.js.map +1 -0
  26. package/dist/parsers/factory.d.ts +7 -0
  27. package/dist/parsers/factory.d.ts.map +1 -0
  28. package/dist/parsers/factory.js +29 -0
  29. package/dist/parsers/factory.js.map +1 -0
  30. package/dist/parsers/pdf-parser.d.ts +7 -0
  31. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  32. package/dist/parsers/pdf-parser.js +67 -0
  33. package/dist/parsers/pdf-parser.js.map +1 -0
  34. package/dist/parsers/text-parser.d.ts +6 -0
  35. package/dist/parsers/text-parser.d.ts.map +1 -0
  36. package/dist/parsers/text-parser.js +39 -0
  37. package/dist/parsers/text-parser.js.map +1 -0
  38. package/dist/parsers/word-parser.d.ts +6 -0
  39. package/dist/parsers/word-parser.d.ts.map +1 -0
  40. package/dist/parsers/word-parser.js +44 -0
  41. package/dist/parsers/word-parser.js.map +1 -0
  42. package/dist/services/ai-factory.d.ts +5 -0
  43. package/dist/services/ai-factory.d.ts.map +1 -0
  44. package/dist/services/ai-factory.js +15 -0
  45. package/dist/services/ai-factory.js.map +1 -0
  46. package/dist/services/claude-service.d.ts +9 -0
  47. package/dist/services/claude-service.d.ts.map +1 -0
  48. package/dist/services/claude-service.js +113 -0
  49. package/dist/services/claude-service.js.map +1 -0
  50. package/dist/services/file-renamer.d.ts +12 -0
  51. package/dist/services/file-renamer.d.ts.map +1 -0
  52. package/dist/services/file-renamer.js +99 -0
  53. package/dist/services/file-renamer.js.map +1 -0
  54. package/dist/services/openai-service.d.ts +9 -0
  55. package/dist/services/openai-service.d.ts.map +1 -0
  56. package/dist/services/openai-service.js +112 -0
  57. package/dist/services/openai-service.js.map +1 -0
  58. package/dist/types/index.d.ts +61 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +2 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/utils/file-templates.d.ts +18 -0
  63. package/dist/utils/file-templates.d.ts.map +1 -0
  64. package/dist/utils/file-templates.js +232 -0
  65. package/dist/utils/file-templates.js.map +1 -0
  66. package/dist/utils/naming-conventions.d.ts +4 -0
  67. package/dist/utils/naming-conventions.d.ts.map +1 -0
  68. package/dist/utils/naming-conventions.js +55 -0
  69. package/dist/utils/naming-conventions.js.map +1 -0
  70. package/package.json +75 -0
  71. package/src/cli/commands.ts +20 -0
  72. package/src/cli/rename.ts +157 -0
  73. package/src/index.ts +17 -0
  74. package/src/parsers/excel-parser.ts +49 -0
  75. package/src/parsers/factory.ts +34 -0
  76. package/src/parsers/pdf-parser.ts +78 -0
  77. package/src/parsers/text-parser.ts +43 -0
  78. package/src/parsers/word-parser.ts +50 -0
  79. package/src/services/ai-factory.ts +16 -0
  80. package/src/services/claude-service.ts +114 -0
  81. package/src/services/file-renamer.ts +123 -0
  82. package/src/services/openai-service.ts +113 -0
  83. package/src/types/index.ts +71 -0
  84. package/src/types/pdf-extraction.d.ts +7 -0
  85. package/src/utils/file-templates.ts +275 -0
  86. package/src/utils/naming-conventions.ts +67 -0
  87. package/tests/data/empty-file.txt +0 -0
  88. package/tests/data/sample-markdown.md +9 -0
  89. package/tests/data/sample-pdf.pdf +0 -0
  90. package/tests/data/sample-text.txt +25 -0
  91. package/tests/integration/end-to-end.test.ts +209 -0
  92. package/tests/integration/workflow.test.ts +336 -0
  93. package/tests/mocks/mock-ai-service.ts +58 -0
  94. package/tests/unit/cli/commands.test.ts +163 -0
  95. package/tests/unit/parsers/factory.test.ts +100 -0
  96. package/tests/unit/parsers/pdf-parser.test.ts +63 -0
  97. package/tests/unit/parsers/text-parser.test.ts +85 -0
  98. package/tests/unit/services/ai-factory.test.ts +37 -0
  99. package/tests/unit/services/claude-service.test.ts +188 -0
  100. package/tests/unit/services/file-renamer.test.ts +299 -0
  101. package/tests/unit/services/openai-service.test.ts +196 -0
  102. package/tests/unit/utils/file-templates.test.ts +199 -0
  103. package/tests/unit/utils/naming-conventions.test.ts +88 -0
  104. package/tsconfig.json +20 -0
  105. 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
+ });