fix: guard note loading without active vault

This commit is contained in:
lucaronin
2026-04-24 02:16:49 +02:00
parent 3c92c200ab
commit dacf9bb19b
2 changed files with 51 additions and 1 deletions

View File

@@ -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')

View File

@@ -73,7 +73,10 @@ function requestNoteContent({ path }: Pick<NoteContentCacheEntry, 'path'>): 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<number>
@@ -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)