Skip to content

fix(health): count only unique asleep intervals in daily summaries - #40

Merged
johnnyhuy merged 1 commit into
mainfrom
agent/issue-38-sleep-duration
Nov 10, 2026
Merged

johnnyhuy merged 1 commit into
mainfrom
agent/issue-38-sleep-duration

Conversation

@johnnyhuy

@johnnyhuy johnnyhuy commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Daily Health export summaries now count only recognised asleep categories and merge overlapping intervals within each start-date day bucket. Awake, in-bed, unknown categories and invalid intervals no longer inflate the duration, and CSV/JSON exports carry the corrected result.

Completes the sleep-duration correctness slice. Part of #6. Closes #38. The parent epic remains open.

Verification

  • mise run lint failed with 503 violations, including 11 errors. An untouched archive of main at be32d20 reproduced the same 503 violations and 11 errors with SwiftLint 0.62.2. Existing failures are tracked in Restore the repository-wide SwiftLint gate #39.
  • Strict lint on the two changed Swift files passed with zero violations using SCRIPT_INPUT_FILE_COUNT=2 SCRIPT_INPUT_FILE_0="$PWD/Sources/BetterFit/Models/DailyHealthSummary.swift" SCRIPT_INPUT_FILE_1="$PWD/Tests/BetterFitTests/SleepSummaryTests.swift" mise exec -- swiftlint lint --strict --config .swiftlint.yml --use-script-input-files.
  • mise run build passed.
  • mise run test passed with 23 pipeline checks, 151 XCTest tests, and 17 Swift Testing tests in two suites. Two new parameterised tests cover 21 category inputs.
  • Linux CI passed mise run build and mise run test on the PR head, including the XML-to-CSV/JSON regression test. CI proof.
  • CI mise run ios:build:ci passed with BUILD SUCCEEDED. iOS build output.
  • CI mise run ios:coverage passed, including the iOS UI test suite and coverage report. UI test output.
  • CI mise run ios:e2e passed. All ten PR checks are green, including build, unit tests, UI coverage, mobile e2e and security checks. Mobile e2e output.
  • git diff --check passed.

Proof

Unit-test transcript capture succeeded. No UI changed, so no simulator screenshot or UI-test capture was attempted. Scratch logs remain under .tmp/proof/ and are not committed.

The mixed XML fixture contains a nine-hour in-bed interval, an awake interval, three asleep stages and an overlapping record from a second device. It contains four hours asleep. Before the fix, the regression test exposed a 15-hour total.

Test exportsFourHoursAsleepFromMixedOverlappingXMLRecords() recorded an issue at SleepSummaryTests.swift:115:9: Expectation failed: (summaries.first?.sleepDuration → 54000.0) == (14400 → 14400.0)
Suite SleepSummaryTests failed after 0.006 seconds with 18 issues.

After the fix, the parser-to-summary-to-export test asserts sleepDuration == 14400, the CSV row 2026-04-19T00:00:00Z,,,,,,,,14400, and JSON "sleepDuration" : 14400, then decodes the JSON back to the same summaries.

The following seven new tests passed. Category tests include legacy asleep, unspecified, core, deep, REM and numeric HealthKit values, plus rejected awake, in-bed, unknown and non-finite values.

summary: 23 passed, 0 failed
	 Executed 151 tests, with 0 failures (0 unexpected) in 2.333 (2.359) seconds
	 Executed 151 tests, with 0 failures (0 unexpected) in 2.333 (2.361) seconds
Test preservesGapsBetweenSeparateSleepPeriods() passed after 0.004 seconds.
Test mergesUnsortedOverlappingNestedAndDuplicateIntervals() passed after 0.004 seconds.
Test keepsStartDayAllocationInTheSuppliedCalendar() passed after 0.004 seconds.
Test countsRecognisedAsleepCategoriesFromXML(_:) with 9 test cases passed after 0.005 seconds.
Test ignoresInvalidAndIncompleteIntervals() passed after 0.004 seconds.
Test excludesNonAsleepAndUnknownCategoriesFromXML(_:) with 12 test cases passed after 0.005 seconds.
Test exportsFourHoursAsleepFromMixedOverlappingXMLRecords() passed after 0.005 seconds.
Suite SleepSummaryTests passed after 0.006 seconds.
Test run with 17 tests in 2 suites passed after 0.006 seconds.

Changed-file lint also passed.

Linting 'DailyHealthSummary.swift' (2/2)
Linting 'SleepSummaryTests.swift' (1/2)
Done linting! Found 0 violations, 0 serious in 2 files.

The iOS CI capture also confirms the app still builds and its existing UI suite passes.

mise run ios:build:ci
** BUILD SUCCEEDED **
mise run ios:coverage
Executed 27 tests, with 0 failures (0 unexpected)
** TEST SUCCEEDED **
BetterFitApp.app 40.33% (9625/23863)

Review notes

Mobile e2e completed successfully with three tests passing first time and one passing on retry. The initial failure was app did not come to foreground in e2e/tests/helpers.ts:60, before the active-workout test reached its behaviour assertions. The successful retry and original error are both visible in the linked job output.

The existing allocation by start date is preserved, including for intervals crossing midnight. Unioning asleep intervals does not prioritise devices or subtract conflicting awake samples. Cross-midnight splitting, source prioritisation, live HealthKit ingestion and UI remain separate slices. Public model fields and serialisation shapes are unchanged.

Category meanings were checked against Apple's sleep-analysis documentation, and numeric values against the installed HealthKit SDK header. The parser and aggregation remain Foundation only.

Filter sleep analysis to recognised asleep categories and merge overlapping intervals within each start-date day bucket. Cover XML parsing, category raw values, invalid intervals and CSV/JSON export with regression tests.

Co-authored-by: OpenAI Codex <noreply@openai.com>
@johnnyhuy
johnnyhuy merged commit 479d230 into main Nov 10, 2026
10 checks passed
@johnnyhuy
johnnyhuy deleted the agent/issue-38-sleep-duration branch November 10, 2026 19:55
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.

Count only asleep time in daily Health export summaries

1 participant