From bd964fb2840fba15de5dfb5213a8d833779fda6c Mon Sep 17 00:00:00 2001 From: hugo Date: Mon, 16 Feb 2026 10:09:14 +0100 Subject: [PATCH] fix: repaired rebuild from filesystem --- src/main/engine/MetaEngine.ts | 25 +++++++++++-------- src/main/ipc/handlers.ts | 20 +++------------ src/renderer/App.tsx | 31 +++++++++++++++-------- tests/engine/MetaEngine.test.ts | 15 ++++++----- tests/ipc/handlers.test.ts | 44 +++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/src/main/engine/MetaEngine.ts b/src/main/engine/MetaEngine.ts index 6f19b16..3ad79af 100644 --- a/src/main/engine/MetaEngine.ts +++ b/src/main/engine/MetaEngine.ts @@ -397,16 +397,21 @@ export class MetaEngine extends EventEmitter { if (projectMetadataFileExists) { await this.loadProjectMetadata(); - // If project.json has a dataPath, sync it back to the database - if (this.projectMetadata?.dataPath !== undefined) { - const projectData = await this.fetchProjectFromDatabase(); - if (projectData && projectData.dataPath !== this.projectMetadata.dataPath) { - const db = getDatabase().getLocal(); - await db.update(projects) - .set({ dataPath: this.projectMetadata.dataPath || null }) - .where(eq(projects.id, this.currentProjectId)); - console.log(`[MetaEngine] Synced dataPath from project.json to database: ${this.projectMetadata.dataPath || '(default)'}`); - } + // Keep dataPath authoritative in database (selected folder path on create/open). + // If project.json has a stale dataPath, update project.json from database. + const projectData = await this.fetchProjectFromDatabase(); + if (!projectData) { + throw new Error(`Project not found in database: ${this.currentProjectId}`); + } + + const databaseDataPath = projectData.dataPath || undefined; + if (this.projectMetadata && this.projectMetadata.dataPath !== databaseDataPath) { + this.projectMetadata = { + ...this.projectMetadata, + dataPath: databaseDataPath, + }; + await this.saveProjectMetadata(); + console.log(`[MetaEngine] Synced dataPath from database to project.json: ${databaseDataPath || '(default)'}`); } } else { // No file exists, fetch project data from database and create file diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index fff0cf1..ec8f708 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -236,10 +236,7 @@ export function registerIpcHandlers(): void { const dataDir = projectEngine.getDataDir(project.id, project.dataPath); engine.setProjectContext(project.id, dataDir); } - // Fire and forget - don't await, let it run in background - engine.rebuildDatabaseFromFiles().catch(err => { - console.error('Post rebuild failed:', err); - }); + return engine.rebuildDatabaseFromFiles(); }); safeHandle('posts:search', async (_, query: string) => { @@ -305,10 +302,7 @@ export function registerIpcHandlers(): void { const dataDir = projectEngine.getDataDir(project.id, project.dataPath); engine.setProjectContext(project.id, dataDir); } - // Fire and forget - let it run as a background task - engine.reindexText().catch(err => { - console.error('Text reindex failed:', err); - }); + return engine.reindexText(); }); // ============ Media Handlers ============ @@ -481,18 +475,12 @@ export function registerIpcHandlers(): void { // This ensures all project data lives in the same location for backup engine.setProjectContext(project.id, dataDir, dataDir); } - // Fire and forget - don't await, let it run in background - engine.rebuildDatabaseFromFiles().catch(err => { - console.error('Media rebuild failed:', err); - }); + return engine.rebuildDatabaseFromFiles(); }); safeHandle('media:reindexText', async () => { const engine = getMediaEngine(); - // Fire and forget - don't await, let it run in background - engine.reindexText().catch(err => { - console.error('Media text reindex failed:', err); - }); + return engine.reindexText(); }); safeHandle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8635188..a368d24 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -242,20 +242,31 @@ const App: React.FC = () => { ); unsubscribers.push( - window.electronAPI?.on('menu:rebuildDatabase', () => { - // Fire and forget - the handlers return immediately now - window.electronAPI?.posts.rebuildFromFiles(); - window.electronAPI?.media.rebuildFromFiles(); - // Also regenerate missing thumbnails after media rebuild - window.electronAPI?.media.regenerateMissingThumbnails(); + window.electronAPI?.on('menu:rebuildDatabase', async () => { + try { + await Promise.all([ + window.electronAPI?.posts.rebuildFromFiles(), + window.electronAPI?.media.rebuildFromFiles(), + ]); + await window.electronAPI?.media.regenerateMissingThumbnails(); + } catch (error) { + console.error('Database rebuild failed:', error); + showToast.error('Database rebuild failed'); + } }) || (() => {}) ); unsubscribers.push( - window.electronAPI?.on('menu:reindexText', () => { - // Fire and forget - runs as background tasks - window.electronAPI?.posts.reindexText(); - window.electronAPI?.media.reindexText(); + window.electronAPI?.on('menu:reindexText', async () => { + try { + await Promise.all([ + window.electronAPI?.posts.reindexText(), + window.electronAPI?.media.reindexText(), + ]); + } catch (error) { + console.error('Text reindex failed:', error); + showToast.error('Text reindex failed'); + } }) || (() => {}) ); diff --git a/tests/engine/MetaEngine.test.ts b/tests/engine/MetaEngine.test.ts index 258e24c..aaffcd2 100644 --- a/tests/engine/MetaEngine.test.ts +++ b/tests/engine/MetaEngine.test.ts @@ -731,19 +731,19 @@ describe('MetaEngine', () => { expect(metaDir).toContain('/custom/data/path'); }); - it('should sync dataPath from project.json to database if different', async () => { + it('should sync dataPath from database to project.json if different', async () => { const metaDir = metaEngine.getMetaDir(); mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({ name: 'Project', - dataPath: '/custom/path/from/file', + dataPath: '/old/path/from/file', })); - // Database has different or missing dataPath + // Database has the currently selected (authoritative) path mockProject = { id: 'test-project', name: 'Project', description: null, - dataPath: null, + dataPath: '/new/path/from/database', slug: 'project', createdAt: new Date(), updatedAt: new Date(), @@ -752,8 +752,11 @@ describe('MetaEngine', () => { await metaEngine.syncOnStartup(); - // Should have synced (database update called) - expect(mockLocalDb.select).toHaveBeenCalled(); + const savedProjectJson = mockFiles.get(normalizePath(`${metaDir}/project.json`)); + expect(savedProjectJson).toBeDefined(); + const parsed = JSON.parse(savedProjectJson!); + expect(parsed.dataPath).toBe('/new/path/from/database'); + expect(mockLocalDb.update).not.toHaveBeenCalled(); }); }); }); diff --git a/tests/ipc/handlers.test.ts b/tests/ipc/handlers.test.ts index e358c71..ce1da8a 100644 --- a/tests/ipc/handlers.test.ts +++ b/tests/ipc/handlers.test.ts @@ -48,6 +48,7 @@ const mockPostEngine = { isSlugAvailable: vi.fn(), generateUniqueSlug: vi.fn(), rebuildDatabaseFromFiles: vi.fn(), + reindexText: vi.fn(), searchPosts: vi.fn(), getPostsFiltered: vi.fn(), getAvailableTags: vi.fn(), @@ -629,6 +630,28 @@ describe('IPC Handlers', () => { expect(result).toEqual(linkedPosts); }); }); + + describe('posts:rebuildFromFiles', () => { + it('should propagate rebuild errors to the caller', async () => { + const rebuildError = new Error('rebuild failed'); + mockPostEngine.rebuildDatabaseFromFiles.mockRejectedValue(rebuildError); + mockProjectEngine.getActiveProject.mockResolvedValue(null); + + await expect(invokeHandler('posts:rebuildFromFiles')).rejects.toThrow('rebuild failed'); + expect(mockPostEngine.rebuildDatabaseFromFiles).toHaveBeenCalled(); + }); + }); + + describe('posts:reindexText', () => { + it('should propagate reindex errors to the caller', async () => { + const reindexError = new Error('post reindex failed'); + mockPostEngine.reindexText.mockRejectedValue(reindexError); + mockProjectEngine.getActiveProject.mockResolvedValue(null); + + await expect(invokeHandler('posts:reindexText')).rejects.toThrow('post reindex failed'); + expect(mockPostEngine.reindexText).toHaveBeenCalled(); + }); + }); }); // ============ Media Handlers ============ @@ -790,6 +813,27 @@ describe('IPC Handlers', () => { expect(result).toEqual(thumbnailDataUrl); }); }); + + describe('media:rebuildFromFiles', () => { + it('should propagate rebuild errors to the caller', async () => { + const rebuildError = new Error('media rebuild failed'); + mockMediaEngine.rebuildDatabaseFromFiles.mockRejectedValue(rebuildError); + mockProjectEngine.getActiveProject.mockResolvedValue(null); + + await expect(invokeHandler('media:rebuildFromFiles')).rejects.toThrow('media rebuild failed'); + expect(mockMediaEngine.rebuildDatabaseFromFiles).toHaveBeenCalled(); + }); + }); + + describe('media:reindexText', () => { + it('should propagate reindex errors to the caller', async () => { + const reindexError = new Error('media reindex failed'); + mockMediaEngine.reindexText.mockRejectedValue(reindexError); + + await expect(invokeHandler('media:reindexText')).rejects.toThrow('media reindex failed'); + expect(mockMediaEngine.reindexText).toHaveBeenCalled(); + }); + }); }); // ============ Meta Handlers ============