Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,7 @@ scratch_*
/fetch_apps.js
#adding firebase_options back so that we dont commit it accidentally in future releases
lib/firebase_options.dart

# Local agent worktrees and execution ledgers.
/.worktrees/
/.superpowers/
4 changes: 4 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,12 @@ flutter {
dependencies {
// Backs isCoreLibraryDesugaringEnabled (required by ota_update 7.x).
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// Native sleep-session writer. Match health 11.1.1's AndroidX version so
// the app and plugin compile against one Health Connect API surface.
implementation("androidx.health.connect:connect-client:1.1.0-alpha07")
// KeepAliveWorker (service watchdog). The workmanager Flutter plugin ships the
// runtime transitively, but as an `implementation` dep it isn't visible to app
// code at compile time — declare it explicitly for our native Worker.
implementation("androidx.work:work-runtime-ktx:2.9.1")
testImplementation("junit:junit:4.13.2")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package wtf.openstrap.openstrap_edge

import android.content.Context
import android.util.Log
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.time.TimeRangeFilter
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.time.Instant
import java.time.ZoneId
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

/** Replaces one day's normalized minute heart-rate samples in Health Connect. */
object HealthConnectHeartRateWriter {
private const val TAG = "OpenStrapHeartRateExport"
private const val CHANNEL = "openstrap/health_connect_heart_rate"
private const val REPLACE_HEART_RATE_DAY = "replaceHeartRateDay"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val replaceMutex = Mutex()

fun register(engine: FlutterEngine, context: Context) {
val app = context.applicationContext
MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method != REPLACE_HEART_RATE_DAY) {
result.notImplemented()
return@setMethodCallHandler
}
scope.launch {
val replaced = withContext(Dispatchers.IO) { replace(app, call) }
result.success(replaced)
}
}
}

@Suppress("TooGenericExceptionCaught")
private suspend fun replace(context: Context, call: MethodCall): Boolean {
return replaceMutex.withLock {
try {
val request = buildRequest(call) ?: return@withLock false
if (request.samples.isEmpty()) return@withLock true
if (HealthConnectClient.getSdkStatus(context) != HealthConnectClient.SDK_AVAILABLE) {
false
} else {
val client = HealthConnectClient.getOrCreate(context)
client.deleteRecords(
HeartRateRecord::class,
TimeRangeFilter.between(request.start, request.end),
)
client.insertRecords(
listOf(
HeartRateRecord(
startTime = request.start,
endTime = request.end,
startZoneOffset = ZoneId.systemDefault().rules.getOffset(request.start),
endZoneOffset = ZoneId.systemDefault().rules.getOffset(request.end),
samples = request.samples,
metadata = Metadata(
recordingMethod = Metadata.RECORDING_METHOD_AUTOMATICALLY_RECORDED,
),
),
),
).recordIdsList.size == 1
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Exception) {
Log.e(TAG, "HeartRateRecord replace failed", error)
false
}
}
}

private data class Request(
val start: Instant,
val end: Instant,
val samples: List<HeartRateRecord.Sample>,
)

private fun buildRequest(call: MethodCall): Request? {
val start = (call.argument<Any>("startTime") as? Number)
?.toLong()?.let(Instant::ofEpochMilli) ?: return null
val end = (call.argument<Any>("endTime") as? Number)
?.toLong()?.let(Instant::ofEpochMilli) ?: return null
if (!start.isBefore(end)) return null

val rawSamples = call.argument<List<*>>("samples") ?: return null
val samples = ArrayList<HeartRateRecord.Sample>(rawSamples.size)
var previous: Instant? = null
for (raw in rawSamples) {
val sample = (raw as? Map<*, *>)?.let(::buildSample) ?: return null
if (sample.time.isBefore(start) || !sample.time.isBefore(end)) return null
if (previous != null && !previous.isBefore(sample.time)) return null
samples.add(sample)
previous = sample.time
}
return Request(start, end, samples)
}

private fun buildSample(raw: Map<*, *>): HeartRateRecord.Sample? {
val time = (raw["time"] as? Number)?.toLong()?.let(Instant::ofEpochMilli)
?: return null
val beatsPerMinute = (raw["beatsPerMinute"] as? Number)?.toLong() ?: return null
if (beatsPerMinute !in 1..300) return null
return HeartRateRecord.Sample(time, beatsPerMinute)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package wtf.openstrap.openstrap_edge

import android.content.Context
import android.util.Log
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.records.SleepSessionRecord
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.time.TimeRangeFilter
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.time.Instant
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

internal data class SleepCleanupRange(val start: Instant, val end: Instant)

internal fun sleepCleanupRange(start: Instant, end: Instant, zoneId: ZoneId): SleepCleanupRange {
require(start.isBefore(end))
val localEnd = end.atZone(zoneId)
val endDate = if (localEnd.toLocalTime().isBefore(LocalTime.NOON)) {
localEnd.toLocalDate()
} else {
localEnd.toLocalDate().plusDays(1)
}
val cleanupEnd = endDate.atTime(LocalTime.NOON).atZone(zoneId).toInstant()
val calculatedStart = endDate.minusDays(1).atTime(LocalTime.NOON).atZone(zoneId).toInstant()
val cleanupStart = if (start.isBefore(calculatedStart)) start else calculatedStart
return SleepCleanupRange(cleanupStart, cleanupEnd)
}
Comment on lines +27 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the sleep-window derivation to see whether an offset can land at/after local noon.
set -euo pipefail

fd -e dart . lib --exec rg -n -C4 'offset_ms|onset_ms' {} \; | head -100

printf '\n--- sleep window segmentation source ---\n'
fd -i 'sleep' -e dart . lib

Repository: OpenStrap/edge

Length of output: 4333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- candidate files ---\n'
fd -e dart . lib | sort
fd -e kt android . | sort

printf '\n--- HealthConnectSleepWriter outline ---\n'
ast-grep outline android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/HealthConnectSleepWriter.kt --view expanded || true

printf '\n--- HealthConnectSleepWriter relevant lines ---\n'
cat -n android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/HealthConnectSleepWriter.kt | sed -n '1,140p'

printf '\n--- references to sleepCleanupRange and replace ---\n'
rg -n "sleepCleanupRange|fun replace|class.*Sleep|replace\\(" android/lib -S

Repository: OpenStrap/edge

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd -e dart . lib | sort
fd -e kt android . | sort

printf '%s\n' ''
printf '%s\n' '--- HealthConnectSleepWriter outline ---'
ast-grep outline android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/HealthConnectSleepWriter.kt --view expanded || true

printf '%s\n' ''
printf '%s\n' '--- HealthConnectSleepWriter relevant lines ---'
cat -n android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/HealthConnectSleepWriter.kt | sed -n '1,140p'

printf '%s\n' ''
printf '%s\n' '--- references to sleepCleanupRange and replace ---'
rg -n "sleepCleanupRange|fun replace|class.*Sleep|replace\\(" android/lib -S || true

Repository: OpenStrap/edge

Length of output: 13949


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- sleep-related Dart files ---'
fd -e dart . lib | rg -i 'sleep|heal' || true

printf '%s\n' ''
printf '%s\n' '--- sleep session normalizer relevant lines ---'
cat -n lib/health/health_sleep_session.dart | sed -n '1,240p'

printf '%s\n' ''
printf '%s\n' '--- derivation files sleep terms ---'
rg -n -C3 "sleep(\\.window|Window|window)|sleep_|onset|offset|wake_ts|startTime|endTime|offset_ms|onset_ms" lib lib/test --glob '*.dart' || true

printf '%s\n' ''
printf '%s\n' '--- day_id groupby and bulk export references ---'
rg -n -C4 "day_id|groupBy|exportAll|replaceSleepSession|sleepCleanupRange|sleep.window" lib --glob '*.dart' || true

Repository: OpenStrap/edge

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact search for replaceSleepSession call sites ---'
rg -n "replaceSleepSession|HealthConnectSleepWriter|sleepCleanupRange" lib android --glob '*.dart' --glob '*.kt' || true

printf '%s\n' ''
printf '%s\n' '--- sleep export/write channel usage ---'
rg -n "openstrap/health_connect_sleep|HealthConnect|sleep_session|startTime|endTime" lib --glob '*.dart' --max-count 120 || true

printf '%s\n' ''
printf '%s\n' '--- targetDayWindow and day attribution region ---'
cat -n lib/compute/derivation_engine.dart | sed -n '4300,4345p'
cat -n lib/compute/substrate.dart | sed -n '428,434p'

printf '%s\n' ''
python3 - <<'PY'
from datetime import datetime, timezone, timedelta

def cleanup_range_ms(epoch_start_ms, epoch_end_ms, tz_offset_ms):
    zone = timezone(timedelta(milliseconds=tz_offset_ms))
    start = datetime.fromtimestamp(epoch_start_ms/1000, tz=zone)
    end = datetime.fromtimestamp(epoch_end_ms/1000, tz=zone)
    local_end = end.astimezone(zone)
    if local_end.time() < datetime(2000,1,1,12,0).time():
        end_date = local_end.date()
    else:
        end_date = local_end.date() + timedelta(days=1)
    cleanup_end = datetime.combine(end_date, datetime.min.time()).replace(hour=12, tzinfo=zone).timestamp()*1000
    calculated_start_ms = (datetime.combine(end_date - timedelta(days=1), datetime.min.time()).replace(hour=12, tzinfo=zone).timestamp()*1000)
    cleanup_start_ms = min(epoch_start_ms, calculated_start_ms)
    return cleanup_start_ms, cleanup_end_ms

def to_window(epoch):
    end = datetime.fromtimestamp(epoch/1000, tz=timezone.utc) + timedelta(hours=5)
    return end.date().isoformat(), end.time().isoformat()

base = datetime(2026, 8, 3, 0, 0, tzinfo=timezone.utc).timestamp()*1000
cases = [
    ("nap ending 13:00 day N and following night ending 07:00 day N+1",
     base + 13*3600*1000, base + 24*3600*1000 + 7*3600*1000, +5*3600*1000),
]
for label, start, end, offset in cases:
    cstart, cend = cleanup_range_ms(start, end, offset)
    print(label)
    print("session:", to_window(start), to_window(end))
    print("cleanup:", to_window(cstart), to_window(cend))
    print("overlap:", (max(start, cstart) < min(end, cend)) and (max(start, cstart) < min(cend, end)))
PY

printf '%s\n' ''
printf '%s\n' '--- tests around sleepCleanupRange if present ---'
fd -e kt . | rg -i test || true
rg -n "sleepCleanupRange|SleepCleanupRange|noon|LocalTime.NOON|cleanup" . --glob '*.kt' --glob '*.test.*' --glob '*_test.*' || true

Repository: OpenStrap/edge

Length of output: 10473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- HealthConnect sleep writer call path ---'
cat -n lib/health/health_sleep_session.dart | sed -n '120,210p'
cat -n lib/health/health_export.dart | sed -n '560,620p'
cat -n lib/health/health_export.dart | sed -n '780,850p'

printf '%s\n' ''
printf '%s\n' '--- day bundle grouping and replacement context ---'
cat -n lib/health/health_export.dart | sed -n '540,560p'
rg -n -C2 "day_id|sleep_session_candidates|fromMap|toMap|sleep:" lib/health/health_sleep_session.dart lib/health/health_export.dart

printf '%s\n' ''
printf '%s\n' '--- derive window and daylight search horizon ---'
cat -n lib/compute/derivation_engine.dart | sed -n '540,580p'
cat -n lib/compute/substrate.dart | sed -n '464,488p'
cat -n lib/compute/derivation_engine.dart | sed -n '1932,1950p'

Repository: OpenStrap/edge

Length of output: 15423


Keep sleep cleanup inside the day window.

_exportDay marks success if the sleep replace succeeds, then deletes other data only for [dayStart, dayEnd). If a sleep window exits the local day, replace() can then delete records beyond dayEnd while the day is considered exported, and later data for that exported day is not re-published in that pass. Clamp cleanupRange.end to the exported day interval, or avoid exporting a sleep window that crosses the day boundary without handling the cleanup boundary separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/main/kotlin/wtf/openstrap/openstrap_edge/HealthConnectSleepWriter.kt`
around lines 27 - 39, Update sleepCleanupRange so its returned cleanupEnd cannot
extend beyond the exported day interval used by _exportDay. Clamp the calculated
end to the day’s end boundary, or skip/handle cross-boundary sleep windows
separately, while preserving valid cleanup for windows entirely within the day.


/** Writes one Health Connect sleep parent containing all normalized stages. */
object HealthConnectSleepWriter {
private const val TAG = "OpenStrapSleepExport"
private const val CHANNEL = "openstrap/health_connect_sleep"
private const val REPLACE_SLEEP_SESSION = "replaceSleepSession"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val replaceMutex = Mutex()

fun register(engine: FlutterEngine, context: Context) {
val app = context.applicationContext
MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method != REPLACE_SLEEP_SESSION) {
result.notImplemented()
return@setMethodCallHandler
}
scope.launch {
val replaced = withContext(Dispatchers.IO) { replace(app, call) }
result.success(replaced)
}
}
}

@Suppress("TooGenericExceptionCaught")
private suspend fun replace(context: Context, call: MethodCall): Boolean {
return replaceMutex.withLock {
try {
if (HealthConnectClient.getSdkStatus(context) != HealthConnectClient.SDK_AVAILABLE) {
false
} else {
val session = buildSession(call)
if (session == null) {
false
} else {
val client = HealthConnectClient.getOrCreate(context)
val cleanupRange = sleepCleanupRange(
session.startTime,
session.endTime,
ZoneId.systemDefault(),
)

// This removes both records created by this writer and legacy
// one-stage fragments whose intervals fall inside the real
// overnight window, including the portion before midnight.
client.deleteRecords(
SleepSessionRecord::class,
TimeRangeFilter.between(cleanupRange.start, cleanupRange.end),
)
client.insertRecords(listOf(session)).recordIdsList.size == 1
}
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (error: Exception) {
// Health Connect may surface several unrelated platform and
// transport exceptions; the channel reports all of them as false.
Log.e(TAG, "SleepSessionRecord replace failed", error)
false
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

private fun buildSession(call: MethodCall): SleepSessionRecord? {
val start = (call.argument<Any>("startTime") as? Number)
?.toLong()?.let(Instant::ofEpochMilli) ?: return null
val end = (call.argument<Any>("endTime") as? Number)
?.toLong()?.let(Instant::ofEpochMilli) ?: return null
if (!start.isBefore(end)) return null

val rawStages = call.argument<List<*>>("stages").orEmpty()
val stages = rawStages
.mapNotNull { (it as? Map<*, *>)?.let(::buildStage) }
.sortedBy { it.startTime }
if (stages.isEmpty()) return null
var previousEnd = start
for (stage in stages) {
if (stage.startTime.isBefore(start) || stage.endTime.isAfter(end)) return null
if (!stage.startTime.isBefore(stage.endTime)) return null
if (stage.startTime.isBefore(previousEnd)) return null
previousEnd = stage.endTime
}

val zoneRules = ZoneId.systemDefault().rules
return SleepSessionRecord(
startTime = start,
startZoneOffset = zoneRules.getOffset(start),
endTime = end,
endZoneOffset = zoneRules.getOffset(end),
title = "OpenStrap sleep",
stages = stages,
metadata = Metadata(
recordingMethod = Metadata.RECORDING_METHOD_AUTOMATICALLY_RECORDED,
),
)
}

private fun buildStage(raw: Map<*, *>): SleepSessionRecord.Stage? {
val start = (raw["startTime"] as? Number)?.toLong()?.let(Instant::ofEpochMilli)
?: return null
val end = (raw["endTime"] as? Number)?.toLong()?.let(Instant::ofEpochMilli)
?: return null
val type = when (raw["stage"] as? String) {
"awake" -> SleepSessionRecord.STAGE_TYPE_AWAKE
"rem" -> SleepSessionRecord.STAGE_TYPE_REM
"light" -> SleepSessionRecord.STAGE_TYPE_LIGHT
"deep" -> SleepSessionRecord.STAGE_TYPE_DEEP
else -> return null
}
return SleepSessionRecord.Stage(start, end, type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ object NativeChannels {
fun register(engine: FlutterEngine, context: Context) {
val app = context.applicationContext

HealthConnectSleepWriter.register(engine, app)
HealthConnectHeartRateWriter.register(engine, app)

MethodChannel(engine.dartExecutor.binaryMessenger, EDGE_TRACKING_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package wtf.openstrap.openstrap_edge

import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class HealthConnectSleepWriterTest {
private val berlin = ZoneId.of("Europe/Berlin")

private fun localInstant(year: Int, month: Int, day: Int, hour: Int, minute: Int): Instant =
ZonedDateTime.of(year, month, day, hour, minute, 0, 0, berlin).toInstant()

@Test
fun cleanupRangeIncludesLegacyFragmentThatStartsBeforeRecomputedSession() {
val sessionStart = localInstant(2026, 8, 6, 1, 36)
val sessionEnd = localInstant(2026, 8, 6, 8, 20)
val staleFragmentStart = localInstant(2026, 8, 5, 23, 34)

val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)

assertEquals(localInstant(2026, 8, 5, 12, 0), range.start)
assertEquals(localInstant(2026, 8, 6, 12, 0), range.end)
assertTrue(!staleFragmentStart.isBefore(range.start))
assertTrue(staleFragmentStart.isBefore(range.end))
}

@Test
fun cleanupRangeUsesLocalNoonAcrossDstTransition() {
val sessionStart = localInstant(2026, 10, 25, 1, 30)
val sessionEnd = localInstant(2026, 10, 25, 9, 0)

val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)

assertEquals(localInstant(2026, 10, 24, 12, 0), range.start)
assertEquals(localInstant(2026, 10, 25, 12, 0), range.end)
assertEquals(25, Duration.between(range.start, range.end).toHours())
}
Comment on lines +17 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the early-start branch of sleepCleanupRange.

Both tests exercise only the path where calculatedStart wins on line 37. The branch that returns the session start (a session starting before local noon of the previous day) has no coverage. That branch protects a long session from a truncated cleanup window.

♻️ Proposed test
+    `@Test`
+    fun cleanupRangeExtendsBackToAnEarlySessionStart() {
+        val sessionStart = localInstant(2026, 8, 5, 9, 15)
+        val sessionEnd = localInstant(2026, 8, 6, 8, 20)
+
+        val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)
+
+        assertEquals(sessionStart, range.start)
+        assertEquals(localInstant(2026, 8, 6, 12, 0), range.end)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
fun cleanupRangeIncludesLegacyFragmentThatStartsBeforeRecomputedSession() {
val sessionStart = localInstant(2026, 8, 6, 1, 36)
val sessionEnd = localInstant(2026, 8, 6, 8, 20)
val staleFragmentStart = localInstant(2026, 8, 5, 23, 34)
val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)
assertEquals(localInstant(2026, 8, 5, 12, 0), range.start)
assertEquals(localInstant(2026, 8, 6, 12, 0), range.end)
assertTrue(!staleFragmentStart.isBefore(range.start))
assertTrue(staleFragmentStart.isBefore(range.end))
}
@Test
fun cleanupRangeUsesLocalNoonAcrossDstTransition() {
val sessionStart = localInstant(2026, 10, 25, 1, 30)
val sessionEnd = localInstant(2026, 10, 25, 9, 0)
val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)
assertEquals(localInstant(2026, 10, 24, 12, 0), range.start)
assertEquals(localInstant(2026, 10, 25, 12, 0), range.end)
assertEquals(25, Duration.between(range.start, range.end).toHours())
}
`@Test`
fun cleanupRangeIncludesLegacyFragmentThatStartsBeforeRecomputedSession() {
val sessionStart = localInstant(2026, 8, 6, 1, 36)
val sessionEnd = localInstant(2026, 8, 6, 8, 20)
val staleFragmentStart = localInstant(2026, 8, 5, 23, 34)
val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)
assertEquals(localInstant(2026, 8, 5, 12, 0), range.start)
assertEquals(localInstant(2026, 8, 6, 12, 0), range.end)
assertTrue(!staleFragmentStart.isBefore(range.start))
assertTrue(staleFragmentStart.isBefore(range.end))
}
`@Test`
fun cleanupRangeUsesLocalNoonAcrossDstTransition() {
val sessionStart = localInstant(2026, 10, 25, 1, 30)
val sessionEnd = localInstant(2026, 10, 25, 9, 0)
val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)
assertEquals(localInstant(2026, 10, 24, 12, 0), range.start)
assertEquals(localInstant(2026, 10, 25, 12, 0), range.end)
assertEquals(25, Duration.between(range.start, range.end).toHours())
}
`@Test`
fun cleanupRangeExtendsBackToAnEarlySessionStart() {
val sessionStart = localInstant(2026, 8, 5, 9, 15)
val sessionEnd = localInstant(2026, 8, 6, 8, 20)
val range = sleepCleanupRange(sessionStart, sessionEnd, berlin)
assertEquals(sessionStart, range.start)
assertEquals(localInstant(2026, 8, 6, 12, 0), range.end)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/app/src/test/kotlin/wtf/openstrap/openstrap_edge/HealthConnectSleepWriterTest.kt`
around lines 17 - 41, Add a test covering the early-start branch in
sleepCleanupRange, using a session start before local noon on the previous day
so the function returns the session start rather than calculatedStart. Assert
the cleanup range preserves that early start and still uses the expected session
end boundary.

}
Loading