test(cardio): prove multi-occurrence program heart-rate ownership

This commit is contained in:
fy59 2026-09-23 19:02:59 +02:00
parent 21f56e3986
commit c8bfb7a759
4 changed files with 72 additions and 54 deletions

View file

@ -75,11 +75,12 @@
- Added one end-to-end tomorrow-session readiness regression. A synchronized
Program session retains its stable session and Program identities through
edits, two explicit exercise intervals, pre-exercise/inter-exercise/active
heart-rate ownership, finalization, full-generation publication, desktop
import, replay and correlated ACK. Raw observation timestamps, BPM, RR,
nullable stable exercise ownership and capture/session bounds are compared
on both platforms without inferring ownership from time overlap.
edits, two ordered Program-derived occurrence intervals,
pre-exercise/inter-exercise/active heart-rate ownership, finalization,
full-generation publication, desktop import, replay and correlated ACK. Raw
observation timestamps, BPM, RR, nullable stable exercise ownership and
capture/session bounds are compared on both platforms without inferring
ownership from time overlap.
- Added factual synchronized heart-rate timelines to the local Web. One bounded C17 read model and loopback GET endpoint now project raw BPM/RR measurements with canonical workout exercise intervals or Sleep Diary/medication markers. Cardio history additionally presents resolved phase targets, instruction changes and calibration peak/recovery facts. History and Sleep reuse one responsive SVG component; full-sample min/max/mean statistics remain exact while only very large chart projections retain bounded bucket extrema. No sleep stage, HRV, readiness or medical inference is added.

View file

