Skip to content

Fix habit edit gating, end dates, and reminder updates - #736

Merged
thomasluizon merged 4 commits into
mainfrom
thomasluizon/ticket-1514-habit-edit-gate
Oct 18, 2026
Merged

thomasluizon merged 4 commits into
mainfrom
thomasluizon/ticket-1514-habit-edit-gate

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Closes thomasluizon/orbit-tickets#1514

Free accounts can save ordinary habit edits even when shipped clients send slipAlertEnabled: false. Enabling slip alerts still requires Pro. Root habits retain their requested end date, unchanged reminder schedules preserve sent rows, and clearing an end time validates the resulting time range.

Changes

  • src/Orbit.Application/Habits/Commands/UpdateHabitCommand.cs: check slip alert activation after loading the owned habit under the existing update lock. Compare due time, reminder enablement, and effective reminder rule sets before and after the domain update. This accounts for legacy clock fields, relative rules, ordering, and duplicate offsets across fields before deciding to clear today's sent rows.
  • src/Orbit.Application/Habits/Commands/CreateHabitCommand.cs: pass EndDate into root creation, matching the child path and retaining domain validation.
  • src/Orbit.Domain/Entities/Habit.cs: validate the same replacement time values that the update assigns.
  • Regression coverage stays in the existing application handler and domain entity test files. The request and response contracts stay compatible with shipped clients.

Test evidence

Before changing tests or implementation, these commands passed with all four defects present:

dotnet test tests/Orbit.Application.Tests --filter 'FullyQualifiedName~UpdateHabitCommandHandlerTests|FullyQualifiedName~CreateHabitCommandHandlerTests' --verbosity quiet
dotnet test tests/Orbit.Domain.Tests --filter FullyQualifiedName~HabitTests --verbosity quiet

The application run passed 45 tests, including Handle_ValidCommand_UpdatesAndSaves, Handle_ValidCommand_CreatesHabitAndSaves, Handle_DueTimeChanged_ClearsSentReminder, and Handle_TimedHabitScheduledReminderEdit_SavesFoldedClockReminder. The domain run passed 157 tests, including Update_AllOptionalFields_Applied and Update_DueEndTimeBeforeDueTime_ReturnsFailure. Those cases did not exercise the failing payloads.

With the strengthened root-create test and new regression theories, before any production fix:

dotnet test tests/Orbit.Application.Tests --filter 'FullyQualifiedName~UpdateHabitCommandHandlerTests|FullyQualifiedName~CreateHabitCommandHandlerTests' --verbosity quiet
dotnet test tests/Orbit.Domain.Tests --filter FullyQualifiedName~HabitTests --verbosity normal

The application run failed 9 of 63 cases; the domain run failed 1 of 162. Each failure was observed before its fix:

Defect Regression and red observation Green observation
Slip alert gate Handle_SlipAlerts_OnlyEnablingRequiresPro: free edits sending false, disabling alerts, and leaving true unchanged failed the success assertion. All 7 cases pass, including denied activation without Pro, successful activation with Pro, and no gate call for unchanged, false, or omitted values.
Root end date Strengthened Handle_ValidCommand_CreatesHabitAndSaves: no added root matched the requested end date. The root passed to AddAsync carries the requested end date.
Reminder reset Handle_ReminderSchedule_ClearsSentRowsOnlyWhenChanged: unchanged, reordered, and equivalent clock payloads incorrectly removed a sent row; clearing due time failed to remove it. Handle_UntimedReminderSchedule_ClearsSentRowsOnlyWhenChanged failed to clear after a clock change. All final reminder cases pass, covering changed due time, clearing time, reminder enablement, offsets, relative rules, and clocks. Yesterday's rows and another habit's rows remain intact.
Time validation Update_TimeRange_ValidatesResultingValues: moving the start from 09:00 to 11:00 while clearing the old 10:00 end failed the success assertion. All 5 cases pass, including clearing both times, setting a valid end, and rejecting an invalid range without mutation.

After the first reminder fix, the existing 9 reminder theory cases passed. Adding the offset-equivalent case exposed an unnecessary reset when two fields described the same reminder. This command failed 1 of 10 cases with an unexpected Remove call. After comparing effective rules, the same theory passes within the final 37 update-handler cases:

dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~Handle_ReminderSchedule_ClearsSentRowsOnlyWhenChanged --logger 'console;verbosity=normal'

Focused compilation and green checks before the corresponding commits:

