fix(tasks): refuse a due or start date the calendar does not have - #1951
Merged
Conversation
TaskCreateSchema and TaskUpdateSchema validate dueDate and startDate with a
bare \d{4}-\d{2}-\d{2}, so 2026-02-30, 2025-02-29 and 2026-13-01 parse
clean and reach the tasks row. completedAt is a z.string().datetime() and zod
already refuses the impossible instant, which is the asymmetry these cases
record. 2024-02-29 is here to hold the line the other way: a leap day is a
real date and must keep parsing.
Fourteen of the sixteen cases fail on this commit.
Part of #1923.
CalendarDateSchema replaces the shape regex on dueDate and startDate in TaskCreateSchema and TaskUpdateSchema. It keeps the regex as a first pass and then rebuilds the date from its parts, so a value that rolls over to another day is refused. The IPC boundary already turns a ZodError into `Validation failed: dueDate: <message>`, and the task mutation hooks already read that through extractErrorMessage, so the refusal reaches the user as a toast instead of a row nobody asked for. The round trip uses the local constructor and the local getters, the pair localMidnight already uses for the done date in #1922. new Date('2026-01-01') parses as UTC, so reading getDate() off it answers 31 December anywhere west of UTC and refuses a real date; the test pins that in four zones plus two whose clocks skip local midnight. taskPatchSchema gets the same schema on due, due_date and start_date. The agent's writes go handles.tasks.create -> createDesktopTasksDomain, which never touches TaskCreateSchema, so the MCP tool schema is the only gate on that path. The isoDateSchema the journal and calendar read tools share is left alone. Validate on write, tolerate on read. Nothing parses a task row on the way out: the IPC responses, the RPC Task and the calendar projection items are all plain interfaces, and the query layer compares dueDate as a SQLite string. A seeded row holding 2026-02-30 and 2025-02-29 is read back verbatim, listed, sorted, counted and duplicated. TaskSyncPayloadSchema stays deliberately loose, because a strict date there would make a receiving device reject a peer's old row forever. The renderer keeps rolling such a value onto a real day rather than throwing. Closes #1923.
…ble date The page already covered a done date the calendar does not have. Due and start dates now behave the same way, and the outcome is different enough to write down: the import stops, the checkbox stays a checkbox, the line is left as the user wrote it, and the message names the refused date. Part of #1923.
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
h4yfans
marked this pull request as ready for review
September 2, 2026 16:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1923.
dueDateandstartDatewere validated by a bare/^\d{4}-\d{2}-\d{2}$/, so2026-02-30,2025-02-29and2026-13-01parsed clean and landed in thetasksrow.completedAtis az.string().datetime()and zod already refused the impossible instant, which is the asymmetry the issue names.packages/contracts/src/calendar-date.tsholds oneCalendarDateSchema. It keeps the regex as a first pass, then rebuilds the date from its parts and checks the parts survive.TaskCreateSchemaandTaskUpdateSchemause it on both fields. The round trip goes through the local constructor and the local getters, the same pairlocalMidnightuses for the done date in #1922;new Date('2026-01-01')parses as UTC, so readinggetDate()off it answers 31 December anywhere west of UTC and refuses a real date.taskPatchSchemain the Vault MCP tool schemas gets the same schema ondue,due_dateandstart_date. The agent's writes runhandles.tasks.createintocreateDesktopTasksDomain, which never touchesTaskCreateSchema, so that tool schema is the only gate on the path. TheisoDateSchemathe journal and calendar read tools share is left alone.Validate on write, tolerate on read. Nothing parses a task row on the way out: the IPC responses, the RPC
Taskand the calendar projection items are plain interfaces, and the query layer comparesdueDateas a SQLite string.TaskSyncPayloadSchemastays deliberately loose, because a strict date there would make a receiving device reject a peer's older row forever, on a value the user cannot see to fix.Release note
An Obsidian task line carrying a date the calendar does not have, such as
📅 2026-02-30or📅 2025-02-29, no longer imports as a task with a due date that never arrives. The line is left exactly as written and a message names the refused date.Test plan
vitest --project shared packages/contracts/src/— 52 files, 1724 tests pass.tasks-api.test.tscovers2026-02-30,2025-02-29,2026-13-01and2024-02-29on create and update;calendar-date.test.tspins the year boundary and the leap day inPacific/Pago_Pago,Pacific/Kiritimati,UTCandAsia/Kolkata, and a day whose local midnight does not exist inAmerica/SantiagoandAmerica/Havana;sync-payloads.test.tspins that a peer's stored impossible date still parses.vitest --project main src/main/agent/mcp— 15 files, 124 tests pass. Revertingschemas.tstoorigin/mainand rerunning turns the new case red withexpected true to be false, so the MCP guard is load-bearing rather than duplicated.vitest --project main src/main/database/queries/tasks.test.ts— a seeded row holdingdueDate: '2026-02-30'andstartDate: '2025-02-29'reads back verbatim throughgetTaskById, appears inlistTasks, sorts correctly undersortBy: 'dueDate', is found bygetTasksByDueDate, counts ingetTaskStats, survivesduplicateTask, and does not throw fromgetOverdueTasks.vitest --project renderer src/renderer/src/lib/task-utils— 4 files, 423 tests pass, including new cases pinning thatparseDueDaterolls a stored impossible date onto a real day instead of throwing or returningInvalid Date.pnpm lint,pnpm typecheck,pnpm --filter @memry/desktop typecheck:test,pnpm check:architecture,pnpm check:contracts,pnpm ipc:generate && pnpm ipc:check,pnpm --filter @memry/desktop i18n:check,git diff --checkall pass.pnpm docs:impact --base origin/main --strictandpnpm docs:buildpass.npx react-doctorreports only pre-existing findings underapps/mobile, none in files this branch touches.apps/desktop/tests/e2e/obsidian-tasks-import.e2e.tsneeded no edit; it uses no impossible dates.