1
+
Added:
(** Tests for {!Hito_app.Service} — the whole HD flow with no web tier. *)
2
+
Added:
3
+
Added:
open Hito_app
4
+
Added:
module S = Service.Make (Memory_repo)
5
+
Added:
6
+
Added:
let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok"
7
+
Added:
8
+
Added:
let get id =
9
+
Added:
match Exercise.find id with
10
+
Added:
| Some e -> e
11
+
Added:
| None -> Alcotest.failf "catalog is missing %S" id
12
+
Added:
13
+
Added:
let kg n = ok (Units.Weight.of_kg n)
14
+
Added:
let reps n = ok (Units.Reps.of_int n)
15
+
Added:
let at s = Recovery.timestamp_of_unix_seconds s
16
+
Added:
let day n = at (n * 86_400)
17
+
Added:
let ideal = Repository.routine_id "ideal"
18
+
Added:
let service () = S.make ~repo:(Memory_repo.create ())
19
+
Added:
20
+
Added:
let move id load r =
21
+
Added:
Stimulus.Movement.make ~exercise:(get id) ~load:(kg load) ~reps:(reps r)
22
+
Added:
~outcome:Stimulus.Positive_failure
23
+
Added:
24
+
Added:
let single id load r = ok (Stimulus.make (Stimulus.Single (move id load r)))
25
+
Added:
26
+
Added:
let pair ~isolation ~compound =
27
+
Added:
ok (Stimulus.make (Stimulus.Pre_exhaust { isolation; compound }))
28
+
Added:
29
+
Added:
(* HD1's Day 1 as performed. *)
30
+
Added:
let day_one_stimuli =
31
+
Added:
[
32
+
Added:
pair
33
+
Added:
~isolation:(move "dumbbell-flyes" 20. 9)
34
+
Added:
~compound:(move "incline-press" 60. 7);
35
+
Added:
single "laterals" 12. 8;
36
+
Added:
single "bent-over-laterals" 10. 9;
37
+
Added:
pair
38
+
Added:
~isolation:(move "lying-french-press" 30. 8)
39
+
Added:
~compound:(move "dips" 0. 6);
40
+
Added:
]
41
+
Added:
42
+
Added:
let routine_tests =
43
+
Added:
[
44
+
Added:
( "the seeded repository offers HD1's Ideal Routine",
45
+
Added:
`Quick,
46
+
Added:
fun () ->
47
+
Added:
match S.list_routines (service ()) with
48
+
Added:
| [ (_, r) ] ->
49
+
Added:
Alcotest.(check string) "name" "Ideal Routine" (Routine.name r)
50
+
Added:
| rs -> Alcotest.failf "expected one routine, got %d" (List.length rs)
51
+
Added:
);
52
+
Added:
( "an unknown routine is refused",
53
+
Added:
`Quick,
54
+
Added:
fun () ->
55
+
Added:
match
56
+
Added:
S.next_workout (service ()) ~routine:(Repository.routine_id "nope")
57
+
Added:
with
58
+
Added:
| Error S.Unknown_routine -> ()
59
+
Added:
| _ -> Alcotest.fail "expected Unknown_routine" );
60
+
Added:
( "with nothing logged the cycle starts at Day 1",
61
+
Added:
`Quick,
62
+
Added:
fun () ->
63
+
Added:
let w = ok (S.next_workout (service ()) ~routine:ideal) in
64
+
Added:
Alcotest.(check string) "Day 1" "Day 1" (Workout_prescription.name w) );
65
+
Added:
]
66
+
Added:
67
+
Added:
let clearance_tests =
68
+
Added:
[
69
+
Added:
( "a first workout needs no recovery: nothing has been done yet",
70
+
Added:
`Quick,
71
+
Added:
fun () ->
72
+
Added:
let s = service () in
73
+
Added:
Alcotest.(check bool)
74
+
Added:
"ready" true
75
+
Added:
(Recovery.is_ready (ok (S.readiness s ~routine:ideal ~now:(day 1))));
76
+
Added:
Alcotest.(check bool)
77
+
Added:
"starts" true
78
+
Added:
(Result.is_ok (S.begin_workout s ~routine:ideal ~now:(day 1) ())) );
79
+
Added:
( "training too soon after a workout is refused",
80
+
Added:
`Quick,
81
+
Added:
fun () ->
82
+
Added:
let s = service () in
83
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
84
+
Added:
let _ = S.finish s ~ended_at:(day 1) in
85
+
Added:
match S.begin_workout s ~routine:ideal ~now:(day 2) () with
86
+
Added:
| Error (S.Not_recovered readiness) ->
87
+
Added:
Alcotest.(check bool)
88
+
Added:
"and says so" false
89
+
Added:
(Recovery.is_ready readiness)
90
+
Added:
| _ -> Alcotest.fail "expected Not_recovered" );
91
+
Added:
( "once rested, the next workout starts and the cycle has advanced",
92
+
Added:
`Quick,
93
+
Added:
fun () ->
94
+
Added:
let s = service () in
95
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
96
+
Added:
let _ = S.finish s ~ended_at:(day 1) in
97
+
Added:
let entry = ok (S.begin_workout s ~routine:ideal ~now:(day 3) ()) in
98
+
Added:
Alcotest.(check string)
99
+
Added:
"Day 2" "Day 2"
100
+
Added:
(Workout_prescription.name (Entry.prescription entry)) );
101
+
Added:
( "an override is accepted and keeps its reason on the record",
102
+
Added:
`Quick,
103
+
Added:
fun () ->
104
+
Added:
let s = service () in
105
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
106
+
Added:
let _ = S.finish s ~ended_at:(day 1) in
107
+
Added:
let entry =
108
+
Added:
ok
109
+
Added:
(S.begin_workout s ~routine:ideal ~now:(day 2)
110
+
Added:
~override:"travelling tomorrow" ())
111
+
Added:
in
112
+
Added:
match Recovery.basis (Entry.clearance entry) with
113
+
Added:
| Recovery.Overridden { reason; _ } ->
114
+
Added:
Alcotest.(check string) "reason" "travelling tomorrow" reason
115
+
Added:
| Recovery.Recovered -> Alcotest.fail "expected Overridden" );
116
+
Added:
( "an override while genuinely rested is not recorded as one",
117
+
Added:
`Quick,
118
+
Added:
fun () ->
119
+
Added:
let s = service () in
120
+
Added:
let entry =
121
+
Added:
ok
122
+
Added:
(S.begin_workout s ~routine:ideal ~now:(day 1)
123
+
Added:
~override:"just in case" ())
124
+
Added:
in
125
+
Added:
match Recovery.basis (Entry.clearance entry) with
126
+
Added:
| Recovery.Recovered -> ()
127
+
Added:
| Recovery.Overridden _ ->
128
+
Added:
Alcotest.fail "nothing was outstanding to override" );
129
+
Added:
]
130
+
Added:
131
+
Added:
let logging_tests =
132
+
Added:
[
133
+
Added:
( "logging without a workout in progress is refused",
134
+
Added:
`Quick,
135
+
Added:
fun () ->
136
+
Added:
match S.log (service ()) (single "laterals" 12. 8) with
137
+
Added:
| Error S.No_workout_in_progress -> ()
138
+
Added:
| _ -> Alcotest.fail "expected No_workout_in_progress" );
139
+
Added:
( "a stimulus the prescription does not call for is refused",
140
+
Added:
`Quick,
141
+
Added:
fun () ->
142
+
Added:
let s = service () in
143
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
144
+
Added:
match S.log s (single "shrugs" 80. 10) with
145
+
Added:
| Error (S.Rejected (Entry.Not_prescribed _)) -> ()
146
+
Added:
| _ -> Alcotest.fail "expected Rejected Not_prescribed" );
147
+
Added:
( "Day 1 can be logged in full and finished",
148
+
Added:
`Quick,
149
+
Added:
fun () ->
150
+
Added:
let s = service () in
151
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
152
+
Added:
List.iter (fun st -> ignore (ok (S.log s st))) day_one_stimuli;
153
+
Added:
let entry = Option.get (S.in_progress s) in
154
+
Added:
Alcotest.(check int)
155
+
Added:
"four stimuli" 4
156
+
Added:
(List.length (Entry.stimuli entry));
157
+
Added:
Alcotest.(check int)
158
+
Added:
"nothing outstanding" 0
159
+
Added:
(List.length (Entry.unperformed entry));
160
+
Added:
let record = Option.get (S.finish s ~ended_at:(at 3600)) in
161
+
Added:
Alcotest.(check bool)
162
+
Added:
"persisted as finished" true
163
+
Added:
(Entry.is_finished record.Repository.entry);
164
+
Added:
Alcotest.(check bool)
165
+
Added:
"slot cleared" true
166
+
Added:
(Option.is_none (S.in_progress s)) );
167
+
Added:
( "history returns the finished workout",
168
+
Added:
`Quick,
169
+
Added:
fun () ->
170
+
Added:
let s = service () in
171
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
172
+
Added:
let _ = ok (S.log s (single "laterals" 12. 8)) in
173
+
Added:
let _ = S.finish s ~ended_at:(at 3600) in
174
+
Added:
Alcotest.(check int) "one workout" 1 (List.length (S.history s)) );
175
+
Added:
]
176
+
Added:
177
+
Added:
let assessment_tests =
178
+
Added:
[
179
+
Added:
( "evidence accumulates across cycles and feeds progression",
180
+
Added:
`Quick,
181
+
Added:
fun () ->
182
+
Added:
let s = service () in
183
+
Added:
(* The cycle rotates, so laterals — a Day 1 movement — recur only once
184
+
Added:
per three workouts. Log whole cycles and record laterals whenever
185
+
Added:
Day 1 comes round, at an unchanging load. *)
186
+
Added:
let run ~on ~load =
187
+
Added:
let entry =
188
+
Added:
ok (S.begin_workout s ~routine:ideal ~now:on ~override:"fixture" ())
189
+
Added:
in
190
+
Added:
if
191
+
Added:
String.equal "Day 1"
192
+
Added:
(Workout_prescription.name (Entry.prescription entry))
193
+
Added:
then ignore (ok (S.log s (single "laterals" load 8)));
194
+
Added:
ignore (S.finish s ~ended_at:on)
195
+
Added:
in
196
+
Added:
List.iter
197
+
Added:
(fun on -> run ~on ~load:12.)
198
+
Added:
[ day 1; day 3; day 5; day 8; day 10; day 12; day 16 ];
199
+
Added:
(* Laterals seen on days 1, 8 and 16 with no gain: fifteen days without
200
+
Added:
an advance, which is past HD1's two-week threshold. *)
201
+
Added:
Alcotest.(check int)
202
+
Added:
"seven workouts logged" 7
203
+
Added:
(List.length (S.history s));
204
+
Added:
Alcotest.(check bool)
205
+
Added:
"stalled" true
206
+
Added:
(S.progress s (get "laterals") = Ok Progression.Stalled) );
207
+
Added:
( "training on overrides shows up as a diagnostic",
208
+
Added:
`Quick,
209
+
Added:
fun () ->
210
+
Added:
let s = service () in
211
+
Added:
let _ = ok (S.begin_workout s ~routine:ideal ~now:(day 1) ()) in
212
+
Added:
let _ = S.finish s ~ended_at:(day 1) in
213
+
Added:
let _ =
214
+
Added:
ok
215
+
Added:
(S.begin_workout s ~routine:ideal ~now:(day 2) ~override:"impatient"
216
+
Added:
())
217
+
Added:
in
218
+
Added:
let _ = S.finish s ~ended_at:(day 2) in
219
+
Added:
match
220
+
Added:
List.filter
221
+
Added:
(function
222
+
Added:
| Progression.Trained_under_recovered _ -> true | _ -> false)
223
+
Added:
(S.diagnostics s)
224
+
Added:
with
225
+
Added:
| [ Progression.Trained_under_recovered n ] ->
226
+
Added:
Alcotest.(check int) "one such workout" 1 n
227
+
Added:
| _ -> Alcotest.fail "expected the under-recovery diagnostic" );
228
+
Added:
]
229
+
Added:
230
+
Added:
let suite =
231
+
Added:
[
232
+
Added:
("service.routines", routine_tests);
233
+
Added:
("service.clearance", clearance_tests);
234
+
Added:
("service.logging", logging_tests);
235
+
Added:
("service.assessment", assessment_tests);
236
+
Added:
]