test(cardio): prove multi-occurrence program heart-rate ownership
This commit is contained in:
parent
21f56e3986
commit
c8bfb7a759
4 changed files with 72 additions and 54 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue