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 });