diff --git a/package.json b/package.json index a4d9ad68..f4390afd 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test", - "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/frontmatter-date-picker.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/wikilink-path-fix.spec.ts", + "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/frontmatter-date-picker.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/missing-string-metadata-open-note.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/wikilink-path-fix.spec.ts", "playwright:regression": "playwright test tests/smoke/", "playwright:integration": "playwright test --config playwright.integration.config.ts", "test:coverage": "node scripts/run-vitest-coverage.mjs", diff --git a/src/hooks/useVaultLoader.test.ts b/src/hooks/useVaultLoader.test.ts index 59632214..21c06828 100644 --- a/src/hooks/useVaultLoader.test.ts +++ b/src/hooks/useVaultLoader.test.ts @@ -395,6 +395,32 @@ describe('useVaultLoader', () => { expect(result.current.entries).toBe(entriesBefore) }) + + it('keeps entry metadata safe when a stale reload patch has undefined fields', async () => { + const { result } = await renderVaultLoader() + + act(() => { + result.current.updateEntry('/vault/note/hello.md', { + title: undefined, + filename: undefined, + aliases: undefined, + outgoingLinks: undefined, + relationships: undefined, + properties: undefined, + snippet: undefined, + } as unknown as Partial) + }) + + expect(result.current.entries[0]).toEqual(expect.objectContaining({ + title: 'hello', + filename: 'hello.md', + aliases: [], + outgoingLinks: [], + relationships: {}, + properties: {}, + snippet: '', + })) + }) }) describe('getNoteStatus', () => { @@ -754,6 +780,34 @@ describe('useVaultLoader', () => { title: 'Renamed', })) }) + + it('normalizes stale replacement metadata during reload-heavy note switching', async () => { + const { result } = await renderVaultLoader() + + act(() => { + result.current.replaceEntry('/vault/note/hello.md', { + path: '/vault/note/reloaded.md', + title: undefined, + filename: undefined, + aliases: undefined, + outgoingLinks: undefined, + relationships: undefined, + properties: undefined, + snippet: undefined, + } as unknown as Partial & { path: string }) + }) + + expect(result.current.entries[0]).toEqual(expect.objectContaining({ + path: '/vault/note/reloaded.md', + filename: 'reloaded.md', + title: 'reloaded', + aliases: [], + outgoingLinks: [], + relationships: {}, + properties: {}, + snippet: '', + })) + }) }) describe('reloadVault', () => { diff --git a/src/hooks/useVaultLoader.ts b/src/hooks/useVaultLoader.ts index 843ed099..8f8de1cd 100644 --- a/src/hooks/useVaultLoader.ts +++ b/src/hooks/useVaultLoader.ts @@ -16,6 +16,7 @@ import { reloadVaultEntries, tauriCall, } from './vaultLoaderCommands' +import { normalizeVaultEntry } from '../utils/vaultMetadataNormalization' function resetVaultState(options: { clearNewPaths: () => void @@ -273,19 +274,23 @@ function useEntryMutations( trackNew: (path: string) => void, ) { const addEntry = useCallback((entry: VaultEntry) => { + const normalizedEntry = normalizeVaultEntry(entry) setEntries((prev) => { - if (prev.some(e => e.path === entry.path)) return prev - return [entry, ...prev] + if (prev.some(e => e.path === normalizedEntry.path)) return prev + return [normalizedEntry, ...prev] }) - trackNew(entry.path) + trackNew(normalizedEntry.path) }, [setEntries, trackNew]) const updateEntry = useCallback((path: string, patch: Partial) => { setEntries((prev) => { let changed = false - const next = prev.map((e) => { - if (e.path === path) { changed = true; return { ...e, ...patch } } - return e + const next = prev.map((entry, index) => { + if (entry.path === path) { + changed = true + return normalizeVaultEntry({ ...entry, ...patch }, '', index) + } + return entry }) return changed ? next : prev }) @@ -302,7 +307,9 @@ function useEntryMutations( }, [setEntries]) const replaceEntry = useCallback((oldPath: string, patch: Partial & { path: string }) => { - setEntries((prev) => prev.map((e) => e.path === oldPath ? { ...e, ...patch } : e)) + setEntries((prev) => prev.map((entry, index) => + entry.path === oldPath ? normalizeVaultEntry({ ...entry, ...patch }, '', index) : entry, + )) }, [setEntries]) return { addEntry, updateEntry, removeEntry, removeEntries, replaceEntry } diff --git a/src/utils/vaultMetadataNormalization.ts b/src/utils/vaultMetadataNormalization.ts index ef636785..7f688c56 100644 --- a/src/utils/vaultMetadataNormalization.ts +++ b/src/utils/vaultMetadataNormalization.ts @@ -124,7 +124,7 @@ function fallbackViewName(filename: string, index: number): string { return stem && stem !== `view-${index + 1}` ? stem : `View ${index + 1}` } -function normalizeVaultEntry({ rawEntry, vaultPath, index }: EntryNormalizationArgs): VaultEntry { +function normalizeVaultEntryRecord({ rawEntry, vaultPath, index }: EntryNormalizationArgs): VaultEntry { const source = recordFrom(rawEntry) const filename = fallbackEntryFilename(source, index) const path = resolveEntryPath({ @@ -209,7 +209,11 @@ function normalizeViewFile({ rawView, index }: ViewNormalizationArgs): ViewFile export function normalizeVaultEntries(rawEntries: unknown, vaultPath: string): VaultEntry[] { if (!Array.isArray(rawEntries)) return [] - return rawEntries.map((rawEntry, index) => normalizeVaultEntry({ rawEntry, vaultPath, index })) + return rawEntries.map((rawEntry, index) => normalizeVaultEntry(rawEntry, vaultPath, index)) +} + +export function normalizeVaultEntry(rawEntry: unknown, vaultPath = '', index = 0): VaultEntry { + return normalizeVaultEntryRecord({ rawEntry, vaultPath, index }) } export function normalizeViewFiles(rawViews: unknown): ViewFile[] {