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 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-05 10:00:56 +01:00
parent e1afaaa5b6
commit 533c9da4d0
3 changed files with 37 additions and 10 deletions

View File

@@ -103,9 +103,9 @@ struct Frontmatter {
cadence: Option<String>,
#[serde(rename = "Archived")]
archived: Option<bool>,
#[serde(rename = "Trashed")]
#[serde(rename = "Trashed", alias = "trashed")]
trashed: Option<bool>,
#[serde(rename = "Trashed at")]
#[serde(rename = "Trashed at", alias = "trashed_at")]
trashed_at: Option<String>,
#[serde(rename = "Created at")]
created_at: Option<String>,
@@ -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

View File

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

View File

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