Android: unblock testFullDebugUnitTest — implement DeviceRegistryDao.setModel in the three fake DAOs - #725
Closed
pipiche38 wants to merge 1 commit into
Closed
Conversation
… DAOs (unblock testFullDebugUnitTest) ryanbr#716 added `suspend fun setModel(id, model)` to the DeviceRegistryDao interface (and the production Room DAO + Swift twin) but did not update the three hand-written test doubles that implement the interface. They became non-abstract classes missing an abstract member, so the Full-flavor test source set no longer COMPILED — and because Gradle compiles the whole set as one unit, every com.noop.* JVM unit test was blocked, not just these three files. Android's unit-test CI job is red on main as a result. Add the override to each fake, backed by the same in-memory `devices` map they already use for `setPeripheralId` — a real implementation (copy(model = model)) so the fake's behaviour matches Room's `UPDATE pairedDevice SET model` instead of silently no-op'ing: - app/src/test/java/com/noop/analytics/RegistryDayOwnerSourceTest.kt (FakeDao) - app/src/test/java/com/noop/ble/SourceCoordinatorAdoptionTest.kt (FakeRegistryDao) - app/src/test/java/com/noop/data/DeviceRegistryTest.kt (FakeRegistryDao) Test-only; no production code changes. Verification: `./gradlew testFullDebugUnitTest --tests DeviceRegistryTest --tests RegistryDayOwnerSourceTest --tests SourceCoordinatorAdoptionTest` now compiles and passes (was: test-source compile failure). The whole Full-flavor suite compiles again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 23, 2026
… unit-tested (both platforms) The start-clamp added in the previous commit lived inline in the app-layer persistHypnogramBurst, which CI never compiles or tests. Move the actual clip into the PURE reconstruction `OuraHypnogramBurst.codesWithTimes(endUnixSeconds:sleepStartUnixSeconds:)` (OuraProtocol / Kotlin com.noop.oura), where `swift test` and the JVM tests cover it. The app just computes the 0x49 onset (with its sanity guards) and logs the trim. The pure clip: lay codes backward from the end as before, then drop those with ts < onset. A clamp that would drop EVERY code is IGNORED (returns the full unclamped lay) so a mis-paired 0x49 can never empty the night — the safety guard is now IN the tested function, not the untested app path. Fixtures added on both platforms (HypnogramAssemblerTests / HypnogramAssemblerTest): - drops codes before the onset (kept codes keep their end-anchored times); - boundary is inclusive (>=) and an onset before the burst drops nothing; - an onset after every code falls back to the full night (never-empty guard); - a nil onset is byte-identical to the unclamped lay. Verification: swift test OuraProtocol green incl. the 4 new StartClamp cases; macOS Strand BUILD SUCCEEDED; Android compileFullDebugKotlin OK. (The Kotlin twin fixtures mirror the passing Swift ones; their RUN is still gated by the unrelated ryanbr#716 test-fake compile gap / PR ryanbr#725.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 23, 2026
… 0x49 start-clamp Unit tests for the feature above, on both platforms (byte-identical Swift/Kotlin fixtures): - HypnogramAssembler: the 0x49 start-clamp in the pure codesWithTimes — drops codes before the onset, inclusive boundary, an onset before the burst keeps all, and the never-empty fallback for a mis-paired 0x49; plus the existing backward-layout cases. - OuraSleepSessionMapping: anchored per-code hypnogram → CachedSleepSession stage breakdown. - OuraSleepWindowPairing: closest-0x49-window selection (overnight vs nap, tolerance, empties). - DeviceBrandCatalog.isOura: registry-id → brand resolution (Oura true; WHOOP/computed/import false). - OuraStreamMapping / Decoder golden / Framing / Driver / HistoryDrain: the twin decode + mapping tests. swift test OuraProtocol + WhoopStore green; the Kotlin twins mirror them (their run is gated only by the unrelated ryanbr#716 test-fake compile gap / PR ryanbr#725). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 23, 2026
… 0x49 start-clamp Unit tests for the feature above, on both platforms (byte-identical Swift/Kotlin fixtures): - HypnogramAssembler: the 0x49 start-clamp in the pure codesWithTimes — drops codes before the onset, inclusive boundary, an onset before the burst keeps all, and the never-empty fallback for a mis-paired 0x49; plus the existing backward-layout cases. - OuraSleepSessionMapping: anchored per-code hypnogram → CachedSleepSession stage breakdown. - OuraSleepWindowPairing: closest-0x49-window selection (overnight vs nap, tolerance, empties). - DeviceBrandCatalog.isOura: registry-id → brand resolution (Oura true; WHOOP/computed/import false). - OuraStreamMapping / Decoder golden / Framing / Driver / HistoryDrain: the twin decode + mapping tests. swift test OuraProtocol + WhoopStore green; the Kotlin twins mirror them (their run is gated only by the unrelated ryanbr#716 test-fake compile gap / PR ryanbr#725). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 24, 2026
… 0x49 start-clamp Unit tests for the feature above, on both platforms (byte-identical Swift/Kotlin fixtures): - HypnogramAssembler: the 0x49 start-clamp in the pure codesWithTimes — drops codes before the onset, inclusive boundary, an onset before the burst keeps all, and the never-empty fallback for a mis-paired 0x49; plus the existing backward-layout cases. - OuraSleepSessionMapping: anchored per-code hypnogram → CachedSleepSession stage breakdown. - OuraSleepWindowPairing: closest-0x49-window selection (overnight vs nap, tolerance, empties). - DeviceBrandCatalog.isOura: registry-id → brand resolution (Oura true; WHOOP/computed/import false). - OuraStreamMapping / Decoder golden / Framing / Driver / HistoryDrain: the twin decode + mapping tests. swift test OuraProtocol + WhoopStore green; the Kotlin twins mirror them (their run is gated only by the unrelated ryanbr#716 test-fake compile gap / PR ryanbr#725). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 24, 2026
…name digits (ryanbr#772) On-device a Gen3 ring registered as "Oura Ring 5": a factory-reset ring advertises its SERIAL in the BLE name ("Oura 2H3B2405003655"), and `recognise(advertisedName:)` read the "5" in that serial as gen5. The authoritative generation is in the ring's GetProductInfo hardware id, which the capture just confirmed: `get_hardware` reply (op 0x19) = "BLB_03" → Gen3. - Pure (OuraProtocol, both platforms): `OuraDecoders.productInfoString` decodes a GetProductInfo reply (byte0 status + NUL-terminated ASCII → serial "2H3B2405003655" / hardware "BLB_03"); `OuraRingGen.from(hardwareId:)` maps the trailing "_NN" → gen (BLB_03→gen3 validated; gen4/5 codes unconfirmed → nil, never a guess). Harden `recognise(advertisedName:)` to infer a gen ONLY from an explicit "gen"/"ring" token, so a serial-bearing name yields nil (→ safe default) instead of a bogus gen. - App (OuraLiveSource, both platforms): on a product-info reply, if the hardware id resolves a gen that differs from the session's, fire a new `onModel` callback with the true "Oura Ring N" label; the SourceCoordinator wires it to `registry.setModel`, correcting a mis-stamped row ON CONNECT — no re-pair needed (the same WHOOP `setModel`-on-connect pattern, ryanbr#716). The serial reply has no "_NN" marker, so it never triggers a correction even though both pages answer under op 0x19. Tier discipline: read-only; no new writes to the ring; nothing persisted beyond the corrected model label. Verification: swift test OuraProtocol 127/0 (incl. new RingGenProductInfoTests, 10); macOS Strand BUILD SUCCEEDED; Android compileFullDebugKotlin OK. The Kotlin twin test mirrors the Swift one but its RUN is gated by the pre-existing ryanbr#725 fake-`setModel` gap (three test fakes on main don't implement the DAO method) — unrelated to this change; the main module compiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 24, 2026
) Mirrors the Apple ryanbr#771 serial→id re-point on Android now that it's hardware-validated on macOS. - DeviceRegistryDao: 24 `reKey<Table>(from,to)` `UPDATE OR IGNORE` queries (the SAME device-scoped table set as deleteDeviceData; Room has no dynamic table names), plus `pairedDevice(id)` and `deletePairedDeviceRow`/`deleteDeviceRow`. - DeviceRegistry.adoptSerialIdentity(activeId, serialId): one-transaction twin of the Swift store method — clone the active (provisional) row under the serial id when new, or carry this pairing's peripheralId/model onto an existing serial row (keep active, preserve its addedAt); re-key each table then clear leftovers; drop the provisional CB-UUID pairedDevice + device rows. ONLY the active row is folded — other past oura-* rows are left untouched (agreed scope). - OuraLiveSource: `onSerial` callback fired from the product-info peek when the serial page decodes (guarded by `isPlausibleSerial`); the hardware page still routes to `onModel` (ryanbr#772). Byte-identical to Swift. - SourceCoordinator.adoptOuraSerial: build `oura-<serial>` from the brand catalog (never a literal), migrate the install key via OuraInstallKeyStore (else the re-pointed session can't auth), `setActive` + `onActiveDeviceChanged` re-point the source onto the serial id. Verification: Android compileFullDebugKotlin OK (Room KSP validates the new queries). The adoptSerialIdentity unit test is deferred like the other Oura Kotlin tests — the test module doesn't compile until ryanbr#725 adds the three fake-DAO `setModel` overrides; the Swift twin's 4 tests already pin the logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6
ryanbr
pushed a commit
that referenced
this pull request
Jul 24, 2026
… labelled "Oura" (#773) * feat(oura): persist the ring's SleepNet hypnogram as a stage-timeline sleep session, shown as "Oura" Decode the ring's whole-night SleepNet phase-code burst, reconstruct its 30 s/code time axis, anchor+refine it with the 0x49 sleep-summary window (BOTH ends), and upsert the night as a CachedSleepSession under the ring's own deviceId — so SleepMerge's imported-over-computed rule surfaces Oura's own staging instead of a computed guess. The sleep surfaces then label that night honestly as "Oura" with a caveat that its stages are the ring's raw on-device classification. Persist (both platforms, byte-identical Swift/Kotlin twins): - OuraProtocol: HypnogramAssembler time-axis reconstruction with the 0x49 window applied at BOTH ends — the end anchored to the sleep-end, and the START clamped to the sleep-onset so the burst's pre-window epochs are clipped (the clip lives in the PURE codesWithTimes(…, sleepStartUnixSeconds:) with a never-empty guard, so a mis-paired 0x49 can never erase the night); 0x49 pairing in OuraHistoryDrain; OuraEvents/Decoders/Framing/OuraDriver additions. - WhoopStore: OuraSleepSessionMapping (anchored per-code hypnogram → stage breakdown), OuraStreamMapping sleepPhase pass-through. - App: OuraLiveSource assembles the burst, anchors + 0x49-refines both ends, calls persistSleepSession; SourceCoordinator wires the persist closure. Provenance UI (both platforms): a persisted Oura night is the ring's OWN SleepNet output, not a NOOP computation, so the surfaces say so instead of the generic "On-device": - DeviceBrandCatalog.isOura(deviceId) — ONE canonical resolver (brand table, no "oura" literal); Swift Repository.activeDeviceIsOura + Kotlin SleepScreen call it. - SleepView / SleepScreen: hero + main-sleep badges read "Oura"; the stage caption reads "raw on-device stages"; a caveat note states the split is the ring's RAW BLE classification (more Awake / less Deep+REM) — NOT the Oura app's cloud-adjusted stages. daySourceBadge gains the Oura brand so the ring-id session (and By-Day) label correctly. Design tokens only; WHOOP/Apple imports still win the provenance order above Oura. Tier discipline: the persisted session is a ring-PROVIDED night, not a NOOP-computed score. Supersedes DRAFT #446; docs/OURA_PROTOCOL.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(oura): coverage for the SleepNet persist, "Oura" provenance, and 0x49 start-clamp Unit tests for the feature above, on both platforms (byte-identical Swift/Kotlin fixtures): - HypnogramAssembler: the 0x49 start-clamp in the pure codesWithTimes — drops codes before the onset, inclusive boundary, an onset before the burst keeps all, and the never-empty fallback for a mis-paired 0x49; plus the existing backward-layout cases. - OuraSleepSessionMapping: anchored per-code hypnogram → CachedSleepSession stage breakdown. - OuraSleepWindowPairing: closest-0x49-window selection (overnight vs nap, tolerance, empties). - DeviceBrandCatalog.isOura: registry-id → brand resolution (Oura true; WHOOP/computed/import false). - OuraStreamMapping / Decoder golden / Framing / Driver / HistoryDrain: the twin decode + mapping tests. swift test OuraProtocol + WhoopStore green; the Kotlin twins mirror them (their run is gated only by the unrelated #716 test-fake compile gap / PR #725). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * i18n(oura): localize the Oura raw-stages caveat (Apple) + baseline it (Android) The SleepNet provenance caveat added with the Oura raw-stages note was new UI copy the i18n gate (`Tools/i18n_audit.py --ci`) flagged: on Apple the two literals were absent from their String Catalog; on Android they were new hardcoded literals not in the baseline. - Apple: add both strings — the `Text` caveat and its `accessibilityLabel` — to Strand/Resources/Localizable.xcstrings with translations for all 8 focus locales (de, es, fr, it, pt-PT, ru, zh-Hans, zh-Hant), state `translated`, mirroring the sibling "Low confidence staging" note's terminology (deep/REM, efficiency, stages, "anneau/Ring/anillo/…"). - Android: `SleepScreen.kt` is fully hardcoded (0 stringResource, 6 entries already baselined) and has no low-confidence twin, so the two literals join the accepted backlog via `--update-baseline` — the diff is exactly the two SleepScreen.kt entries, nothing else. (Localizing the Android sleep screen's copy is a separate backlog task; this only closes the NEW-literal regression.) `python3 Tools/i18n_audit.py --ci origin/main` now exits 0 (Android: no new hardcoded literals + focus locales complete; Apple: no new un-extracted literals + all catalogs complete). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ryanbr
added a commit
that referenced
this pull request
Jul 24, 2026
…rom its hardware id (#771, #772) (#776) * feat(oura): capture GetProductInfo serial + hardware replies (groundwork, #771/#772) Read-only capture instrumentation, both platforms. On reaching the stream, ASK the ring its already- defined, pre-auth-readable GetProductInfo serial (`18 03 08 00 10`) + hardware (`18 03 18 00 10`) pages and LOG the raw replies once per op per session (hex + ASCII, since both are strings, e.g. "BLB_03"). Nothing is decoded, minted into a device id, or persisted — this is the capture-first step so a real fixture backs the decode before it drives: - #771 a STABLE `oura-<serial>` identity (iOS can't read the MAC; the CoreBluetooth UUID rotates on re-pair and orphans the ring's history), and - #772 generation from the hardware id instead of stray digits in the advertised name. - OuraLiveSource (Swift + Kotlin twin): send the two reads alongside the existing SpO2 / real-steps status reads in the `.streaming` first-reached block (same read-only class, never a set/enable write); peek every notification's outer frames and log any product-info reply. The reply op is captured as BOTH 0x18 and 0x19 (request→response +1 convention, cf. GetBattery 0x0C→0x0D), so the fixture lands whatever the firmware uses. Neither op is an event tag (>= 0x41), so the peek never disturbs the TLV decode. `loggedProductInfoOps` gates once-per-op and resets on stop/disconnect. Verification: macOS Strand BUILD SUCCEEDED; Android compileFullDebugKotlin OK. The reply layout + whether the serial is stable across a factory-reset-and-adopt are confirmed on the next on-device capture (this instrumentation is how that fixture is collected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6 * fix(oura): dedupe product-info capture by content so get_hardware isn't swallowed (#771/#772) On-device the first capture logged only ONE product-info reply (the serial, op 0x19) even though both get_serial AND get_hardware were sent: the guard printed once per OP, and both pages answer under op 0x19, so the get_hardware reply — the page that carries the hardware id for #772 generation detection — was suppressed. Key the once-per-session guard by op+body instead of op, so each DISTINCT reply logs once and only identical re-serves are collapsed. Both platforms. Field-confirmed by the capture this fixes: get_serial reply op=0x19 `00 <ascii serial> 00 00` = "2H3B2405003655", byte-identical before and after a factory-reset-and-adopt (which rotated the CoreBluetooth UUID from 99B6BA9D… to 4DD70E24…) — i.e. the serial is a STABLE ring identity where the CBUUID is not. Still capture-only; nothing decoded/minted yet. Verification: macOS Strand BUILD SUCCEEDED; Android compileFullDebugKotlin OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6 * fix(oura): resolve generation from the ring's hardware id, not stray name digits (#772) On-device a Gen3 ring registered as "Oura Ring 5": a factory-reset ring advertises its SERIAL in the BLE name ("Oura 2H3B2405003655"), and `recognise(advertisedName:)` read the "5" in that serial as gen5. The authoritative generation is in the ring's GetProductInfo hardware id, which the capture just confirmed: `get_hardware` reply (op 0x19) = "BLB_03" → Gen3. - Pure (OuraProtocol, both platforms): `OuraDecoders.productInfoString` decodes a GetProductInfo reply (byte0 status + NUL-terminated ASCII → serial "2H3B2405003655" / hardware "BLB_03"); `OuraRingGen.from(hardwareId:)` maps the trailing "_NN" → gen (BLB_03→gen3 validated; gen4/5 codes unconfirmed → nil, never a guess). Harden `recognise(advertisedName:)` to infer a gen ONLY from an explicit "gen"/"ring" token, so a serial-bearing name yields nil (→ safe default) instead of a bogus gen. - App (OuraLiveSource, both platforms): on a product-info reply, if the hardware id resolves a gen that differs from the session's, fire a new `onModel` callback with the true "Oura Ring N" label; the SourceCoordinator wires it to `registry.setModel`, correcting a mis-stamped row ON CONNECT — no re-pair needed (the same WHOOP `setModel`-on-connect pattern, #716). The serial reply has no "_NN" marker, so it never triggers a correction even though both pages answer under op 0x19. Tier discipline: read-only; no new writes to the ring; nothing persisted beyond the corrected model label. Verification: swift test OuraProtocol 127/0 (incl. new RingGenProductInfoTests, 10); macOS Strand BUILD SUCCEEDED; Android compileFullDebugKotlin OK. The Kotlin twin test mirrors the Swift one but its RUN is gated by the pre-existing #725 fake-`setModel` gap (three test fakes on main don't implement the DAO method) — unrelated to this change; the main module compiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6 * feat(oura): mint the device id from the ring's stable serial, re-pointing off the CBUUID (#771) An Oura re-pair mints a fresh CoreBluetooth UUID, so `oura-<CBUUID>` changes every pairing and orphans the ring's history under the old id (on-device: one ring produced FOUR oura-<UUID> rows, the live data stranded under an archived one). The ring's SERIAL (read via GetProductInfo on connect) is the one identity that survives a re-pair, so key the device on `oura-<serial>` instead. Approach (option 1, agreed with the user — Apple side; Android twin deferred, see below): - The live source keeps minting the provisional `oura-<CBUUID>` at pairing; once it reads the serial page at `.streaming`, it fires a new `onSerial` callback (guarded by `isPlausibleSerial`: alphanumeric, 8..24, so a misframed reply can't mint a bogus id). The serial page is told apart from the hardware page (both op 0x19) by content — the hardware id has an "_NN" gen marker, the serial does not. - `SourceCoordinator.adoptOuraSerial` re-points the ACTIVE row onto `oura-<serial>` (prefix from the brand catalog, never a literal): `DeviceRegistryStore.adoptSerialIdentity(from:to:)` folds ONLY the active CB-UUID row's data + registry into the serial id (a rename when new, an UPDATE-OR-IGNORE merge when a prior pairing already established it) and DELIBERATELY leaves other past `oura-*` rows untouched — the agreed scope. The install key (Keychain-keyed by deviceId) is migrated onto the serial id or the re-pointed session couldn't authenticate. Then `setActive(serialId)` moves the read/write spine over (the coordinator rebuilds the source under the serial id via the activeDeviceId observer; deferred one main-loop turn so the current BLE callback returns before its own source is torn down). Result: after the first connect on the new build the active id becomes `oura-<serial>`, and every future re-pair converges onto it — no more orphaning. Idempotent (serialId == deviceId ⇒ no-op). Verification: swift test WhoopStore 273/0 (+4 adoptSerialIdentity tests: rename-when-new, merge-when-exists, leaves-other-pairings-untouched, no-op); macOS Strand BUILD SUCCEEDED. The re-point + key migration + adopt- flow interaction need on-device validation (a mid-session identity change is novel), so the Android twin (a sizeable per-table Room reconcile + wiring) is deliberately deferred until this is proven on hardware — porting first risks redoing it. The pure decode (productInfoString / fromHardwareId) is already twinned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6 * feat(oura): Android twin of the serial-based device identity (#771) Mirrors the Apple #771 serial→id re-point on Android now that it's hardware-validated on macOS. - DeviceRegistryDao: 24 `reKey<Table>(from,to)` `UPDATE OR IGNORE` queries (the SAME device-scoped table set as deleteDeviceData; Room has no dynamic table names), plus `pairedDevice(id)` and `deletePairedDeviceRow`/`deleteDeviceRow`. - DeviceRegistry.adoptSerialIdentity(activeId, serialId): one-transaction twin of the Swift store method — clone the active (provisional) row under the serial id when new, or carry this pairing's peripheralId/model onto an existing serial row (keep active, preserve its addedAt); re-key each table then clear leftovers; drop the provisional CB-UUID pairedDevice + device rows. ONLY the active row is folded — other past oura-* rows are left untouched (agreed scope). - OuraLiveSource: `onSerial` callback fired from the product-info peek when the serial page decodes (guarded by `isPlausibleSerial`); the hardware page still routes to `onModel` (#772). Byte-identical to Swift. - SourceCoordinator.adoptOuraSerial: build `oura-<serial>` from the brand catalog (never a literal), migrate the install key via OuraInstallKeyStore (else the re-pointed session can't auth), `setActive` + `onActiveDeviceChanged` re-point the source onto the serial id. Verification: Android compileFullDebugKotlin OK (Room KSP validates the new queries). The adoptSerialIdentity unit test is deferred like the other Oura Kotlin tests — the test module doesn't compile until #725 adds the three fake-DAO `setModel` overrides; the Swift twin's 4 tests already pin the logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: ryanbr <mp3geek@gmail.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 25, 2026
…series survives storage The 0x5D hrv_event body is a run of (u8 avg HR, u8 avg RMSSD) pairs, one per 5-min bucket. The mapping stamped every bucket in a record with the SAME `ts`, but the event key is (deviceId, ts, kind) with ON CONFLICT DO NOTHING — so N buckets collided on insert and only the first survived, silently dropping the rest of the ring's per-5-min series (the very series this tag now represents). Apply the documented 5-min cadence at mapping time (the OuraHRV.index contract: per-sample times step back from the event time, OURA_PROTOCOL.md s6): bucket `index` is stamped 300 * index seconds before the record time. Distinct rows, ordered oldest-last, none dropped. Derived from the known cadence + record anchor, not a guessed time. Applied identically on Swift and Kotlin. Also fix the stale mapping-layer header docs on both platforms — they still described the dead (time_ms/b1/b2) layout and forbade the rmssd_ms field the code now emits. Addresses reviewer feedback on the storage collision and stale docs. Swift: WhoopStore builds, OuraStreamMappingTests 12/12 pass incl. new multi-bucket test. Kotlin: main source compiles; twin test added (module unit tests still blocked pre-existing by the ryanbr#725 DAO fake gap). No new i18n strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6
12 tasks
ryanbr
pushed a commit
that referenced
this pull request
Jul 26, 2026
…i.kt (#733) Moves the Sleep stage breakdown and hypnogram rendering helpers out of SleepScreen.kt into SleepStageBreakdownUi.kt, beside the existing Sleep logic helper files. Verified as a genuine pure move rather than trusting the label: every function body was extracted from both sides and compared. Five functions moved, five removed, sets identical, bodies byte-for-byte unchanged — with one exception, `private` -> `internal` on exactly the three that cross the file boundary (ClockLabelRow, HypnogramWithAxis, StageBreakdownRows). That is the minimum widening the move requires. StageBreakdownRow and stageColorFor stay private because they are only called from inside the new file. The two apparent external references are false hits: StageBreakdownRow matches as a substring of StageBreakdownRows, and stageColorFor appears only in a comment at SleepStageTimelineLogic.kt:128. No blanket widening. The +30 line delta is package declaration, imports and file header. Localization checked and clean — the moved code uses uiString(R.string.…) resource lookups, not the Text(variable) pattern the i18n audit cannot see. Contributor compiled with compileFullDebugKotlin locally and said so, which with android.yml disabled is the only build this code gets. Unit tests were not run, and that is not on this PR: DeviceRegistryDao.setModel blocks testFullDebugUnitTest for everyone, #725 is the open fix, and this is the third contributor to hit it today. Follow-up filed separately: stageColorFor duplicates the canonicalisation in canonicalStage(), which carries a comment saying the two must stay in parity. The refactor did not create that — it made it visible by putting the halves in adjacent files.
3 tasks
ryanbr
pushed a commit
that referenced
this pull request
Jul 26, 2026
…e test fakes (#828) The Android test source set did not compile. DeviceRegistryDao declares the reKey* family (#771 adopt-serial), pairedDevice(id) and deletePairedDeviceRow(id), but three standalone in-memory fakes were never updated: DeviceRegistryTest.FakeRegistryDao SourceCoordinatorAdoptionTest.FakeRegistryDao RegistryDayOwnerSourceTest.FakeDao Kotlin compiles a source set as one unit, so this blocked the ENTIRE Android unit-test tree, not just these three classes — no Android test could run locally at all, whatever it covered. Several PRs open today could not tick their Android-test box because of it. Verified independently rather than on the build claim, and re-verified at the merged head: DeviceRegistryDao declares 64 suspend funs and all three fakes override all 64, none missing. reKeyDayOwnership is the only non-no-op and its semantics are correct. dayOwnership's primary key is `day` alone, so UPDATE OR IGNORE ... SET deviceId can never hit a key collision and the OR IGNORE is a no-op safeguard there; the fake's LinkedHashMap<day, row> models the table exactly. Replacing a value for an existing key mid-iteration is safe — value replacement is not a structural modification. The remaining reKey* overrides are no-ops, matching the existing delete*For pattern for tables these fakes do not model. Test-only, pure additions, no production logic touched. Supersedes #725, which implements setModel — already present in all three fakes on main.
Owner
|
Closing as superseded by #828 (merged as The specific change here — implementing Thanks for spotting the breakage and pushing on it — the Android test tree compiles again, so |
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 27, 2026
…irs — was mis-framed The 0x5D hrv_event body is a run of (u8 avg HR bpm, u8 avg RMSSD ms) PAIRS, one per 5-min bucket (open_oura `decode_hrv`; confirmed on a real overnight capture). NOOP read it as a 4-byte (u16 time, int8 b1, int8 b2) stride — a mis-framing that: - garbled the first (hr, rmssd) byte-pair into a bogus `time_ms`, - sign-flipped the RMSSD byte (int8, so 131 read as -125), - and only `b1` accidentally landed on a real HR byte. Validation: the corrected HR byte tracks sleeping HR — a captured 22-Jul night decoded to ~52 bpm across the small hours, matching the ryanbr#511 IBI-derived median (54 bpm). The bogus `time_ms` values reconstructed byte-for-byte into the (hr, rmssd) of the first pair. Changes (byte-identical Swift/Kotlin twins): - OuraProtocol: `OuraHRV` now carries `(index, hrBpm, rmssdMs)`; `decodeHRV` walks 2-byte (u8 hr, u8 rmssd) pairs, both UNSIGNED, and returns nil on an empty/ODD-length body (never a partial pair). Golden tests use real overnight bytes (32 84 32 83) + an odd-length-nil case. `oura-decode` CLI line updated. - WhoopStore/data: OURA_HRV event payload is now honestly labelled `hr_bpm`/`rmssd_ms`/ `pair_index` (the byte->unit scaling is pinned now, so the old "raw, units-neutral, no rmssd_ms" caveat no longer applies). Keys/values identical across platforms. - docs/OURA_PROTOCOL.md s6.9 corrected with the pair layout + the decode-correction note. This is a pure decoder-correctness fix. It also unblocks a follow-up (ryanbr#728): the ring's own overnight HR + RMSSD are now available honestly, feeding avgHrv directly and giving a ground-truth HR to validate the IBI-derived hrSample against. Verification: swift test OuraProtocol + WhoopStore green (incl. the corrected 0x5D golden + mapping tests); macOS Strand BUILD SUCCEEDED; Android compileFullDebugKotlin OK. The Android unit-test RUN is blocked only by the unrelated ryanbr#716 test-fake gap (PR ryanbr#725); the Kotlin 0x5D tests mirror the passing Swift ones and run once that lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pipiche38
added a commit
to pipiche38/noop
that referenced
this pull request
Jul 27, 2026
…series survives storage The 0x5D hrv_event body is a run of (u8 avg HR, u8 avg RMSSD) pairs, one per 5-min bucket. The mapping stamped every bucket in a record with the SAME `ts`, but the event key is (deviceId, ts, kind) with ON CONFLICT DO NOTHING — so N buckets collided on insert and only the first survived, silently dropping the rest of the ring's per-5-min series (the very series this tag now represents). Apply the documented 5-min cadence at mapping time (the OuraHRV.index contract: per-sample times step back from the event time, OURA_PROTOCOL.md s6): bucket `index` is stamped 300 * index seconds before the record time. Distinct rows, ordered oldest-last, none dropped. Derived from the known cadence + record anchor, not a guessed time. Applied identically on Swift and Kotlin. Also fix the stale mapping-layer header docs on both platforms — they still described the dead (time_ms/b1/b2) layout and forbade the rmssd_ms field the code now emits. Addresses reviewer feedback on the storage collision and stale docs. Swift: WhoopStore builds, OuraStreamMappingTests 12/12 pass incl. new multi-bucket test. Kotlin: main source compiles; twin test added (module unit tests still blocked pre-existing by the ryanbr#725 DAO fake gap). No new i18n strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtXcBU1t6Xk1qJhaQEeDx6
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.
Problem
The Android Full-flavor unit test source set does not compile on
main, so everycom.noop.*JVM unit test is blocked (Android's unit-test CI job is red).PR #716 added a method to the
DeviceRegistryDaointerface — and updated the production Room DAO + Swift twin — but not the three hand-written test doubles that implement the interface: