@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,63 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PDFParser } from '../../../src/parsers/pdf-parser.js';
3
+ import path from 'path';
4
+
5
+ describe('PDFParser', () => {
6
+ let parser: PDFParser;
7
+ const testDataDir = path.join(process.cwd(), 'tests/data');
8
+
9
+ beforeEach(() => {
10
+ parser = new PDFParser();
11
+ });
12
+
13
+ describe('supports()', () => {
14
+ it('should support .pdf files', () => {
15
+ expect(parser.supports('test.pdf')).toBe(true);
16
+ });
17
+
18
+ it('should not support other file types', () => {
19
+ expect(parser.supports('test.txt')).toBe(false);
20
+ expect(parser.supports('test.docx')).toBe(false);
21
+ expect(parser.supports('test.xlsx')).toBe(false);
22
+ expect(parser.supports('test.md')).toBe(false);
23
+ });
24
+
25
+ it('should be case insensitive', () => {
26
+ expect(parser.supports('test.PDF')).toBe(true);
27
+ expect(parser.supports('test.Pdf')).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe('parse()', () => {
32
+ it('should parse PDF file content correctly', async () => {
33
+ const filePath = path.join(testDataDir, 'sample-pdf.pdf');
34
+ const result = await parser.parse(filePath);
35
+
36
+ expect(result.content.length).toBeGreaterThan(0);
37
+ expect(typeof result.content).toBe('string');
38
+ expect(result.metadata).toBeDefined();
39
+ // Check for some expected content from the test PDF
40
+ expect(result.content.toLowerCase()).toMatch(/trace|type|specialization|dynamic|languages/);
41
+ });
42
+
43
+ it('should throw error for non-existent files', async () => {
44
+ const filePath = path.join(testDataDir, 'non-existent.pdf');
45
+
46
+ await expect(parser.parse(filePath)).rejects.toThrow('Failed to parse PDF file');
47
+ });
48
+
49
+ it('should throw error for invalid PDF files', async () => {
50
+ const filePath = path.join(testDataDir, 'sample-text.txt'); // Not a PDF
51
+
52
+ await expect(parser.parse(filePath)).rejects.toThrow('Failed to parse PDF file');
53
+ });
54
+
55
+ it('should trim whitespace from extracted content', async () => {
56
+ const filePath = path.join(testDataDir, 'sample-pdf.pdf');
57
+ const result = await parser.parse(filePath);
58
+
59
+ expect(result.content).not.toMatch(/^\s/);
60
+ expect(result.content).not.toMatch(/\s$/);
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { TextParser } from '../../../src/parsers/text-parser.js';
3
+ import path from 'path';
4
+
5
+ describe('TextParser', () => {
6
+ let parser: TextParser;
7
+ const testDataDir = path.join(process.cwd(), 'tests/data');
8
+
9
+ beforeEach(() => {
10
+ parser = new TextParser();
11
+ });
12
+
13
+ describe('supports()', () => {
14
+ it('should support .txt files', () => {
15
+ expect(parser.supports('test.txt')).toBe(true);
16
+ });
17
+
18
+ it('should support .md files', () => {
19
+ expect(parser.supports('test.md')).toBe(true);
20
+ });
21
+
22
+ it('should support .rtf files', () => {
23
+ expect(parser.supports('test.rtf')).toBe(true);
24
+ });
25
+
26
+ it('should not support other file types', () => {
27
+ expect(parser.supports('test.pdf')).toBe(false);
28
+ expect(parser.supports('test.docx')).toBe(false);
29
+ expect(parser.supports('test.xlsx')).toBe(false);
30
+ expect(parser.supports('test.png')).toBe(false);
31
+ });
32
+
33
+ it('should be case insensitive', () => {
34
+ expect(parser.supports('test.TXT')).toBe(true);
35
+ expect(parser.supports('test.MD')).toBe(true);
36
+ expect(parser.supports('test.RTF')).toBe(true);
37
+ });
38
+ });
39
+
40
+ describe('parse()', () => {
41
+ it('should parse text file content correctly', async () => {
42
+ const filePath = path.join(testDataDir, 'sample-text.txt');
43
+ const result = await parser.parse(filePath);
44
+
45
+ expect(result.content).toContain('Project Requirements Document');
46
+ expect(result.content).toContain('customer management system');
47
+ expect(result.content).toContain('React.js');
48
+ expect(result.content.length).toBeGreaterThan(0);
49
+ expect(result.metadata).toBeDefined();
50
+ });
51
+
52
+ it('should parse markdown file content correctly', async () => {
53
+ const filePath = path.join(testDataDir, 'sample-markdown.md');
54
+ const result = await parser.parse(filePath);
55
+
56
+ expect(result.content).toContain('Meeting Notes');
57
+ expect(result.content).toContain('Action Items');
58
+ expect(result.content).toContain('John, Sarah, Mike');
59
+ expect(result.content.length).toBeGreaterThan(0);
60
+ expect(result.metadata).toBeDefined();
61
+ });
62
+
63
+ it('should handle empty files', async () => {
64
+ const filePath = path.join(testDataDir, 'empty-file.txt');
65
+ const result = await parser.parse(filePath);
66
+
67
+ expect(result.content).toBe('');
68
+ expect(result.metadata).toBeDefined();
69
+ });
70
+
71
+ it('should throw error for non-existent files', async () => {
72
+ const filePath = path.join(testDataDir, 'non-existent.txt');
73
+
74
+ await expect(parser.parse(filePath)).rejects.toThrow('Failed to parse text file');
75
+ });
76
+
77
+ it('should trim whitespace from content', async () => {
78
+ const filePath = path.join(testDataDir, 'sample-text.txt');
79
+ const result = await parser.parse(filePath);
80
+
81
+ expect(result.content).not.toMatch(/^\s/);
82
+ expect(result.content).not.toMatch(/\s$/);
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AIServiceFactory } from '../../../src/services/ai-factory.js';
3
+
4
+ describe('AIServiceFactory', () => {
5
+ describe('create()', () => {
6
+ it('should create ClaudeService for claude provider', () => {
7
+ const service = AIServiceFactory.create('claude', 'test-api-key');
8
+
9
+ expect(service).toBeDefined();
10
+ expect(service.name).toBe('Claude');
11
+ expect(typeof service.generateFileName).toBe('function');
12
+ });
13
+
14
+ it('should create OpenAIService for openai provider', () => {
15
+ const service = AIServiceFactory.create('openai', 'test-api-key');
16
+
17
+ expect(service).toBeDefined();
18
+ expect(service.name).toBe('OpenAI');
19
+ expect(typeof service.generateFileName).toBe('function');
20
+ });
21
+
22
+ it('should throw error for unsupported provider', () => {
23
+ expect(() => {
24
+ AIServiceFactory.create('unsupported' as any, 'test-api-key');
25
+ }).toThrow('Unsupported AI provider: unsupported');
26
+ });
27
+
28
+ it('should pass API key to created services', () => {
29
+ const claudeService = AIServiceFactory.create('claude', 'claude-key');
30
+ const openaiService = AIServiceFactory.create('openai', 'openai-key');
31
+
32
+ // Services should be created without throwing errors
33
+ expect(claudeService).toBeDefined();
34
+ expect(openaiService).toBeDefined();
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ClaudeService } from '../../../src/services/claude-service.js';
3
+
4
+ // Mock the Anthropic SDK
5
+ vi.mock('@anthropic-ai/sdk', () => {
6
+ const MockAnthropic = vi.fn().mockImplementation(() => ({
7
+ messages: {
8
+ create: vi.fn()
9
+ }
10
+ }));
11
+ return {
12
+ default: MockAnthropic
13
+ };
14
+ });
15
+
16
+ describe('ClaudeService', () => {
17
+ let service: ClaudeService;
18
+ let mockClient: any;
19
+
20
+ beforeEach(() => {
21
+ service = new ClaudeService('test-api-key');
22
+ mockClient = (service as any).client;
23
+ });
24
+
25
+ describe('Basic Properties', () => {
26
+ it('should have correct name', () => {
27
+ expect(service.name).toBe('Claude');
28
+ });
29
+
30
+ it('should initialize with API key', () => {
31
+ expect(service).toBeDefined();
32
+ });
33
+ });
34
+
35
+ describe('generateFileName() with different naming conventions', () => {
36
+ const sampleContent = 'This is a project requirements document for Q4 2024 planning meeting.';
37
+ const originalName = 'document1.pdf';
38
+
39
+ beforeEach(() => {
40
+ mockClient.messages.create.mockResolvedValue({
41
+ content: [{ type: 'text', text: 'project requirements document q4 2024' }]
42
+ });
43
+ });
44
+
45
+ it('should generate filename with kebab-case convention (default)', async () => {
46
+ const result = await service.generateFileName(sampleContent, originalName);
47
+
48
+ expect(mockClient.messages.create).toHaveBeenCalledWith(
49
+ expect.objectContaining({
50
+ model: 'claude-3-haiku-20240307',
51
+ messages: [expect.objectContaining({
52
+ role: 'user',
53
+ content: expect.stringContaining('Use lowercase with hyphens between words')
54
+ })]
55
+ })
56
+ );
57
+
58
+ expect(result).toBe('project-requirements-document-q4-2024');
59
+ });
60
+
61
+ it('should generate filename with snake_case convention', async () => {
62
+ const result = await service.generateFileName(sampleContent, originalName, 'snake_case');
63
+
64
+ expect(mockClient.messages.create).toHaveBeenCalledWith(
65
+ expect.objectContaining({
66
+ messages: [expect.objectContaining({
67
+ content: expect.stringContaining('Use lowercase with underscores between words')
68
+ })]
69
+ })
70
+ );
71
+
72
+ expect(result).toBe('project_requirements_document_q4_2024');
73
+ });
74
+
75
+ it('should generate filename with camelCase convention', async () => {
76
+ mockClient.messages.create.mockResolvedValue({
77
+ content: [{ type: 'text', text: 'project requirements document q4 2024' }]
78
+ });
79
+
80
+ const result = await service.generateFileName(sampleContent, originalName, 'camelCase');
81
+
82
+ expect(mockClient.messages.create).toHaveBeenCalledWith(
83
+ expect.objectContaining({
84
+ messages: [expect.objectContaining({
85
+ content: expect.stringContaining('Use camelCase format starting with lowercase')
86
+ })]
87
+ })
88
+ );
89
+
90
+ expect(result).toBe('projectRequirementsDocumentQ42024');
91
+ });
92
+
93
+ it('should generate filename with PascalCase convention', async () => {
94
+ const result = await service.generateFileName(sampleContent, originalName, 'PascalCase');
95
+
96
+ expect(mockClient.messages.create).toHaveBeenCalledWith(
97
+ expect.objectContaining({
98
+ messages: [expect.objectContaining({
99
+ content: expect.stringContaining('Use PascalCase format starting with uppercase')
100
+ })]
101
+ })
102
+ );
103
+
104
+ expect(result).toBe('ProjectRequirementsDocumentQ42024');
105
+ });
106
+
107
+ it('should include original filename and content in prompt', async () => {
108
+ await service.generateFileName(sampleContent, originalName, 'kebab-case');
109
+
110
+ const call = mockClient.messages.create.mock.calls[0][0];
111
+ expect(call.messages[0].content).toContain(sampleContent.substring(0, 2000));
112
+ expect(call.messages[0].content).toContain('Document content (first 2000 characters)');
113
+ });
114
+
115
+ it('should truncate long content to 2000 characters', async () => {
116
+ const longContent = 'a'.repeat(3000);
117
+ await service.generateFileName(longContent, originalName);
118
+
119
+ const call = mockClient.messages.create.mock.calls[0][0];
120
+ const contentInPrompt = call.messages[0].content;
121
+ // Should only contain first 2000 characters
122
+ expect(contentInPrompt).toContain('a'.repeat(2000));
123
+ expect(contentInPrompt).not.toContain('a'.repeat(2001));
124
+ });
125
+ });
126
+
127
+ describe('Error Handling', () => {
128
+ it('should handle API errors gracefully', async () => {
129
+ mockClient.messages.create.mockRejectedValue(new Error('API Error'));
130
+
131
+ await expect(service.generateFileName('content', 'file.txt')).rejects.toThrow(
132
+ 'Failed to generate filename with Claude: API Error'
133
+ );
134
+ });
135
+
136
+ it('should handle non-text response content', async () => {
137
+ mockClient.messages.create.mockResolvedValue({
138
+ content: [{ type: 'image', text: null }]
139
+ });
140
+
141
+ const result = await service.generateFileName('content', 'file.txt');
142
+ expect(result).toBe('untitled-document');
143
+ });
144
+
145
+ it('should handle empty response', async () => {
146
+ mockClient.messages.create.mockResolvedValue({
147
+ content: [{ type: 'text', text: ' ' }]
148
+ });
149
+
150
+ const result = await service.generateFileName('content', 'file.txt');
151
+ expect(result).toBe('untitled-document');
152
+ });
153
+ });
154
+
155
+ describe('Filename Sanitization', () => {
156
+ beforeEach(() => {
157
+ mockClient.messages.create.mockResolvedValue({
158
+ content: [{ type: 'text', text: 'Test File@#$ Name.pdf' }]
159
+ });
160
+ });
161
+
162
+ it('should remove file extensions from AI suggestions', async () => {
163
+ const result = await service.generateFileName('content', 'original.txt', 'kebab-case');
164
+ expect(result).toBe('test-file-name');
165
+ });
166
+
167
+ it('should handle very long filenames', async () => {
168
+ mockClient.messages.create.mockResolvedValue({
169
+ content: [{ type: 'text', text: 'a'.repeat(150) }]
170
+ });
171
+
172
+ const result = await service.generateFileName('content', 'file.txt', 'kebab-case');
173
+ expect(result.length).toBeLessThanOrEqual(100);
174
+ });
175
+
176
+ it('should preserve naming convention when truncating', async () => {
177
+ mockClient.messages.create.mockResolvedValue({
178
+ content: [{ type: 'text', text: 'very long filename that should be truncated properly' }]
179
+ });
180
+
181
+ const kebabResult = await service.generateFileName('content', 'file.txt', 'kebab-case');
182
+ expect(kebabResult).not.toMatch(/-$/); // Should not end with hyphen
183
+
184
+ const snakeResult = await service.generateFileName('content', 'file.txt', 'snake_case');
185
+ expect(snakeResult).not.toMatch(/_$/); // Should not end with underscore
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,299 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import { FileRenamer } from '../../../src/services/file-renamer.js';
5
+ import { DocumentParserFactory } from '../../../src/parsers/factory.js';
6
+ import { MockAIService } from '../../mocks/mock-ai-service.js';
7
+ import { Config, FileInfo } from '../../../src/types/index.js';
8
+
9
+ // Mock fs.rename to avoid actual file 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
+ }
19
+ };
20
+ });
21
+
22
+ describe('FileRenamer', () => {
23
+ let fileRenamer: FileRenamer;
24
+ let mockAIService: MockAIService;
25
+ let parserFactory: DocumentParserFactory;
26
+ let config: Config;
27
+ const testDataDir = path.join(process.cwd(), 'tests/data');
28
+
29
+ beforeEach(() => {
30
+ mockAIService = new MockAIService();
31
+ parserFactory = new DocumentParserFactory();
32
+ config = {
33
+ aiProvider: 'claude',
34
+ apiKey: 'test-key',
35
+ maxFileSize: 10 * 1024 * 1024, // 10MB
36
+ supportedExtensions: ['.txt', '.pdf', '.docx', '.xlsx'],
37
+ dryRun: false,
38
+ namingConvention: 'kebab-case',
39
+ templateOptions: {
40
+ category: 'general',
41
+ personalName: undefined,
42
+ dateFormat: 'none'
43
+ }
44
+ };
45
+
46
+ fileRenamer = new FileRenamer(parserFactory, mockAIService, config);
47
+
48
+ // Reset mocks
49
+ vi.clearAllMocks();
50
+ mockAIService.resetCallCount();
51
+ });
52
+
53
+ afterEach(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ describe('renameFiles()', () => {
58
+ it('should successfully rename files', async () => {
59
+ const testFiles: FileInfo[] = [
60
+ {
61
+ path: path.join(testDataDir, 'sample-text.txt'),
62
+ name: 'sample-text.txt',
63
+ extension: '.txt',
64
+ size: 1000
65
+ }
66
+ ];
67
+
68
+ // Mock fs.access to simulate that new file doesn't exist
69
+ vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
70
+ vi.mocked(fs.rename).mockResolvedValue(undefined);
71
+
72
+ const results = await fileRenamer.renameFiles(testFiles);
73
+
74
+ expect(results).toHaveLength(1);
75
+ expect(results[0].success).toBe(true);
76
+ expect(results[0].originalPath).toBe(testFiles[0].path);
77
+ expect(results[0].newPath).toContain('project-requirements-document.txt');
78
+ expect(mockAIService.getCallCount()).toBe(1);
79
+ expect(fs.rename).toHaveBeenCalledOnce();
80
+ });
81
+
82
+ it('should handle dry run mode', async () => {
83
+ config.dryRun = true;
84
+ fileRenamer = new FileRenamer(parserFactory, mockAIService, config);
85
+
86
+ const testFiles: FileInfo[] = [
87
+ {
88
+ path: path.join(testDataDir, 'sample-text.txt'),
89
+ name: 'sample-text.txt',
90
+ extension: '.txt',
91
+ size: 1000
92
+ }
93
+ ];
94
+
95
+ // Mock fs.access to simulate that new file doesn't exist
96
+ vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
97
+
98
+ const results = await fileRenamer.renameFiles(testFiles);
99
+
100
+ expect(results).toHaveLength(1);
101
+ expect(results[0].success).toBe(true);
102
+ expect(mockAIService.getCallCount()).toBe(1);
103
+ expect(fs.rename).not.toHaveBeenCalled(); // Should not rename in dry run
104
+ });
105
+
106
+ it('should handle file size limits', async () => {
107
+ config.maxFileSize = 100; // Very small limit
108
+ fileRenamer = new FileRenamer(parserFactory, mockAIService, config);
109
+
110
+ const testFiles: FileInfo[] = [
111
+ {
112
+ path: path.join(testDataDir, 'sample-text.txt'),
113
+ name: 'sample-text.txt',
114
+ extension: '.txt',
115
+ size: 1000 // Exceeds limit
116
+ }
117
+ ];
118
+
119
+ const results = await fileRenamer.renameFiles(testFiles);
120
+
121
+ expect(results).toHaveLength(1);
122
+ expect(results[0].success).toBe(false);
123
+ expect(results[0].error).toContain('File size');
124
+ expect(results[0].error).toContain('exceeds maximum');
125
+ expect(mockAIService.getCallCount()).toBe(0); // Should not call AI
126
+ expect(fs.rename).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it('should handle unsupported file types', async () => {
130
+ const testFiles: FileInfo[] = [
131
+ {
132
+ path: path.join(testDataDir, 'unsupported.xyz'),
133
+ name: 'unsupported.xyz',
134
+ extension: '.xyz',
135
+ size: 1000
136
+ }
137
+ ];
138
+
139
+ const results = await fileRenamer.renameFiles(testFiles);
140
+
141
+ expect(results).toHaveLength(1);
142
+ expect(results[0].success).toBe(false);
143
+ expect(results[0].error).toContain('No parser available');
144
+ expect(mockAIService.getCallCount()).toBe(0);
145
+ expect(fs.rename).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it('should handle AI service failures', async () => {
149
+ mockAIService.setShouldFail(true);
150
+
151
+ const testFiles: FileInfo[] = [
152
+ {
153
+ path: path.join(testDataDir, 'sample-text.txt'),
154
+ name: 'sample-text.txt',
155
+ extension: '.txt',
156
+ size: 1000
157
+ }
158
+ ];
159
+
160
+ const results = await fileRenamer.renameFiles(testFiles);
161
+
162
+ expect(results).toHaveLength(1);
163
+ expect(results[0].success).toBe(false);
164
+ expect(results[0].error).toContain('Mock AI service failed');
165
+ expect(mockAIService.getCallCount()).toBe(1);
166
+ expect(fs.rename).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('should handle file conflicts', async () => {
170
+ const testFiles: FileInfo[] = [
171
+ {
172
+ path: path.join(testDataDir, 'sample-text.txt'),
173
+ name: 'sample-text.txt',
174
+ extension: '.txt',
175
+ size: 1000
176
+ }
177
+ ];
178
+
179
+ // Mock fs.access to simulate that new file already exists
180
+ vi.mocked(fs.access).mockResolvedValue(undefined);
181
+
182
+ const results = await fileRenamer.renameFiles(testFiles);
183
+
184
+ expect(results).toHaveLength(1);
185
+ expect(results[0].success).toBe(false);
186
+ expect(results[0].error).toContain('Target filename already exists');
187
+ expect(mockAIService.getCallCount()).toBe(1);
188
+ expect(fs.rename).not.toHaveBeenCalled();
189
+ });
190
+
191
+ it('should handle multiple files', async () => {
192
+ const testFiles: FileInfo[] = [
193
+ {
194
+ path: path.join(testDataDir, 'sample-text.txt'),
195
+ name: 'sample-text.txt',
196
+ extension: '.txt',
197
+ size: 1000
198
+ },
199
+ {
200
+ path: path.join(testDataDir, 'sample-markdown.md'),
201
+ name: 'sample-markdown.md',
202
+ extension: '.md',
203
+ size: 500
204
+ }
205
+ ];
206
+
207
+ vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
208
+ vi.mocked(fs.rename).mockResolvedValue(undefined);
209
+
210
+ const results = await fileRenamer.renameFiles(testFiles);
211
+
212
+ expect(results).toHaveLength(2);
213
+ expect(results.every(r => r.success)).toBe(true);
214
+ expect(mockAIService.getCallCount()).toBe(2);
215
+ expect(fs.rename).toHaveBeenCalledTimes(2);
216
+ });
217
+
218
+ it('should handle empty file content', async () => {
219
+ const testFiles: FileInfo[] = [
220
+ {
221
+ path: path.join(testDataDir, 'empty-file.txt'),
222
+ name: 'empty-file.txt',
223
+ extension: '.txt',
224
+ size: 0
225
+ }
226
+ ];
227
+
228
+ const results = await fileRenamer.renameFiles(testFiles);
229
+
230
+ expect(results).toHaveLength(1);
231
+ expect(results[0].success).toBe(false);
232
+ expect(results[0].error).toContain('No content could be extracted');
233
+ expect(mockAIService.getCallCount()).toBe(0);
234
+ expect(fs.rename).not.toHaveBeenCalled();
235
+ });
236
+
237
+ it('should not rename if filename would be the same', async () => {
238
+ // Set up mock to return a name that would result in the same filename
239
+ mockAIService.setMockResponse('default', 'sample-text');
240
+
241
+ const testFiles: FileInfo[] = [
242
+ {
243
+ path: path.join(testDataDir, 'sample-text.txt'),
244
+ name: 'sample-text.txt',
245
+ extension: '.txt',
246
+ size: 1000
247
+ }
248
+ ];
249
+
250
+ vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
251
+
252
+ const results = await fileRenamer.renameFiles(testFiles);
253
+
254
+ expect(results).toHaveLength(1);
255
+ expect(results[0].success).toBe(true);
256
+ expect(results[0].originalPath).toBe(results[0].newPath); // Same path
257
+ expect(mockAIService.getCallCount()).toBe(1);
258
+ expect(fs.rename).not.toHaveBeenCalled(); // No rename needed
259
+ });
260
+
261
+ it('should pass naming convention to AI service', async () => {
262
+ config.namingConvention = 'snake_case';
263
+ fileRenamer = new FileRenamer(parserFactory, mockAIService, config);
264
+
265
+ // Spy on the generateFileName method
266
+ const generateFileNameSpy = vi.spyOn(mockAIService, 'generateFileName');
267
+
268
+ const testFiles: FileInfo[] = [
269
+ {
270
+ path: path.join(testDataDir, 'sample-text.txt'),
271
+ name: 'sample-text.txt',
272
+ extension: '.txt',
273
+ size: 1000
274
+ }
275
+ ];
276
+
277
+ // Mock fs.access to simulate that new file doesn't exist
278
+ vi.mocked(fs.access).mockRejectedValue({ code: 'ENOENT' });
279
+
280
+ const results = await fileRenamer.renameFiles(testFiles);
281
+
282
+ expect(results).toHaveLength(1);
283
+ expect(results[0].success).toBe(true);
284
+
285
+ // Verify AI service was called with the naming convention, category, and file info
286
+ expect(generateFileNameSpy).toHaveBeenCalledWith(
287
+ expect.any(String),
288
+ 'sample-text.txt',
289
+ 'snake_case',
290
+ 'general', // Uses general template since that's the default (no auto-categorization)
291
+ expect.objectContaining({
292
+ name: 'sample-text.txt',
293
+ extension: '.txt',
294
+ documentMetadata: expect.any(Object)
295
+ })
296
+ );
297
+ });
298
+ });
299
+ });