From dacf9bb19bf476fcaef8fcbb4882a9daf651da0c Mon Sep 17 00:00:00 2001 From: lucaronin Date: Fri, 24 Apr 2026 02:16:49 +0200 Subject: [PATCH] fix: guard note loading without active vault --- src/hooks/useTabManagement.test.ts | 31 ++++++++++++++++++++++++++++++ src/hooks/useTabManagement.ts | 21 +++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/hooks/useTabManagement.test.ts b/src/hooks/useTabManagement.test.ts index 4636ed76..61bf37d2 100644 --- a/src/hooks/useTabManagement.test.ts +++ b/src/hooks/useTabManagement.test.ts @@ -160,6 +160,19 @@ describe('useTabManagement (single-note model)', () => { ) warnSpy.mockRestore() }) + + it('returns to the empty state when no active vault is selected', async () => { + const { mockInvoke } = await import('../mock-tauri') + vi.mocked(mockInvoke).mockRejectedValueOnce(new Error('No active vault selected')) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const { result } = renderHook(() => useTabManagement()) + await selectNote(result, { path: '/vault/note/orphaned.md', title: 'Orphaned Note' }) + + expect(result.current.tabs).toEqual([]) + expect(result.current.activeTabPath).toBeNull() + warnSpy.mockRestore() + }) }) describe('handleReplaceActiveTab', () => { @@ -328,6 +341,24 @@ describe('useTabManagement (single-note model)', () => { await vi.waitFor(() => expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1)) }) + it('swallows no-active-vault prefetch failures and lets a later open recover', async () => { + const { mockInvoke } = await import('../mock-tauri') + vi.mocked(mockInvoke) + .mockRejectedValueOnce(new Error('No active vault selected')) + .mockResolvedValueOnce('# Recovered content') + + prefetchNoteContent('/vault/note/recovered.md') + await vi.waitFor(() => expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(1)) + await Promise.resolve() + await Promise.resolve() + + const { result } = renderHook(() => useTabManagement()) + await selectNote(result, { path: '/vault/note/recovered.md', title: 'Recovered' }) + + expect(result.current.tabs[0].content).toBe('# Recovered content') + expect(vi.mocked(mockInvoke)).toHaveBeenCalledTimes(2) + }) + it('serves refreshed cached content after a save replaces stale prefetched data', async () => { const mockInvoke = await prefetchResolvedContent('/vault/note/saved.md', '# Stale prefetched content') vi.mocked(mockInvoke).mockResolvedValue('# Persisted content') diff --git a/src/hooks/useTabManagement.ts b/src/hooks/useTabManagement.ts index 98c0c217..ab2aa959 100644 --- a/src/hooks/useTabManagement.ts +++ b/src/hooks/useTabManagement.ts @@ -73,7 +73,10 @@ function requestNoteContent({ path }: Pick): Note * Cache is short-lived: cleared on vault reload via clearPrefetchCache(). */ export function prefetchNoteContent(path: string): void { if (prefetchCache.has(path)) return - requestNoteContent({ path }) + void requestNoteContent({ path }).promise.catch((error) => { + if (isNoActiveVaultSelectedError(error)) return + console.warn('Failed to prefetch note content:', error) + }) } export function cacheNoteContent(path: string, content: string): void { @@ -189,6 +192,15 @@ function isMissingNotePathError(error: unknown): boolean { return /does not exist|not found|enoent/i.test(message) } +function isNoActiveVaultSelectedError(error: unknown): boolean { + const message = error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : String(error) + return /no active vault selected/i.test(message) +} + function shouldApplyLoadedEntry(options: { seq: number navSeqRef: React.MutableRefObject @@ -238,6 +250,13 @@ function handleEntryLoadFailure(options: { console.warn('Failed to load note content:', error) if (navSeqRef.current !== seq) return + if (isNoActiveVaultSelectedError(error)) { + clearPrefetchCache() + clearTabs(tabsRef, setTabs) + syncActiveTabPath(activeTabPathRef, setActiveTabPath, null) + failNoteOpenTrace(entry.path, 'missing-active-vault') + return + } if (isMissingNotePathError(error)) { clearTabs(tabsRef, setTabs) syncActiveTabPath(activeTabPathRef, setActiveTabPath, null)