Fix un-completing tasks and field clears; run unit tests in CI - #101
Merged
Conversation
Restoring a task marked done in error failed with "List is empty":
TaskUpdate's fields all default to null and the Supabase client
serializes with encodeDefaults=false, so markUndone's
TaskUpdate(completedAt = null) encoded as an empty PATCH body that
matched no rows. The same omission silently turned "clear owner/notes/
due date" in the edit sheet into no-ops.
Task PATCH bodies are now built as maps (the pattern archiveTag already
used), where null entries encode as explicit JSON nulls: markUndone
sends {"completed_at": null}, and the edit-sheet save always writes its
eight editable columns while never touching completion/archival/reminded
state. Payload builders are extracted and pinned by TaskPayloadTest,
including a test documenting the serialization behaviour behind the bug.
New unit tests also cover chore status thresholds, cadence pressure,
countdown text, and task urgency/priority parsing. A "Unit tests" job in
build.yml now runs testDebugUnitTest on every PR; previously the one
existing test file was never executed by CI.
Recorded as LESSONS.md #33.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ2FuLCyKuhmXsPMV5RRes
The wrapper script was committed without the executable bit, so the new Unit tests job failed with "Permission denied" before Gradle started. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ2FuLCyKuhmXsPMV5RRes
The wrapper's eval quoted the optional option variables, so when unset
they expanded to empty-string arguments and java tried to load "" as the
main class ("Could not find or load main class" with a blank name) -- the
Unit tests job failed before Gradle started. Expanding them unquoted
contributes zero words when unset. Verified locally: ./gradlew --version
now boots Gradle 8.13. Recorded as LESSONS.md #34.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ2FuLCyKuhmXsPMV5RRes
The previous commit swept in wrapper-verification run state; the root .gitignore lacked a .gradle/ entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ2FuLCyKuhmXsPMV5RRes
The .gradle/ append landed on the unterminated .claude line, producing one broken ".claude.gradle/" pattern; restore both entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ2FuLCyKuhmXsPMV5RRes
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.
Fixes the "list is empty" error when restoring a task marked done in error, and wires unit tests into the pipeline.
The bug
TaskUpdate's fields all default to null, and the Supabase client serializes withencodeDefaults = false, so any field equal to its default is omitted from the request body. Two consequences:markUndonesentTaskUpdate(completedAt = null), which encoded as an empty PATCH body — it matched no rows anddecodeSinglefailed with "List is empty". (Marking done worked because the timestamp is non-null.)Chores were unaffected:
archiveTagalready builds its payload as a map, which is why unarchiving chores worked while un-completing tasks didn't.The fix
Task PATCH bodies are now built as
Map<String, String?>payloads (map entries have no defaults, so nulls are sent as explicit JSON nulls):markUndonesends{"completed_at": null};markDonesends the timestamp.editTaskPayload) always writes its eight editable columns, null meaning clear, and deliberately never touchescompleted_at/archived_at/remindedso an edit can't clobber completion state.markRemindedis unchanged (non-null value, always encoded).Recorded as LESSONS.md #33.
Tests + pipeline
build.ymlruns./gradlew testDebugUnitTeston every PR (uploads reports on failure). The repo had one JVM test file that CI never executed.TaskPayloadTestpins the fix: every editable column present, cleared fields as explicit nulls, completion state untouched, plus a test documenting the serialization behaviour that caused the bug (TaskUpdate(completedAt = null)→{}).ChoreModelTest: status thresholds (interval and category chores), cadence-pressure fractions and clamping, countdown text, distant detection.TaskModelTest: due date/period → urgency buckets, garbage-date safety, priority parsing.Checks
changelog/unreleased/task-clear-fix.json, patch bump).python3 a11y_check.pyclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01YZ2FuLCyKuhmXsPMV5RRes
Generated by Claude Code