fix: normalize stale vault entry metadata

This commit is contained in:
lucaronin
2026-04-29 04:33:18 +02:00
parent f4790d5763
commit a7c480af62
4 changed files with 75 additions and 10 deletions

View File

@@ -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",

View File

@@ -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<VaultEntry>)
})
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<VaultEntry> & { 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', () => {

View File

@@ -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<VaultEntry>) => {
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<VaultEntry> & { 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 }

View File

@@ -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[] {