import { workspace } from '@services/wiki/wikiWorker/services'; import type { IWikiWorkspace } from '@services/workspaces/interface'; import type { FileInfo, Tiddler, Wiki } from 'tiddlywiki'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FileSystemAdaptor } from '../FileSystemAdaptor'; // Mock the workspace service vi.mock('@services/wiki/wikiWorker/services', () => ({ workspace: { get: vi.fn(), getWorkspacesAsList: vi.fn(), }, })); // Mock TiddlyWiki global const mockLogger = { log: vi.fn(), alert: vi.fn(), }; const mockUtils = { Logger: vi.fn(() => mockLogger), createDirectory: vi.fn(), generateTiddlerFileInfo: vi.fn(), saveTiddlerToFile: vi.fn(), deleteTiddlerFile: vi.fn(), cleanupTiddlerFiles: vi.fn(), getFileExtensionInfo: vi.fn(() => ({ type: 'application/x-tiddler' })), }; // Setup TiddlyWiki global // @ts-expect-error - Setting up global for testing global.$tw = { node: true, boot: { wikiTiddlersPath: '/test/wiki/tiddlers', files: {} as Record, }, utils: mockUtils, }; describe('FileSystemAdaptor - Routing Logic', () => { let adaptor: FileSystemAdaptor; let mockWiki: Wiki; beforeEach(() => { vi.clearAllMocks(); // @ts-expect-error - TiddlyWiki global global.$tw.boot.files = {}; mockWiki = { getTiddlerText: vi.fn(() => ''), tiddlerExists: vi.fn(() => false), addTiddler: vi.fn(), } as unknown as Wiki; mockUtils.generateTiddlerFileInfo.mockReturnValue({ filepath: '/test/wiki/tiddlers/test.tid', type: 'application/x-tiddler', hasMetaFile: false, }); }); describe('getTiddlerFileInfo - Default Routing', () => { beforeEach(async () => { vi.mocked(workspace.get).mockResolvedValue( { id: 'test-workspace', name: 'Test Workspace', wikiFolderLocation: '/test/wiki', } as Parameters[0] extends Promise ? T : never, ); vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([]); adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); }); it('should generate file info for tiddler without tags', async () => { const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: [] }, } as unknown as Tiddler; const result = await adaptor.getTiddlerFileInfo(tiddler); expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ directory: '/test/wiki/tiddlers', pathFilters: undefined, wiki: mockWiki, }), ); expect(result).toBeTruthy(); }); it('should use FileSystemPaths config when available', async () => { vi.mocked(mockWiki.tiddlerExists).mockImplementation((title) => title === '$:/config/FileSystemPaths'); vi.mocked(mockWiki.getTiddlerText).mockReturnValue('[tag[Journal]]/journal/\n[tag[Task]]/tasks/'); const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: [] }, } as unknown as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ pathFilters: ['[tag[Journal]]/journal/', '[tag[Task]]/tasks/'], }), ); }); it('should filter out empty lines from FileSystemPaths', async () => { vi.mocked(mockWiki.tiddlerExists).mockImplementation((title) => title === '$:/config/FileSystemPaths'); vi.mocked(mockWiki.getTiddlerText).mockReturnValue('[tag[Journal]]/journal/\n\n[tag[Task]]/tasks/\n '); const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: [] }, } as unknown as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ pathFilters: ['[tag[Journal]]/journal/', '[tag[Task]]/tasks/'], }), ); }); it('should pass extension filters to generateTiddlerFileInfo', async () => { const wikiWithConfig = { getTiddlerText: vi.fn((title) => { if (title === '$:/config/FileSystemExtensions') return '.tid\n.json'; return ''; }), tiddlerExists: vi.fn((title) => title === '$:/config/FileSystemExtensions'), addTiddler: vi.fn(), } as unknown as Wiki; adaptor = new FileSystemAdaptor({ wiki: wikiWithConfig, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: [] }, } as unknown as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ extFilters: ['.tid', '.json'], }), ); }); it('should pass existing fileInfo with overwrite flag', async () => { const existingFileInfo: FileInfo = { filepath: '/test/old.tid', type: 'application/x-tiddler', hasMetaFile: false, }; // @ts-expect-error - TiddlyWiki global global.$tw.boot.files['TestTiddler'] = existingFileInfo; const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: [] }, } as unknown as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ fileInfo: expect.objectContaining({ overwrite: true, }), }), ); }); it('should throw error when wikiTiddlersPath is not set', async () => { // @ts-expect-error - TiddlyWiki global global.$tw.boot.wikiTiddlersPath = undefined; adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); const tiddler: Tiddler = { fields: { title: 'TestTiddler' }, } as unknown as Tiddler; await expect(adaptor.getTiddlerFileInfo(tiddler)).rejects.toThrow( 'filesystem adaptor requires a valid wiki folder', ); // Restore for other tests // @ts-expect-error - TiddlyWiki global global.$tw.boot.wikiTiddlersPath = '/test/wiki/tiddlers'; }); }); describe('getTiddlerFileInfo - Sub-Wiki Routing', () => { beforeEach(async () => { vi.mocked(workspace.get).mockResolvedValue( { id: 'test-workspace', name: 'Test Workspace', wikiFolderLocation: '/test/wiki', } as Parameters[0] extends Promise ? T : never, ); // Setup mock wiki with workspace ID mockWiki = { getTiddlerText: vi.fn((title) => { if (title === '$:/info/tidgi/workspaceID') return 'test-workspace'; return ''; }), tiddlerExists: vi.fn(() => false), addTiddler: vi.fn(), } as unknown as Wiki; }); it('should route to sub-wiki when tiddler has matching tag', async () => { const subWiki = { id: 'sub-wiki-1', name: 'Sub Wiki', isSubWiki: true, mainWikiID: 'test-workspace', tagName: 'SubWikiTag', wikiFolderLocation: '/test/wiki/subwiki/sub1', }; vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as IWikiWorkspace[]); adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); // Manually trigger cache update and wait for it await adaptor['updateSubWikisCache'](); const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: ['SubWikiTag', 'OtherTag'] }, } as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); expect(mockUtils.createDirectory).toHaveBeenCalledWith('/test/wiki/subwiki/sub1'); expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ directory: '/test/wiki/subwiki/sub1', pathFilters: undefined, // Sub-wikis don't use path filters }), ); }); it('should use first matching tag when multiple sub-wiki tags exist', async () => { const subWiki1 = { id: 'sub-1', isSubWiki: true, mainWikiID: 'test-workspace', tagName: 'Tag1', wikiFolderLocation: '/test/wiki/sub1', }; const subWiki2 = { id: 'sub-2', isSubWiki: true, mainWikiID: 'test-workspace', tagName: 'Tag2', wikiFolderLocation: '/test/wiki/sub2', }; vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki1, subWiki2] as IWikiWorkspace[]); adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); // Manually trigger cache update and wait for it await adaptor['updateSubWikisCache'](); const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: ['Tag1', 'Tag2'] }, } as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); // Should use Tag1's directory (first match) expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ directory: '/test/wiki/sub1', }), ); }); it('should use default path when no matching sub-wiki (various scenarios)', async () => { // Test scenario 1: Tag doesn't match const subWikiWithDifferentTag = { id: 'sub-wiki-1', isSubWiki: true, mainWikiID: 'test-workspace', tagName: 'SubWikiTag', wikiFolderLocation: '/test/wiki/subwiki', }; // Test scenario 2: Sub-wiki without tagName const subWikiWithoutTag = { id: 'sub-wiki-2', isSubWiki: true, mainWikiID: 'test-workspace', wikiFolderLocation: '/test/wiki/subwiki2', }; // Test scenario 3: Sub-wiki from different main wiki const otherMainWikiSubWiki = { id: 'sub-wiki-3', isSubWiki: true, mainWikiID: 'other-workspace', tagName: 'AnotherTag', wikiFolderLocation: '/test/otherwiki/subwiki', }; vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([ subWikiWithDifferentTag, subWikiWithoutTag, otherMainWikiSubWiki, ] as IWikiWorkspace[]); adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); // Manually trigger cache update and wait for it await adaptor['updateSubWikisCache'](); // Tiddler with unmatched tags const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: ['UnmatchedTag'] }, } as Tiddler; await adaptor.getTiddlerFileInfo(tiddler); // Should use default directory in all scenarios expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith( tiddler, expect.objectContaining({ directory: '/test/wiki/tiddlers', }), ); }); }); describe('getTiddlerFileInfo - Error Handling', () => { beforeEach(async () => { vi.mocked(workspace.get).mockResolvedValue( { id: 'test-workspace', name: 'Test Workspace', wikiFolderLocation: '/test/wiki', } as Parameters[0] extends Promise ? T : never, ); vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([]); adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); }); it('should fallback to default routing on error', async () => { mockUtils.generateTiddlerFileInfo .mockImplementationOnce(() => { throw new Error('Test error'); }) .mockReturnValue({ filepath: '/test/wiki/tiddlers/test.tid', type: 'application/x-tiddler', hasMetaFile: false, }); const tiddler: Tiddler = { fields: { title: 'TestTiddler', tags: [] }, } as unknown as Tiddler; const result = await adaptor.getTiddlerFileInfo(tiddler); expect(mockLogger.alert).toHaveBeenCalledWith( expect.stringContaining('Error in getTiddlerFileInfo'), expect.any(Error), ); expect(result).toBeTruthy(); // Should have tried twice: once failed, once fallback expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledTimes(2); }); }); describe('updateSubWikisCache', () => { it('should clear cache when workspaceID is empty', async () => { const wikiWithoutID = { getTiddlerText: vi.fn(() => ''), // Empty workspace ID tiddlerExists: vi.fn(() => false), addTiddler: vi.fn(), } as unknown as Wiki; adaptor = new FileSystemAdaptor({ wiki: wikiWithoutID, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); // Manually trigger cache update and wait for it await adaptor['updateSubWikisCache'](); expect(adaptor['subWikisWithTag']).toEqual([]); expect(adaptor['tagNameToSubWiki'].size).toBe(0); }); it('should clear cache when currentWorkspace is not found', async () => { vi.mocked(workspace.get).mockResolvedValue(undefined); adaptor = new FileSystemAdaptor({ wiki: mockWiki, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); // Manually trigger cache update and wait for it await adaptor['updateSubWikisCache'](); expect(adaptor['subWikisWithTag']).toEqual([]); expect(adaptor['tagNameToSubWiki'].size).toBe(0); }); it('should handle errors in updateSubWikisCache gracefully', async () => { vi.mocked(workspace.get).mockResolvedValue( { id: 'test-workspace', name: 'Test Workspace', wikiFolderLocation: '/test/wiki', } as Parameters[0] extends Promise ? T : never, ); vi.mocked(workspace.getWorkspacesAsList).mockRejectedValue(new Error('Database error')); const wikiWithID = { getTiddlerText: vi.fn((title) => { if (title === '$:/info/tidgi/workspaceID') return 'test-workspace'; return ''; }), tiddlerExists: vi.fn(() => false), addTiddler: vi.fn(), } as unknown as Wiki; adaptor = new FileSystemAdaptor({ wiki: wikiWithID, // @ts-expect-error - TiddlyWiki global boot: global.$tw.boot, }); // Manually trigger cache update to catch the error await adaptor['updateSubWikisCache'](); // The error should have been logged expect(mockLogger.alert).toHaveBeenCalledWith( expect.stringContaining('Failed to update sub-wikis cache'), expect.any(Error), ); }); }); });