@ -4,11 +4,7 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.test.core.app.ApplicationProvider
import com.labfytools.trainlog.model.HeartRateContextKind
import com.labfytools.trainlog.model.NewExerciseProfile
import com.labfytools.trainlog.model.RecordingMode
import com.labfytools.trainlog.model.SessionExerciseDraft
import com.labfytools.trainlog.model.SessionSetDraft
import com.labfytools.trainlog.model.TrackingMode
import java.nio.file.Files
import java.time.OffsetDateTime
import java.util.UUID
@ -67,21 +63,31 @@ class ProgramSessionCardioReadinessTest {
try {
val programId = "pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
val programSessionId = "pgs_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
val programExerciseId = "ex_dddddddd-dddd-4ddd-8ddd-dddddddddddd"
val programExerciseAId = "ex_dddddddd-dddd-4ddd-8ddd-dddddddddddd"
val programExerciseBId = "ex_11111111-1111-4111-8111-111111111111"
val catalog =
JSONObject()
.put("format", "trainlog-pc-catalog")
.put("version", 1)
.put(
"exercises",
JSONArray().put(
JSONObject()
.put("exercise_id", programExerciseId)
.put("name", "Program exercise fixture")
.put("recording_mode", "sets")
.put("tracking_mode", "reps")
.put("data_fields", 0),
),
JSONArray()
.put(
JSONObject()
.put("exercise_id", programExerciseAId)
.put("name", "Program exercise fixture A")
.put("recording_mode", "sets")
.put("tracking_mode", "reps")
.put("data_fields", 0),
)
.put(
JSONObject()
.put("exercise_id", programExerciseBId)
.put("name", "Program exercise fixture B")
.put("recording_mode", "sets")
.put("tracking_mode", "reps")
.put("data_fields", 0),
),
)
.toString()
assertTrue(android.applyPcCatalogJson(catalog) is PcCatalogImportResult.Applied)
@ -110,22 +116,39 @@ class ProgramSessionCardioReadinessTest {
.put("note", JSONObject.NULL)
.put(
"occurrences",
JSONArray().put(
JSONObject()
.put(
"entry_id",
"pge_cccccccc-cccc-4ccc-8ccc-cccccccccccc",
)
.put("exercise_id", programExerciseId)
.put("equipment_id", JSONObject.NULL)
.put("load_mode", "external")
.put("rest_seconds", 90)
.put("target_sets", 1)
.put("target_reps", 8)
.put("target_duration_seconds", JSONObject.NULL)
.put("target_weight_kg", 40.0)
.put("notes", JSONObject.NULL),
),
JSONArray()
.put(
JSONObject()
.put(
"entry_id",
"pge_cccccccc-cccc-4ccc-8ccc-cccccccccccc",
)
.put("exercise_id", programExerciseAId)
.put("equipment_id", JSONObject.NULL)
.put("load_mode", "external")
.put("rest_seconds", 90)
.put("target_sets", 1)
.put("target_reps", 8)
.put("target_duration_seconds", JSONObject.NULL)
.put("target_weight_kg", 40.0)
.put("notes", JSONObject.NULL),
)
.put(
JSONObject()
.put(
"entry_id",
"pge_22222222-2222-4222-8222-222222222222",
)
.put("exercise_id", programExerciseBId)
.put("equipment_id", JSONObject.NULL)
.put("load_mode", "external")
.put("rest_seconds", 60)
.put("target_sets", 1)
.put("target_reps", 10)
.put("target_duration_seconds", JSONObject.NULL)
.put("target_weight_kg", 25.0)
.put("notes", JSONObject.NULL),
),
),
),
)
@ -141,15 +164,6 @@ class ProgramSessionCardioReadinessTest {
ProgramsImportResult.Applied(1, 0, 0),
android.applyProgramsV1Json(programs),
)
val exerciseB =
(android.createExercise(
NewExerciseProfile(
"Readiness exercise B",
RecordingMode.SETS,
TrackingMode.REPS,
0,
),
) as CreateExerciseResult.Created).exercise
assertEquals(
StartProgramSessionResult.Started,
@ -160,12 +174,12 @@ class ProgramSessionCardioReadinessTest {
val sessionStartedAt = checkNotNull(opened.draft.startedAt)
assertEquals(programId, opened.draft.sourceProgramId)
assertEquals(programSessionId, opened.draft.sourceProgramSessionId)
val exerciseA = opened.draft.exercises.single()
val exerciseBEntry =
SessionExerciseDraft(
exercise = exerciseB,
sets = listOf(SessionSetDraft(reps = 10, weightKg = 25.0)),
)
assertEquals(2, opened.draft.exercises.size)
val exerciseA = opened.draft.exercises[0]
val exerciseBEntry = opened.draft.exercises[1]
assertEquals(programExerciseAId, exerciseA.exercise.exerciseId)
assertEquals(programExerciseBId, exerciseBEntry.exercise.exerciseId)
assertTrue(exerciseA.entryId != exerciseBEntry.entryId)
assertEquals(
ActiveDraftMutationResult.Saved,
android.saveActiveSessionDraft(
@ -175,7 +189,9 @@ class ProgramSessionCardioReadinessTest {
exerciseA.copy(
sets = listOf(SessionSetDraft(reps = 8, weightKg = 40.0)),
),
exerciseBEntry,
exerciseBEntry.copy(
sets = listOf(SessionSetDraft(reps = 10, weightKg = 25.0)),
),
),
),
),

View file

@ -157,7 +157,7 @@ parallel implementation of its rules.
| Cardio BPM guidance | `TRAINLOG_CARDIO_BPM_GUIDANCE_V1=PASS`: absolute/calibration-derived immutable BPM targets, 2 BPM hysteresis, 3 s confirmation, stale-signal suspension, durable phase/condition history, Android live guidance UI and strict Android→desktop `trainlog-cardio-guidance` V1 sync |
| Cardio Web visualization | `TRAINLOG_CARDIO_WEB_VISUALIZATION_V1=PASS`: one bounded factual heart-rate timeline read model/HTTP endpoint powers Web History and Sleep with raw BPM/RR, real session/exercise/Sleep markers, guided targets/instructions and calibration facts; no sleep-stage or medical inference |
| 0.1.5 validation | `TRAINLOG_0_1_5_VALIDATION_V1=SOFTWARE_PASS / DEVICE_VISUAL_PASS`: native 111/111; Android 304 tests (299 pass + 5 historical skips) and debug APK build; Web 183/183 + typecheck/build baseline. Private vc46 preserves the live HR capture and samples across Training → Cardio, drives measured guidance, and renders Sommeil and Calibration cardio without mutation. No stable release is authorized by this status. |
| Tomorrow session readiness | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: the private vc46 is clean on schema v31 with fresh CYCPLUS BPM and the next Program session is unambiguous; one combined regression proves Program provenance, factual two-exercise timeline/HR ownership, finalization, full generation, desktop import, replay and correlated ACK |
| Tomorrow session readiness | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: the private vc46 is clean on schema v31 with fresh CYCPLUS BPM and the next Program session is unambiguous; one combined regression proves two ordered occurrences loaded from the synchronized Program, Program provenance, factual timeline/HR ownership, finalization, full generation, desktop import, replay and correlated ACK |
| Current operational cursor | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: tomorrow's recording path is ready; any stable 0.1.5 tag or release remains a separate explicit decision |
| Complete synchronization gap contract | Frozen dependency contract; operational USB/Drive slices required by v0.1.2 are delivered |
| Isolated synchronization test environment | `TRAINLOG_SYNC_TEST_ENV_V1=PASS/FROZEN` |

View file

@ -86,9 +86,10 @@ Tomorrow's next synchronized Program session is unambiguously
private phone remains on Android schema v31 with integrity/FK checks clean and
no active draft, timeline, HR capture, guidance phase or calibration. A single
combined regression now proves Program start and provenance through two
explicit exercise intervals, four HR ownership positions, finalization, full
generation, desktop import, idempotent replay and correlated ACK. This
readiness result does not authorize a stable 0.1.5 tag or release.
ordered occurrences originating in the synchronized Program payload, four HR
ownership positions, finalization, full generation, desktop import, idempotent
replay and correlated ACK. This readiness result does not authorize a stable
0.1.5 tag or release.
The focused `TRAINLOG_SYNC_CAUSAL_DELETE_V1_CLOSEOUT` is complete before this
cursor: complete predecessor state, imported built-in authorization,