/ src / services / canvas-parser-service.test.ts
canvas-parser-service.test.ts
  1  import { describe, it, expect, beforeEach, vi } from 'vitest';
  2  import { CanvasParserService, CanvasData } from './canvas-parser-service';
  3  import { VaultService } from './vault-service';
  4  
  5  describe('CanvasParserService', () => {
  6    let canvasParserService: CanvasParserService;
  7    let mockVaultService: VaultService;
  8  
  9    beforeEach(() => {
 10      // Mock VaultService
 11      mockVaultService = {
 12        readFile: vi.fn(),
 13        writeFile: vi.fn(),
 14        fileExists: vi.fn(),
 15        folderExists: vi.fn(),
 16        createFolder: vi.fn(),
 17        deleteFile: vi.fn()
 18      } as unknown as VaultService;
 19  
 20      canvasParserService = new CanvasParserService(mockVaultService);
 21    });
 22  
 23    describe('parseCanvas', () => {
 24      it('should parse a valid canvas file', async () => {
 25        const mockCanvasData: CanvasData = {
 26          nodes: [
 27            {
 28              id: 'node1',
 29              type: 'file',
 30              x: 100,
 31              y: 200,
 32              width: 300,
 33              height: 400,
 34              file: 'test-file.md'
 35            },
 36            {
 37              id: 'node2',
 38              type: 'text',
 39              x: 500,
 40              y: 600,
 41              width: 200,
 42              height: 100,
 43              text: 'Test text'
 44            }
 45          ],
 46          edges: [
 47            {
 48              id: 'edge1',
 49              fromNode: 'node1',
 50              toNode: 'node2'
 51            }
 52          ]
 53        };
 54  
 55        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(mockCanvasData));
 56  
 57        const result = await canvasParserService.parseCanvas('test-canvas.canvas');
 58  
 59        expect(result).toEqual(mockCanvasData);
 60        expect(mockVaultService.readFile).toHaveBeenCalledWith('test-canvas.canvas');
 61      });
 62  
 63      it('should throw error for invalid JSON', async () => {
 64        vi.mocked(mockVaultService.readFile).mockResolvedValue('invalid json {');
 65  
 66        await expect(canvasParserService.parseCanvas('invalid.canvas'))
 67          .rejects.toThrow('Invalid canvas JSON in invalid.canvas');
 68      });
 69  
 70      it('should throw error for missing nodes array', async () => {
 71        const invalidCanvas = { edges: [] };
 72        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(invalidCanvas));
 73  
 74        await expect(canvasParserService.parseCanvas('missing-nodes.canvas'))
 75          .rejects.toThrow('Invalid canvas format: missing or invalid nodes array');
 76      });
 77  
 78      it('should throw error for missing edges array', async () => {
 79        const invalidCanvas = { nodes: [] };
 80        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(invalidCanvas));
 81  
 82        await expect(canvasParserService.parseCanvas('missing-edges.canvas'))
 83          .rejects.toThrow('Invalid canvas format: missing or invalid edges array');
 84      });
 85    });
 86  
 87    describe('findDreamNodeBoundary', () => {
 88      it('should find .udd file in parent directory', async () => {
 89        vi.mocked(mockVaultService.fileExists)
 90          .mockImplementation(async (path: string) => {
 91            return path === 'parent/.udd';
 92          });
 93  
 94        const result = await canvasParserService.findDreamNodeBoundary('parent/subdir/file.canvas');
 95  
 96        expect(result).toBe('parent');
 97        expect(mockVaultService.fileExists).toHaveBeenCalledWith('parent/.udd');
 98      });
 99  
100      it('should find .udd file at root level', async () => {
101        vi.mocked(mockVaultService.fileExists)
102          .mockImplementation(async (path: string) => {
103            return path === '.udd';
104          });
105  
106        const result = await canvasParserService.findDreamNodeBoundary('some/deep/path/file.canvas');
107  
108        expect(result).toBe('');
109        expect(mockVaultService.fileExists).toHaveBeenCalledWith('.udd');
110      });
111  
112      it('should return null when no .udd file found', async () => {
113        vi.mocked(mockVaultService.fileExists).mockResolvedValue(false);
114  
115        const result = await canvasParserService.findDreamNodeBoundary('path/file.canvas');
116  
117        expect(result).toBeNull();
118      });
119  
120      it('should use cache for repeated calls', async () => {
121        vi.mocked(mockVaultService.fileExists)
122          .mockImplementation(async (path: string) => {
123            return path === 'parent/.udd';
124          });
125  
126        // First call
127        const result1 = await canvasParserService.findDreamNodeBoundary('parent/file.canvas');
128        // Second call with same file - should use cache
129        const result2 = await canvasParserService.findDreamNodeBoundary('parent/file.canvas');
130  
131        expect(result1).toBe('parent');
132        expect(result2).toBe('parent');
133        // Cache means second call doesn't need any file system checks
134        expect(mockVaultService.fileExists).toHaveBeenCalledTimes(3); // First call: file check + dir check + .udd check
135      });
136    });
137  
138    describe('analyzeCanvasDependencies', () => {
139      const mockCanvasData: CanvasData = {
140        nodes: [
141          {
142            id: 'internal-file',
143            type: 'file',
144            x: 0,
145            y: 0,
146            width: 100,
147            height: 100,
148            file: 'current-node/internal.md'
149          },
150          {
151            id: 'external-file',
152            type: 'file',
153            x: 200,
154            y: 0,
155            width: 100,
156            height: 100,
157            file: 'other-node/external.md'
158          },
159          {
160            id: 'text-node',
161            type: 'text',
162            x: 400,
163            y: 0,
164            width: 100,
165            height: 100,
166            text: 'Some text'
167          }
168        ],
169        edges: []
170      };
171  
172      beforeEach(() => {
173        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(mockCanvasData));
174      });
175  
176      it('should identify external dependencies correctly', async () => {
177        vi.mocked(mockVaultService.fileExists)
178          .mockImplementation(async (path: string) => {
179            if (path === 'current-node/udd') return true;
180            if (path === 'other-node/udd') return true;
181            return false;
182          });
183  
184        // Mock findDreamNodeBoundary behavior
185        canvasParserService.findDreamNodeBoundary = vi.fn()
186          .mockImplementation(async (path: string) => {
187            if (path.startsWith('current-node/')) return 'current-node';
188            if (path.startsWith('other-node/')) return 'other-node';
189            return null;
190          });
191  
192        const analysis = await canvasParserService.analyzeCanvasDependencies('current-node/canvas.canvas');
193  
194        expect(analysis.canvasPath).toBe('current-node/canvas.canvas');
195        expect(analysis.dreamNodeBoundary).toBe('current-node');
196        expect(analysis.dependencies).toHaveLength(2);
197        expect(analysis.externalDependencies).toHaveLength(1);
198        expect(analysis.hasExternalDependencies).toBe(true);
199  
200        const externalDep = analysis.externalDependencies[0];
201        expect(externalDep.filePath).toBe('other-node/external.md');
202        expect(externalDep.isExternal).toBe(true);
203        expect(externalDep.dreamNodePath).toBe('other-node');
204      });
205  
206      it('should handle canvas with no external dependencies', async () => {
207        const internalOnlyCanvas: CanvasData = {
208          nodes: [
209            {
210              id: 'internal-file',
211              type: 'file',
212              x: 0,
213              y: 0,
214              width: 100,
215              height: 100,
216              file: 'current-node/internal.md'
217            }
218          ],
219          edges: []
220        };
221  
222        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(internalOnlyCanvas));
223        canvasParserService.findDreamNodeBoundary = vi.fn().mockResolvedValue('current-node');
224  
225        const analysis = await canvasParserService.analyzeCanvasDependencies('current-node/canvas.canvas');
226  
227        expect(analysis.externalDependencies).toHaveLength(0);
228        expect(analysis.hasExternalDependencies).toBe(false);
229      });
230  
231      it('should throw error if canvas is not in a DreamNode', async () => {
232        canvasParserService.findDreamNodeBoundary = vi.fn().mockResolvedValue(null);
233  
234        await expect(canvasParserService.analyzeCanvasDependencies('orphan-canvas.canvas'))
235          .rejects.toThrow('Canvas orphan-canvas.canvas is not inside a DreamNode');
236      });
237    });
238  
239    describe('updateCanvasFilePaths', () => {
240      it('should update file paths in canvas', async () => {
241        const originalCanvas: CanvasData = {
242          nodes: [
243            {
244              id: 'node1',
245              type: 'file',
246              x: 0,
247              y: 0,
248              width: 100,
249              height: 100,
250              file: 'old-path/file.md'
251            },
252            {
253              id: 'node2',
254              type: 'file',
255              x: 200,
256              y: 0,
257              width: 100,
258              height: 100,
259              file: 'other-old-path/file2.md'
260            }
261          ],
262          edges: []
263        };
264  
265        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(originalCanvas));
266  
267        const pathUpdates = new Map<string, string>([
268          ['old-path/file.md', 'new-path/file.md'],
269          ['other-old-path/file2.md', 'new-path2/file2.md']
270        ]);
271  
272        await canvasParserService.updateCanvasFilePaths('test.canvas', pathUpdates);
273  
274        expect(mockVaultService.writeFile).toHaveBeenCalledWith(
275          'test.canvas',
276          expect.stringContaining('"file": "new-path/file.md"')
277        );
278        expect(mockVaultService.writeFile).toHaveBeenCalledWith(
279          'test.canvas',
280          expect.stringContaining('"file": "new-path2/file2.md"')
281        );
282      });
283  
284      it('should not modify canvas if no paths to update', async () => {
285        const originalCanvas: CanvasData = {
286          nodes: [
287            {
288              id: 'node1',
289              type: 'file',
290              x: 0,
291              y: 0,
292              width: 100,
293              height: 100,
294              file: 'unchanged-path/file.md'
295            }
296          ],
297          edges: []
298        };
299  
300        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(originalCanvas));
301  
302        const pathUpdates = new Map<string, string>([
303          ['different-path/file.md', 'new-path/file.md']
304        ]);
305  
306        await canvasParserService.updateCanvasFilePaths('test.canvas', pathUpdates);
307  
308        expect(mockVaultService.writeFile).not.toHaveBeenCalled();
309      });
310    });
311  
312    describe('getFileNodes', () => {
313      it('should return only file type nodes', async () => {
314        const mixedCanvas: CanvasData = {
315          nodes: [
316            {
317              id: 'file1',
318              type: 'file',
319              x: 0,
320              y: 0,
321              width: 100,
322              height: 100,
323              file: 'test1.md'
324            },
325            {
326              id: 'text1',
327              type: 'text',
328              x: 200,
329              y: 0,
330              width: 100,
331              height: 100,
332              text: 'Some text'
333            },
334            {
335              id: 'file2',
336              type: 'file',
337              x: 400,
338              y: 0,
339              width: 100,
340              height: 100,
341              file: 'test2.md'
342            }
343          ],
344          edges: []
345        };
346  
347        vi.mocked(mockVaultService.readFile).mockResolvedValue(JSON.stringify(mixedCanvas));
348  
349        const fileNodes = await canvasParserService.getFileNodes('test.canvas');
350  
351        expect(fileNodes).toHaveLength(2);
352        expect(fileNodes.every(node => node.type === 'file')).toBe(true);
353        expect(fileNodes.map(node => node.file)).toEqual(['test1.md', 'test2.md']);
354      });
355    });
356  
357    describe('generateAnalysisReport', () => {
358      it('should generate a readable report', () => {
359        const analysis = {
360          canvasPath: 'test/canvas.canvas',
361          dreamNodeBoundary: 'test',
362          dependencies: [
363            { filePath: 'test/internal.md', nodeId: 'node1', isExternal: false },
364            { filePath: 'other/external.md', nodeId: 'node2', isExternal: true, dreamNodePath: 'other' }
365          ],
366          externalDependencies: [
367            { filePath: 'other/external.md', nodeId: 'node2', isExternal: true, dreamNodePath: 'other' }
368          ],
369          hasExternalDependencies: true
370        };
371  
372        const report = canvasParserService.generateAnalysisReport(analysis);
373  
374        expect(report).toContain('Canvas Analysis: test/canvas.canvas');
375        expect(report).toContain('DreamNode Boundary: test');
376        expect(report).toContain('Total Dependencies: 2');
377        expect(report).toContain('External Dependencies: 1');
378        expect(report).toContain('other/external.md (in other)');
379      });
380  
381      it('should handle no external dependencies', () => {
382        const analysis = {
383          canvasPath: 'test/canvas.canvas',
384          dreamNodeBoundary: 'test',
385          dependencies: [
386            { filePath: 'test/internal.md', nodeId: 'node1', isExternal: false }
387          ],
388          externalDependencies: [],
389          hasExternalDependencies: false
390        };
391  
392        const report = canvasParserService.generateAnalysisReport(analysis);
393  
394        expect(report).toContain('No external dependencies found.');
395      });
396    });
397  
398    describe('clearCache', () => {
399      it('should clear the boundary detection cache', async () => {
400        vi.mocked(mockVaultService.fileExists)
401          .mockImplementation(async (path: string) => path === 'parent/.udd');
402  
403        // First call - should check file system
404        await canvasParserService.findDreamNodeBoundary('parent/file.canvas');
405        expect(mockVaultService.fileExists).toHaveBeenCalledTimes(3); // file check + dir check + .udd check
406  
407        // Second call - should use cache
408        await canvasParserService.findDreamNodeBoundary('parent/file.canvas');
409        expect(mockVaultService.fileExists).toHaveBeenCalledTimes(3); // No additional calls
410  
411        // Clear cache
412        canvasParserService.clearCache();
413  
414        // Third call - should check file system again
415        await canvasParserService.findDreamNodeBoundary('parent/file.canvas');
416        expect(mockVaultService.fileExists).toHaveBeenCalledTimes(6); // 3 more calls after cache clear
417      });
418    });
419  });