test(android): prove sleep intake lifecycle retention

This commit is contained in:
fy59 2026-09-24 06:52:03 +02:00
parent fe79c44e51
commit 696dac82a3
2 changed files with 74 additions and 5 deletions

View file

@ -92,16 +92,55 @@ class SyncGenerationServiceTest {
active = true,
)
) as TrainlogRepository.SaveSleepDiaryResult.Saved
val pendingRoot =
source.saveSleepDiary(
SleepDiaryDraft(
nightStartDate = "2026-09-23",
nightEndDate = "2026-09-24",
createdAt = "2026-09-23T22:00:00+02:00",
updatedAt = "2026-09-23T22:00:00+02:00",
sleepQuality = null,
wakeQuality = null,
dayForm = null,
treatmentAndNotes = "",
events = emptyList(),
intakes = emptyList(),
)
) as TrainlogRepository.SaveSleepDiaryResult.Saved
val preBed =
source.quickSleepMedication(
medicationCreated.entryId,
"2026-09-23T22:20:00+02:00",
quantity = 2,
) as TrainlogRepository.SleepQuickActionResult.Applied
assertEquals(pendingRoot.entryId, preBed.receipt.entryId)
source.quickSleepMedication(
medicationCreated.entryId,
"2026-09-23T22:21:00+02:00",
) as TrainlogRepository.SleepQuickActionResult.Applied
source.inSyncGenerationTransaction { db ->
assertEquals(
2,
db.rawQuery(
"SELECT COUNT(*) FROM sleep_medication_intakes WHERE revision_id=" +
"(SELECT current_revision_id FROM sleep_diary_entries WHERE entry_id=?)",
arrayOf(preBed.receipt.entryId),
).use { cursor ->
cursor.moveToFirst()
cursor.getInt(0)
},
)
}
source.close()
source = TrainlogRepository(context, sourceName)
assertEquals(listOf(2, 1), source.listSleepDiary().single().intakes.map { it.quantity })
val bed =
source.quickSleepBedTime("2026-09-23T22:30:00+02:00")
as TrainlogRepository.SleepQuickActionResult.Applied
assertEquals(preBed.receipt.entryId, bed.receipt.entryId)
assertEquals(2, source.listSleepDiary().single().intakes.size)
source.close()
source = TrainlogRepository(context, sourceName)
val firstMeasurement =
ParsedHeartRateMeasurement(
bpm = 61,
@ -120,6 +159,9 @@ class SyncGenerationServiceTest {
source.quickSleepWake("2026-09-24T02:17:00+02:00")
is TrainlogRepository.SleepQuickActionResult.Applied
)
assertEquals(2, source.listSleepDiary().single().intakes.size)
source.close()
source = TrainlogRepository(context, sourceName)
assertTrue(
source.recordLiveHeartRateForActiveSleep(
"Synthetic HR",
@ -131,6 +173,8 @@ class SyncGenerationServiceTest {
source.quickSleepFinalGetUp("2026-09-24T06:31:00+02:00")
is TrainlogRepository.SleepQuickActionResult.Applied
)
source.close()
source = TrainlogRepository(context, sourceName)
assertTrue(source.listSessions().isEmpty())
val night = source.listSleepDiary().single()
@ -140,7 +184,7 @@ class SyncGenerationServiceTest {
.getJSONObject(0)
assertEquals(night.entryId, capture.getString("context_id"))
assertEquals(2, capture.getJSONArray("samples").length())
assertEquals(2, night.intakes.single().quantity)
assertEquals(listOf(2, 1), night.intakes.map { it.quantity })
val service = SyncGenerationService(source)
val captured = service.capture(root, desktopPeer)
@ -154,9 +198,12 @@ class SyncGenerationServiceTest {
val publishedNight = diaryArtifact.getJSONArray("entries").getJSONObject(0)
val publishedCapture = heartArtifact.getJSONArray("captures").getJSONObject(0)
assertEquals(night.entryId, publishedNight.getString("entry_id"))
assertEquals(2, publishedNight.getJSONArray("intakes").length())
assertEquals(
2,
publishedNight.getJSONArray("intakes").getJSONObject(0).getInt("quantity"),
listOf(1, 2),
(0 until publishedNight.getJSONArray("intakes").length()).map { index ->
publishedNight.getJSONArray("intakes").getJSONObject(index).getInt("quantity")
}.sorted(),
)
assertEquals(capture.getString("capture_id"), publishedCapture.getString("capture_id"))
@ -193,13 +240,13 @@ class SyncGenerationServiceTest {
assertEquals(
// The desktop fixture owns one reserved calibration session;
// the empty Android history must not add a workout.
"1|1|2|2|2|1",
"1|2|2,1|2|2|1",
run(
"sqlite3",
desktopDb.absolutePath,
"SELECT (SELECT count(*) FROM sleep_diary_entries WHERE entry_id='${night.entryId}') || '|' || " +
"(SELECT count(*) FROM sleep_medication_intakes WHERE revision_id='${night.revisionId}') || '|' || " +
"(SELECT quantity FROM sleep_medication_intakes WHERE revision_id='${night.revisionId}') || '|' || " +
"(SELECT group_concat(quantity,',') FROM (SELECT quantity FROM sleep_medication_intakes WHERE revision_id='${night.revisionId}' ORDER BY taken_at,intake_id)) || '|' || " +
"(SELECT count(*) FROM heart_rate_samples WHERE capture_id='${capture.getString("capture_id")}') || '|' || " +
"(SELECT count(*) FROM heart_rate_rr_intervals WHERE capture_id='${capture.getString("capture_id")}') || '|' || " +
"(SELECT count(*) FROM sessions);",
@ -207,6 +254,19 @@ class SyncGenerationServiceTest {
.trim(),
)
val staleWithoutIntakes = JSONObject(source.buildSleepDiaryV1Json())
val staleEntry = staleWithoutIntakes.getJSONArray("entries").getJSONObject(0)
staleEntry.put("revision_id", pendingRoot.revisionId)
staleEntry.put("parent_revision_id", JSONObject.NULL)
staleEntry.put("ancestry", org.json.JSONArray().put(pendingRoot.revisionId))
staleEntry.put("updated_at", "2026-09-23T22:00:00+02:00")
staleEntry.put("events", org.json.JSONArray())
staleEntry.put("intakes", org.json.JSONArray())
val staleResult = source.applySleepDiaryV1Json(staleWithoutIntakes.toString())
assertTrue(staleResult is TrainlogRepository.SleepDiaryImportResult.Applied)
assertEquals(1, (staleResult as TrainlogRepository.SleepDiaryImportResult.Applied).unchanged)
assertEquals(listOf(2, 1), source.listSleepDiary().single().intakes.map { it.quantity })
val revised =
source.saveSleepDiary(
SleepDiaryDraft(
@ -255,6 +315,7 @@ class SyncGenerationServiceTest {
)
.trim(),
)
assertEquals(2, source.listSleepDiary().single().intakes.size)
assertTrue(source.listSessions().isEmpty())
} finally {
source.close()

View file

@ -34,6 +34,14 @@ successful intake; the per-unit dose and unit remain independent structured
facts. Pending entries persist through restart and can synchronize without
inventing bedtime.
The persistence regression begins from an empty pending revision, records two
pre-bed intakes (one with quantity two), and reopens the repository after
intake, Couché, Réveil and Levé. It then proves the same stable entry and intake
snapshots through generation, desktop import/ACK, exact replay, rejection of a
stale intake-free ancestor and a later current revision. This is the executable
contract for lifecycle and synchronization retention; it does not infer facts
that are absent from a device database.
Sleep-owned HR capture remains sample-driven. A pending medication alone opens
no capture. After Couché, the first fresh real measurement creates or resumes
one `context_kind=sleep` capture for that entry, with `started_at` equal to the