Repository navigation
Add guarded undo for single habit skips - #681
Conversation
There was a problem hiding this comment.
Caution
Undo can overwrite a committed schedule edit when the skip receipt is captured from a stale tracked habit. The two-context SQLite reproduction confirms this data-integrity issue.
Reviewed changes Reviewed the complete PR diff, its persistence and agent execution paths, and the existing consumer contract.
- Persisted undo receipts: Records pre-skip schedule state and post-skip guards, restores a single skip, and creates a sync tombstone for its flexible log.
- Conditional persistence: Adds the receipt table, transactional locking, write-time timestamp checks, habit-ceiling checks, goal synchronization, and cache invalidation.
- REST and agent surfaces: Preserves the skip endpoint's 204 response, appends optional
skipIdand an undo route, and registers Astra/MCP undo through the existing execution policy. - Coverage and rollout: Adds handler, domain, validator, controller, MCP, OpenAPI, and conditional-write tests; documents API-first deployment and the follow-up UI ticket.
The existing focused suites passed 207 tests. An additional temporary SQLite regression test failed because undo replaced the newer due date with the stale baseline; its source is linked in the inline finding. No repository changes remain.
openai/gpt-6.1-sol | 𝕏
There was a problem hiding this comment.
No new blocking issues found.
Reviewed changes Reviewed 0b2ea91e, the changes since the prior Pullfrog review at 21398269, with the complete PR diff as context.
- Refreshed skip baselines: Reloaded unchanged tracked habit values under the mutation lock before validation and receipt capture, preventing undo from restoring a stale schedule.
- Preserved staged mutations: Retained pending edits when their original timestamp matches persisted state and rejected conflicting edits without saving or discarding them.
- Moved bulk reads under the lock: Loaded and refreshed selected habits before processing skips and computing due-date resolution.
- Added regression coverage: Verified preservation of committed schedule edits, valid staged changes, conflict rejection, current-schedule validation, and unrelated staged habit changes through actual SQLite execution.
All 94 focused tests passed: 83 application skip/undo/bulk tests and 11 persistence regression tests, with none skipped. The prior finding is fixed and settled. No full-suite rerun was performed in this review.
openai/gpt-6.1-sol | 𝕏

Skipping a habit now records the state needed to undo that single skip. Undo restores its due date, schedule anchor and completion state, removes only its flexible skip log, and refuses newer habit or log changes. Repeating a completed undo is a no-op.
Closes thomasluizon/orbit-tickets#1052
Approach
HabitSkipUndoand its EF migration persist the pre-skip schedule, created log ID and post-skip state fingerprint.SkipHabitCommandandUndoSkipHabitCommandshare the user transaction lock, goal synchronization and cache invalidation.HabitSkipUndoWriterconditionally updates the habit against its recorded timestamp, protecting edits that arrive after validation without changing ordinary habit writers.HabitsControllerkeeps the existing skip response at 204, adds optionalSkipHabitRequest.skipId, and exposesPOST /api/Habits/{id}/skip/{skipId}/undo. A client generates one UUID per skip, sends it asskipId, and keeps it for undo and retries. Missing receipts return 404; newer state returns 409.skip_id. Both Astra and MCP exposeundo_skip_habit; MCP uses the existing executor policy and audit bridge. The catalog and pending approval action key are registered.src/Orbit.Api/openapi.jsonis regenerated. Handler, domain, validator, conditional-write, controller, MCP and OpenAPI contract tests cover the behavior. Architecture artifacts were regenerated and remain uncommitted as instructed.Test evidence
dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~SkipHabit --verbosity quietpassed all 60 unchanged tests, includingSkipHabitToolTests.SuccessfulSkip_RecurringHabit_AdvancesDueDate, while the tool still bypassed the application command.SkipHabitCommand, then runningdotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~SuccessfulSkip_RecurringHabit_AdvancesDueDate --logger 'console;verbosity=normal'before the dispatch fix failed withReceivedCallsException: no matching command was sent. After the fix it passes, including in the full suite.HabitSkipUndoWriterTests, for both a matching row and a concurrent edit. No external service response fields or fixtures are introduced.dotnet build Orbit.slnx --verbosity quiet: succeeded with 0 errors.dotnet test --verbosity quiet: all 8,570 tests passed (32 analyzer, 656 domain, 4,908 application, 2,974 infrastructure), with no skips.git diff --exit-code -- src/Orbit.Api/openapi.jsonis clean after the build.env -u LANG dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~HabitSkipUndoWriterTests --logger 'console;verbosity=normal'passed both unchanged tests.env -u LANG dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~ReviewSkipBaselineTests --logger 'console;verbosity=normal'failed at the intended assertion: expected April 2, found April 3.env -u LANG dotnet build Orbit.slnx: zero errors.env -u LANG dotnet test: all 8,579 tests passed, none skipped.Assumptions
Manual steps
redesign/main, Keep Astra request prefixes stable so the provider cache serves them #1053 must rebaseline UIpackages/shared/src/types/__generated__/api.generated.tsandpackages/shared/src/types/__generated__/api.spec.commitagainst the carrying API commit. In the UI repository's GitHub Actions screen, runRedesign drift control(redesign-drift.yml) onredesign/main, or regenerate from that pinned spec with its existing orval configuration. The generated snapshot must contain the undo route and optional skip ID, and contract drift checks must pass.skipIdfor the Undo toast, add the new endpoint constant and callers, and handle the pending operation action keyundoSkipHabitin the UI approval copy. Deploy the API carrying the migration before those UI callers; a successful skip followed by undo verifies activation.None specific to this fix. No new configuration, secrets, migration, or backfill is required.