trainlog/docs/tests.md

479 lines
17 KiB
Markdown

# Tests and validation
## 1. Principle
A Trainlog feature is not complete without relevant validation.
Frozen formats, persistence migrations, synchronization semantics, and user-data
mutations require executable coverage where practical.
## 2. Frozen Trainlog JSON v1
Run:
```bash
python tools/validate_json.py
```
It validates:
- `examples/session-v1.json`;
- all positive fixtures;
- all negative fixtures.
A negative fixture passes only when Trainlog rejects it.
Semantic validation includes:
```text
stable-ID uniqueness
normalized exercise-name uniqueness
timestamp offsets
end > start
catalog/reference equality
tracking-mode consistency
load-mode/weight consistency
unknown-field rejection
non-blank notes
zero actual repetitions allowed
```
## 3. Import reconciliation contract
Run:
```bash
python tools/validate_import_contract.py
```
Coverage includes:
```text
same ID + same name/profile -> reuse
same ID + renamed display text -> controlled reuse/warning
same ID + incompatible mode -> reject
different ID + equivalent normalized name -> reject
new unique identity -> create
```
That command validates the frozen Trainlog JSON V1 importer contract. Active
mobile V2 synchronization has a separate, stricter safe-reconciliation policy
covered below; it does not modify the frozen V1 expectation.
## 4. Desktop Meson suite
Current registered set (Meson executes tests in parallel, so displayed order is
not contractual):
```text
database
catalog
equipment_catalog
custom_equipment
session_detail
duration
body_metrics
bodyviz
exercise_performance
session_type_schema
session_edit
body_observation_edit
usb
mtp
exercise_profile_schema
continuous_session
continuous_detail
reps
variable_sets
schema_v5_migration
schema_v9_migration
schema_v7_migration
mobile_import_variable_sets
mobile_import_multi_occurrence
equipment_associations_exchange
equipment_definitions_exchange
exercise_reconciliation
sync_direction
sync_history
sync_screen_action
measured_max
max_results
max_sync
body_analytics
terminal_input_event_type_policy
tui_workflows
```
Validated current suite:
```text
36/36 Meson tests PASS
```
The desktop executable is additionally smoke-checked in isolated tmux PTYs at
100x30, the exact 72x20 minimum, and the 60x15 fallback; a resize down/up must
recover before a clean keyboard quit. Notcurses is verified as the executable's
direct terminal dependency with `readelf -d`.
Notable regression coverage:
- transactional persisted-session replacement;
- exercise removal from a session;
- body-observation stable-identity editing;
- profile-aware exercise constraints;
- continuous activity without fake sets;
- table-only desktop SETS workflow: planning does not create actual rows,
explicit add requires an actual metric, and normal zero-row completion is
rejected; Android legacy compact-draft decoding remains separately covered;
- direct v4 -> v7 database migration and v7 -> v8 custom-equipment migration;
- bounded v8 -> v9 explicit-max migration, including ambiguous-attempt preservation;
- lossless v9 -> v10 `performed_sets` rebuild: historic NULL and positive
weights/IDs/owners/positions/metrics survive, explicit zero is accepted, and
injected failure rolls back with integrity, foreign-key and enforcement
checks;
- heterogeneous mobile-set import;
- per-set load persistence and correction: ordered rows retain mixed actual
repetitions, nullable loads, positions and assistance semantics through
desktop replacement and detail retrieval;
- V2 mobile round-trip and idempotent replay preserve each ordered set's own
nullable `weight_kg`, without collapsing it to an occurrence target;
- V2 explicit-max Android -> desktop -> Android replay and resumed same-session update;
- Notcurses input lifecycle translation: PRESS/REPEAT are actionable while a
RELEASE event is consumed without creating a second navigation action.
- targetless mobile SETS persistence;
- mobile-import idempotence.
- V2 mobile import with repeated exercise occurrences and stable `entry_id`;
- companion equipment import after session import, including idempotent
reimport and independent associations for repeated occurrences;
- rejection of the historical equipment-import invocation without its required
`--database` target, followed by the corrected complete V2 export chain.
- stable-ID mobile-to-desktop rename reconciliation without duplicate catalog
rows or historical-reference replacement.
- supplied catalogue search/detail, local custom-equipment creation and
selection, resolution of supplied/local occurrence links, and explicit
visibility of an unknown historic equipment reference;
- strict custom-equipment definitions V1 import/export: reserved supplied IDs,
additive omission, equal reimport, and divergent same-ID conflict;
- three-mode shared-engine plans and their directional receive/publish bounds;
- Sync-page action dispatch: direct `a`/`p`/`b` confirmation, one `Enter` run,
`Esc` cancellation without a run, refresh-only `r`, inert retired `s`, and
equivalent full/compact footer direction labels;
- definition-first ordering before V2 artifacts, so associations can resolve
custom IDs without changing V2 shapes.
- exercise reconciliation with identical profiles, compatible subset/superset
masks, explicit incomparable-profile rejection, current Marche identities,
preservation of two historic occurrences and their `entry_id`/continuous
values, ordered custom-equipment definitions/catalog/mobile/association
replay, complete definitions/mobile/associations/body import, outbound
publication, and stable second replay;
- a production PC-exporter to Android-importer regression over definitions,
catalog, mobile V2, and association V2 artifacts: the fixture includes
Marche, Leg press, two Marche occurrences, per-set loads, body data, a
durable draft, and custom equipment; after the first import, both the second
and third imports report zero additions and an exact snapshot of every
Android business table remains unchanged;
- synchronization summary reporting where a reconciliation-only result keeps
`+0 exercice(s)` and only an actual inserted row produces `+1`;
- all fourteen body metrics through the same complete reconciliation import;
- desktop-created occurrence identities use `sxe_<uuid-v4>`, not the `sy_`
synchronization-run prefix.
## 5. Build
```bash
meson setup --reconfigure build
meson compile -C build
meson test -C build --print-errorlogs
```
Strict warning flags remain active. Do not weaken warnings to make a change pass.
## 6. Android build
When Android code changes:
```bash
cd android
printf 'sdk.dir=%s\n' "$HOME/Android/Sdk" > local.properties
JAVA_HOME=/usr/lib/jvm/java-17-openjdk \
./gradlew testDebugUnitTest assembleDebug
```
Install to the connected device when hardware behavior changes:
```bash
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
Android repository host tests additionally cover exercise editing:
- trimmed rename preserves `exercise_id` and recalculates `normalized_name`;
- duplicate and invalid names are rejected;
- completed history and active-draft references resolve the renamed catalog row;
- a referenced profile change is explicitly rejected;
- same-ID PC-catalog rename reconciles in place without a duplicate.
## 7. Hardware MTP validation
Hardware probes and real synchronization are separate from the normal automated
suite because a test runner cannot assume an unlocked MTP phone.
Available probe binaries include:
```text
trainlog-usb-probe
trainlog-mtp-probe
trainlog-mtp-exchange-probe
trainlog-mtp-roundtrip-probe
trainlog-mtp-mobile-export-probe
```
Current physical baseline:
```text
USB_MTP_DETECTION=PASS
MTP_STORAGE_ACCESS=PASS
MTP_WRITE=PASS
MTP_LIST_FOLDER=PASS
MTP_READ=PASS
MTP_ROUNDTRIP=PASS
```
The automated `sync_direction` regression also covers selection of the newest
Android V2 export, equipment-definitions V1 artifact, and equipment-association
V2 artifact when scoped storage has retained collision-suffixed sibling files.
It rejects non-versioned/
non-numeric names and uses deterministic ties; JSON validation remains part of
the normal import path. `mobile_import_multi_occurrence` exercises the real
definition importer before the V2 session importer, including idempotent
reimport and rejection of a missing custom definition.
## 8. Bidirectional synchronization validation
Validated workflow:
```text
Android
-> Synchroniser maintenant
-> unique sr_ request
-> trainlog-syncd
-> shared engine
-> Android -> PC import
-> PC -> Android catalog
-> sy_ structured run
-> matching receipt
-> Android final status
```
Multiple distinct Android request IDs were processed successfully without
reprocessing one request as a new one.
The TUI also invokes the same engine manually and exposes structured run
details.
## 9. Sanitizers
For meaningful C checkpoints:
```bash
CC=clang meson setup build-asan \
-Db_sanitize=address,undefined \
-Db_lundef=false
meson compile -C build-asan
meson test -C build-asan --print-errorlogs
```
## 10. Pre-push checklist
```bash
meson compile -C build
meson test -C build --print-errorlogs
python tools/validate_json.py
python tools/validate_import_contract.py
git diff --check
git status --short
```
When Android changed, add:
```bash
cd android
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleDebug
```
Documentation must describe the resulting state, not retain contradictory old
`NEXT` checkpoints.
## 11. Measured-max regression
The normal Meson suite contains:
```text
measured_max
```
Coverage proves:
- stronger ordinary training is ignored by measured-max classification;
- only `max_test` sessions participate;
- zero-repetition failed attempts are not promoted;
- newest successful explicit test is the current measurement;
- historical external-load record can remain older than current;
- lower assistance is better;
- no-load max tests compare actual reps/duration;
- external working loads round to the configured increment;
- working-load percentages reject assistance.
Validated current normal suite:
```text
36/36 Meson tests PASS
```
## 12. Body analytics regression
The Meson suite adds:
```text
body_analytics
```
Coverage includes:
- male circumference-formula branch;
- female circumference-formula branch;
- estimated fat and lean mass from real body weight;
- waist/hip, shoulder/waist, and chest/waist ratios;
- left/right asymmetry;
- profile-independent analytics without a configured estimation profile;
- missing required circumference handling;
- invalid estimation-profile rejection.
Validated current normal suite:
```text
36/36 Meson tests PASS
```
## 13. Android session draft v1
Android schema v4 introduced one durable active draft; the current additive
chain reaches schema v9 without clearing completed history or the draft. The
current `testDebugUnitTest` suite and `assembleDebug` pass. Host coverage
includes exercise
shapes and raw partial text, fresh repository restore, remove/discard, atomic
finalization and repeated-finalize rejection, rollback, catalog reconciliation,
missing-selection recovery, explicit DB-open failure, historical migration,
equipment selection and occurrence identity.
Schema-v9 host coverage additionally proves French raw max-text persistence,
explicit max creation/edit/finalization without sets, distinct movement values
on the same equipment, latest-per-exercise history, V2 replay, stable-ID resume
and bounded conversion that leaves multiple legacy attempts untouched.
The current set-row-editor coverage additionally proves that row edits,
deletion and append preserve neighbouring rows; French-comma loads, blank
loads and explicit zero loads remain distinct; reopening preserves aligned raw
row fields; and completed history keeps the ordered per-set values. The
instrumentation source exercises row edit, deletion, append and Activity
recreation, but this document does not claim that instrumentation was executed
for the current documentation checkpoint.
For the settled implementation, the validation inventory is:
```text
Android JVM: ./gradlew testDebugUnitTest
Android compilation: ./gradlew assembleDebug
Android instrumentation: adb shell am instrument ... (execution is explicit;
no device execution is asserted here)
Desktop: meson compile -C build
meson test -C build --print-errorlogs
Frozen JSON/import: python tools/validate_json.py
python tools/validate_import_contract.py
V2 regression: mobile_import_variable_sets and the desktop/Android
V2 round-trip/idempotent-replay coverage
Sanitizers: clang ASan/UBSan Meson build and test invocation
```
Executed postrepair evidence is: 37 Android unit tests, `assembleDebug`, 36/36
Meson tests, valid and invalid JSON checks, import-contract 6/6, and 14/14
ASan/UBSan Meson tests. No device, installation, real-store migration, or
instrumentation execution is asserted by this checkpoint.
```bash
cd android
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew test
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleDebug assembleDebugAndroidTest
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
adb shell am instrument -w -e package com.labfytools.trainlog \
com.labfytools.trainlog.test/androidx.test.runner.AndroidJUnitRunner
```
The following device instrumentation evidence is a prior baseline, not a
blanket device-validation claim for schema-v8 definitions or three-mode sync.
That suite has **5 tests**: two real-SQLite repository
checks and three production-screen Compose UI checks. Coverage includes exact
raw form restoration through Activity recreation, cancellation and confirmation
of discard, final save with no stale Resume after recreation, and refusal of a
second completion. All tests use isolated databases; the debug-only,
non-exported Activity renders production Home/Session screens without exporting
synthetic data. The phone must be unlocked and interactive. Do not interpret a
locked-screen `No compose hierarchies found` failure as a passing UI check.
The Samsung SM_G990B additionally passed the normal `MainActivity` matrix:
- two real catalog exercises with distinct continuous values survived Home,
another app, background `am kill` with verified PID exit, and force-stop;
- rotation recreated the Activity and Home Resume restored both exercises;
- removal of one exercise survived process death, with the other intact;
- raw duration/speed text survived process death and force-stop exactly;
- Back and the normal new-session action preserved the existing draft;
- discard cancellation preserved the DB exactly; confirmation and relaunch
left all draft tables empty and no Resume action;
- completed History and mobile export excluded the populated draft;
- every original domain row survived migration and the entire device matrix.
Final-save UI and repeated-finalization tests ran on-device with isolated data;
no fictitious completed session was added to the user's history. Real migration
and final state passed SQLite integrity/foreign-key checks. The pre-upgrade DB
and preferences backup is outside the repository. No uninstall, package-data
clear, desktop schema change or frozen artifact change is part of this repair.
## 14. Current real-data reconciliation validation
Before mutation, coherent desktop and Android v8 copies were created and passed
`PRAGMA integrity_check` plus `PRAGMA foreign_key_check`. The newest real
Android V2 artifacts were selected, including scoped-storage `(N)` siblings.
The production tools then ran in protocol order on the desktop copy:
```text
equipment definitions -> exercise/session/body V2 -> equipment associations
-> PC definitions/catalog/mobile/associations export -> second import
```
The result retained the desktop Marche identity with catalog
`data_fields = 3`, both older speed-only Marche occurrences, the Android
speed-and-distance occurrence, all original `entry_id`/positions, custom
equipment, set weights and body observations. The retired Android Marche ID had
no remaining desktop catalog/reference owner. The second replay changed no row
counts and both integrity checks remained clean.
This exercises real artifacts and production import/export code on copies. It
does not substitute for a final direct-MTP run when USB ownership or sandbox
permissions prevent libmtp access.
For this checkpoint, the USB probe discovered the connected Samsung interface
but libmtp failed at `libusb_open()`. The canonical desktop DB was also
read-only to the sandbox; the first attempted definitions import failed before
mutation, and a repeated dump hash plus integrity/FK checks proved it unchanged.
The Android package remained force-stopped and ADB was used only for read-only
inspection/pull. Applying the validated state to both real stores and running
the two transport directions remains an out-of-sandbox hardware validation.
Detailed retained evidence: [Android draft execution record](reviews/android_session_draft_v1_resume.md).