From 533c9da4d0f0f65686d8c2fc1a293b061b2d27f9 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 5 Mar 2026 10:00:56 +0100 Subject: [PATCH] fix: trashed notes reappear after restart due to frontmatter key casing mismatch Frontend wrote `trashed` (lowercase) but Rust parser expected `Trashed` (title-case via serde rename). On restart, the lowercase key didn't match, defaulted to false, and trashed notes reappeared. - Frontend: use title-case keys (Trashed, Trashed at) matching vault convention - Rust: add serde aliases for lowercase keys (backward compat with already-written files) - Add regression tests for both title-case and lowercase parsing Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/vault/mod.rs | 31 +++++++++++++++++++++++++++++-- src/hooks/useEntryActions.test.ts | 8 ++++---- src/hooks/useEntryActions.ts | 8 ++++---- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/vault/mod.rs b/src-tauri/src/vault/mod.rs index d2135fac..2d6d925f 100644 --- a/src-tauri/src/vault/mod.rs +++ b/src-tauri/src/vault/mod.rs @@ -103,9 +103,9 @@ struct Frontmatter { cadence: Option, #[serde(rename = "Archived")] archived: Option, - #[serde(rename = "Trashed")] + #[serde(rename = "Trashed", alias = "trashed")] trashed: Option, - #[serde(rename = "Trashed at")] + #[serde(rename = "Trashed at", alias = "trashed_at")] trashed_at: Option, #[serde(rename = "Created at")] created_at: Option, @@ -1312,6 +1312,33 @@ Company: Acme Corp ); } + #[test] + fn test_parse_trashed_title_case() { + let dir = TempDir::new().unwrap(); + let content = "---\nTrashed: true\nTrashed at: \"2025-02-01\"\n---\n# Gone\n"; + let entry = parse_test_entry(&dir, "gone.md", content); + assert!(entry.trashed); + assert!(entry.trashed_at.is_some()); + } + + #[test] + fn test_parse_trashed_lowercase_alias() { + let dir = TempDir::new().unwrap(); + let content = "---\ntrashed: true\ntrashed_at: \"2025-02-01\"\n---\n# Gone\n"; + let entry = parse_test_entry(&dir, "gone.md", content); + assert!(entry.trashed, "lowercase 'trashed' must be parsed via alias"); + assert!(entry.trashed_at.is_some(), "lowercase 'trashed_at' must be parsed via alias"); + } + + #[test] + fn test_trashed_false_when_absent() { + let dir = TempDir::new().unwrap(); + let content = "---\nIs A: Note\n---\n# Active\n"; + let entry = parse_test_entry(&dir, "active.md", content); + assert!(!entry.trashed); + assert!(entry.trashed_at.is_none()); + } + // Frontmatter update/delete tests are in frontmatter.rs // save_image tests are in vault/image.rs // purge_trash tests are in vault/trash.rs diff --git a/src/hooks/useEntryActions.test.ts b/src/hooks/useEntryActions.test.ts index 3f0abc13..da4b863e 100644 --- a/src/hooks/useEntryActions.test.ts +++ b/src/hooks/useEntryActions.test.ts @@ -66,8 +66,8 @@ describe('useEntryActions', () => { await result.current.handleTrashNote('/vault/note/test.md') }) - expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed', true) - expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed_at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/)) + expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed', true) + expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed at', expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/)) expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { trashed: true, trashedAt: expect.any(Number), @@ -84,8 +84,8 @@ describe('useEntryActions', () => { await result.current.handleRestoreNote('/vault/note/test.md') }) - expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'trashed', false) - expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'trashed_at') + expect(handleUpdateFrontmatter).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed', false) + expect(handleDeleteProperty).toHaveBeenCalledWith('/vault/note/test.md', 'Trashed at') expect(updateEntry).toHaveBeenCalledWith('/vault/note/test.md', { trashed: false, trashedAt: null, diff --git a/src/hooks/useEntryActions.ts b/src/hooks/useEntryActions.ts index 9bf63611..b91572d4 100644 --- a/src/hooks/useEntryActions.ts +++ b/src/hooks/useEntryActions.ts @@ -19,15 +19,15 @@ export function useEntryActions({ }: EntryActionsConfig) { const handleTrashNote = useCallback(async (path: string) => { const now = new Date().toISOString().slice(0, 10) - await handleUpdateFrontmatter(path, 'trashed', true) - await handleUpdateFrontmatter(path, 'trashed_at', now) + await handleUpdateFrontmatter(path, 'Trashed', true) + await handleUpdateFrontmatter(path, 'Trashed at', now) updateEntry(path, { trashed: true, trashedAt: Date.now() / 1000 }) setToastMessage('Note moved to trash') }, [handleUpdateFrontmatter, updateEntry, setToastMessage]) const handleRestoreNote = useCallback(async (path: string) => { - await handleUpdateFrontmatter(path, 'trashed', false) - await handleDeleteProperty(path, 'trashed_at') + await handleUpdateFrontmatter(path, 'Trashed', false) + await handleDeleteProperty(path, 'Trashed at') updateEntry(path, { trashed: false, trashedAt: null }) setToastMessage('Note restored from trash') }, [handleUpdateFrontmatter, handleDeleteProperty, updateEntry, setToastMessage])