dotnet test tests/Orbit.Domain.Tests --filter FullyQualifiedName~HabitTests --verbosity quiet
dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~CreateHabitCommandHandlerTests --verbosity quiet
dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~UpdateHabitCommandHandlerTests --verbosity quiet

Results: 162 domain cases, 27 create-handler cases, and 37 final update-handler cases passed.

Final verification:

dotnet build Orbit.slnx --verbosity quiet
dotnet test tests/Orbit.Domain.Tests --verbosity quiet
dotnet test tests/Orbit.Application.Tests --verbosity quiet
dotnet test tests/Orbit.Infrastructure.Tests --verbosity quiet
dotnet test --no-build --verbosity quiet
  • Solution build: exit 0, zero errors, 19 warnings in untouched files and package dependencies. Generated OpenAPI documents have no diff.
  • Domain suite: exit 0, 671 passed.
  • Application suite: exit 0, 5,485 passed.
  • Infrastructure suite: exit 0, 3,357 passed.
  • Full solution test run: exit 0, 9,545 passed across all four projects, including 32 analyzer tests.
  • Final focused regressions: 20 application cases and 5 domain cases passed after the final build.
  • Git hooks passed on each commit. Diff whitespace, dash, timeless prose, suppression, and added-comment checks passed.

Assumptions

  • Preserve the existing replacement semantics for DueTime and DueEndTime: null clears each time. Rejected treating null as keep, because the domain update assigns null.
  • Compare reminder rules by their effective values, ignoring order and duplicate offsets across fields. Rejected comparing raw request lists, because the scheduler combines and deduplicates those rules.

Manual steps

  • After merge and the owner's explicit release approval, use GitHub Actions > Release API (.github/workflows/release.yml) in thomasluizon/orbit-api. Dispatch from main with environment=production and branch=main. The workflow's Verify API health and live commit step must succeed for the released commit; verify an ordinary free-account edit succeeds with slipAlertEnabled: false. Production release was left for the owner as required by the ticket.

@sonarqubecloud

Copy link
Copy Markdown

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes across all six changed files, including domain guards, transaction boundaries, reminder scheduling, and the sibling client's request builders; no High or Critical regressions found.

  • Slip-alert activation gate: Requires Pro only when enabling previously disabled alerts, allowing ordinary free-account edits and alert disablement.
  • Root habit end dates: Passes the requested end date into root creation and retains domain validation.
  • Reminder deduplication: Preserves sent rows for unchanged effective schedules while detecting due-time, enablement, offset, and clock-rule changes across stored formats.
  • Replacement time validation: Validates the time values actually assigned, allowing end-time clearing without comparing against the old end time.
  • Verification: Passed 64 create/update handler cases, 162 habit domain cases, and 84 reminder scheduler cases. Full solution tests were not rerun during this review.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@thomasluizon
thomasluizon merged commit 7472203 into main Oct 18, 2026
28 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-1514-habit-edit-gate branch October 18, 2026 05:39
thomasluizon added a commit that referenced this pull request Oct 18, 2026
* fix: pin analyzer test Pkcs dependency to patched version (#728)

(cherry picked from commit 61b0090)

* fix: shard pull request domain mutation testing (#729)

(cherry picked from commit bcfd81e)

* Promote the API redesign with production compatibility checks (#723)

* Bring the API redesign tree onto main for #1413

* Preserve calendar connection detection for shipped clients

* Prove API and migrated schema compatibility with shipped clients

* Trim whitespace in the archived main schema proof

* Require the production health response to report the released commit

* Bind initial approval previews to stored pending operations

* fix: return the bulk skip refresh outcome and split the undo skip query

SonarCloud failed the quality gate on two reliability findings in code this pull request brings to main:
the bulk skip conflict check read a flag only a lambda wrote (S2583), and the undo skip check loaded
Tags and Goals in one query (S8733). The transaction now returns whether every habit refreshed, and the
undo skip check splits its query. A new case covers the concurrent change conflict; it failed with the
refresh outcome forced to true and passes with the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5526c9b)

* Add per-account feature flag targeting and admin controls (#724)

* feat: resolve feature flags per account

* feat: administer account feature flags through MCP and REST

* test: exclude feature flag targets from rate limit fixture

* fix: reconcile account flag tests and migration metadata with redesign

(cherry picked from commit 1b886bd)

* Add header API versioning for v1 and v2 (#733)

* Add header API versioning with v1 compatibility

* Verify versioned OpenAPI and browser header compatibility

* Preserve skip undo path contract with optional version header

(cherry picked from commit ac8b34a)

* fix: expose the config response in OpenAPI (#734)

(cherry picked from commit a9d8dd5)

* Route production redesign accounts through Cloudflare (#735)

* Add cookie-based routing for the production redesign web service

* Document web routing rollout, permissions, and rollback checks

* Fix web routing coverage and disable infra install scripts

* chore: regenerate the v2 OpenAPI document on main

orbit-api#733 added openapi_v2.json and orbit-api#734 typed the /api/config
response in parallel, so main carries a v2 document without the typed config
schema and the Build check's stale-document step fails on every merge with
main. dotnet build src/Orbit.Api regenerates it; openapi.json is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9c64770)

* Fix habit edit gating, end dates, and reminder updates (#736)

* fix: validate habit updates with the resulting time range

* fix: preserve end dates when creating root habits

* fix: gate slip alert activation and preserve unchanged reminders

* fix: compare effective reminder rules across stored formats

(cherry picked from commit 7472203)

* Reduce mutation and guard CI jobs (#737)

* Select mutation shards from changed projects

* Consolidate guards and filter artifact generation

* Skip project documentation in mutation selection

* Validate mutation base refs and restrict Git PATH

* Read mutation shard changes from standard input

(cherry picked from commit b802f9e)

* test: strengthen habit and goal ownership coverage (#739)

(cherry picked from commit 0e32628)

* test: verify checkout session user metadata (#740)

(cherry picked from commit da2f53a)

* fix: protect OAuth code delivery with Turnstile (#741)

(cherry picked from commit 1149606)

* fix: enforce chat interaction scope before loading context (#743)

(cherry picked from commit 98faf4e)

* fix: erase accountability pairs during account deletion and reset (#745)

* fix: erase accountability pairs during account deletion and reset

* test: preserve migration compatibility checks after accountability cleanup

(cherry picked from commit a9e5fb9)

* Fix goal sync dates and deadline reminder counts (#747)

* fix: compare streak sync dates in the user timezone

* fix: show actual days remaining in goal deadline reminders

* test: configure timezone for streak scheduler fixtures

(cherry picked from commit 0e63842)

* Manage staging landing routing in Terraform (#742)

(cherry picked from commit abdd738)

* Restrict Web Push endpoints to supported push services (#1503) (#751)

Web Push subscriptions accept only HTTPS endpoints on the FCM, Mozilla, Apple and WNS push hosts, and the push HTTP client follows no redirects. Replaces orbit-api#748 as one commit so no commit carries the high-entropy sample fixture.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d9f847a)

* Erase account uploads before permanent deletion (#1507) (#750)

* fix: erase uploaded objects before completing account deletion

* fix: refuse upload reads after permanent account deletion

* fix: grant uploads bucket deletion permissions and verify provider wiring

* test: align storage startup checks with erasure requirements

* fix: retire deleted Supabase upload storage

* test: configure upload erasure providers in deletion fixtures

Share the existing object-store fake across account deletion and scheduler fixtures after merging main. Preserve accountability assertions and production provider requirements for thomasluizon/orbit-tickets#1507.

(cherry picked from commit fa33a94)

* fix: limit anonymous auth and waitlist by email and source IP (ORB-108) (#749)

* fix: enforce source IP ceilings alongside anonymous email limits

* test: cover both rate limit dimensions before bot verification

* fix: isolate BFF email rate limits by verified client IP

* fix: generate and persist BFF forwarding secrets

(cherry picked from commit de0f42f)

* Limit access tokens to fifteen minutes (#738)

* fix(auth): limit access tokens to fifteen minutes

* fix(auth): align managed JWT lifetime and prove logout rejection

(cherry picked from commit ade7016)

* fix(auth): equalize account provisioning work (ORB-96) (#744)

* test(auth): expose registration dependent sign-in work

* fix(auth): perform uniform registration work before sign-in responses

* test(auth): cover expired codes and tracked upsert conflicts

* fix(auth): isolate signup retries and restore provisioning guards

* fix(auth): recover signup work after dispatch failures

* fix(auth): roll back failed signup referrals before retry

(cherry picked from commit 1bd0d26)

* fix: expose redesign controllers in both API versions

Apply the carried version declarations to the Notion and proactive controllers and regenerate the v2 contract. Refs thomasluizon/orbit-tickets#746.

* fix: honor account flags in proactive engine

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant