(** Tests for {!Hito_persistence.Import_codec} — the length-prefixed framing for import batches and promoted records. The framing must preserve arbitrary bytes in every free-text field, and a decode must rebuild the batch and its promotion exactly, so {!Already_promoted} and edited re-promotion survive a reconnect. *) open Hito_app open Hito_persistence module WI = Workout_import let get id = match Exercise.find_id id with | Some e -> e | None -> Alcotest.failf "catalog is missing %S" id let at s = Recovery.timestamp_of_unix_seconds s let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok" let find_exercise (id : Exercise.id) = Exercise.find_id (id :> string) let wid = WI.workout_id (* Awkward text: commas, newlines, colons, and non-ASCII UTF-8. A length-prefixed field must carry every byte verbatim. *) let nasty = "a,b\nc:d\t\"quote\" — café 🏋️, 12:corrupt\n99:end" let set ?(row = 0) ?(set_index = 1) ?(superset_id = None) ~name ~kind ~kg ~reps () : WI.source_set = { row; exercise_name = name; set_index; set_type = kind; weight_kg = kg; reps; superset_id; } (* A batch with a failure set and a normal set, both under one awkward exercise name, plus an awkward warning and awkward source text. *) let sample_batch () = let sets = [ set ~row:1 ~name:nasty ~kind:WI.Failure ~kg:102.5 ~reps:8 (); set ~row:2 ~name:nasty ~kind:WI.Normal ~kg:60.0 ~reps:10 ~superset_id:(Some 3) (); ] in let w = ok (WI.make_workout ~id:(wid "w1") ~title:nasty ~started_at:(at 1_000) ~ended_at:(at 4_600) ~sets) in let warnings = [ { WI.row = 7; exercise_name = Some nasty; kind = WI.Unsupported_row nasty; }; ] in WI.make_batch ~id:(WI.batch_id "b1") ~source:nasty ~fingerprint:"fp-🔒" ~imported_at:(at 5_000) ~workouts:[ w ] ~warnings (* Map, acknowledge, and promote, so the snapshot carries a promotion. *) let promoted_batch () = let b = sample_batch () in let b = ok (WI.map_exercise b ~workout:(wid "w1") ~source_name:nasty (get "squats")) in let b = ok (WI.confirm_normal_sets b ~workout:(wid "w1") ~acknowledgement:nasty) in let b, _ = ok (WI.promote_workout b (wid "w1")) in b let roundtrip_batch b = let encoded = Import_codec.encode_snapshot (WI.snapshot b) in match Import_codec.decode_snapshot ~find_exercise encoded with | Ok b' -> b' | Error e -> Alcotest.failf "decode failed: %a" Import_codec.pp_error e let codec_tests = [ ( "an unpromoted batch round-trips, preserving special characters", `Quick, fun () -> let b = sample_batch () in let d = roundtrip_batch b in Alcotest.(check string) "source verbatim" (WI.batch_source b) (WI.batch_source d); Alcotest.(check string) "fingerprint verbatim" (WI.batch_fingerprint b) (WI.batch_fingerprint d); Alcotest.(check int) "imported_at seconds" (Recovery.timestamp_to_unix_seconds (WI.batch_imported_at b)) (Recovery.timestamp_to_unix_seconds (WI.batch_imported_at d)); let w = Option.get (WI.find_workout d (wid "w1")) in Alcotest.(check string) "title verbatim" nasty (WI.workout_title w); Alcotest.(check int) "two sets" 2 (List.length (WI.workout_sets w)); match WI.batch_warnings d with | [ { kind = WI.Unsupported_row detail; exercise_name = Some name; _ } ] -> Alcotest.(check string) "warning detail verbatim" nasty detail; Alcotest.(check string) "warning name verbatim" nasty name | _ -> Alcotest.fail "expected one unsupported-row warning" ); ( "a mapping round-trips and stays confirmed", `Quick, fun () -> let b = sample_batch () in let b = ok (WI.map_exercise b ~workout:(wid "w1") ~source_name:nasty (get "squats")) in let d = roundtrip_batch b in let w = Option.get (WI.find_workout d (wid "w1")) in match WI.workout_mapping w ~source_name:nasty with | Some e -> Alcotest.(check bool) "mapped to squats" true (Exercise.equal e (get "squats")) | None -> Alcotest.fail "expected the mapping to survive" ); ( "a promotion round-trips, preserving acknowledgement and revision", `Quick, fun () -> let b = promoted_batch () in let d = roundtrip_batch b in let w = Option.get (WI.find_workout d (wid "w1")) in Alcotest.(check string) "acknowledgement verbatim" nasty (Option.get (WI.workout_acknowledgement w)); (* Re-promoting the decoded batch at the same revision is idempotent: the promotion survived the round-trip. *) let _, result = ok (WI.promote_workout d (wid "w1")) in match result with | WI.Already_promoted p -> Alcotest.(check int) "two observations" 2 (List.length p.observations); Alcotest.(check (option string)) "normal confirmation verbatim" (Some nasty) p.provenance.normal_confirmation | WI.Promoted _ -> Alcotest.fail "expected Already_promoted, not a fresh promotion" | WI.Blocked _ -> Alcotest.fail "expected Already_promoted, not Blocked" ); ( "an unknown exercise id in a snapshot is reported, not raised", `Quick, fun () -> let b = promoted_batch () in let encoded = Import_codec.encode_snapshot (WI.snapshot b) in (* Resolve nothing: every id is unknown to this catalog view. *) match Import_codec.decode_snapshot ~find_exercise:(fun _ -> None) encoded with | Error (Import_codec.Unknown_exercise _) -> () | _ -> Alcotest.fail "expected Unknown_exercise" ); ( "truncated framing is reported as malformed", `Quick, fun () -> match Import_codec.decode_snapshot ~find_exercise "100:not-that-long" with | Error (Import_codec.Malformed _) -> () | _ -> Alcotest.fail "expected Malformed" ); ] (* A promoted record built straight from a promotion, for the record codec. *) let sample_record () : Repository.imported_record = let b = promoted_batch () in match WI.snapshot b with | { workouts = [ { promotion = Some p; id; _ } ]; batch_id; _ } -> let observations = List.map (fun (o : WI.observation_snapshot) -> let exercise = get (o.exercise_id :> string) in let effort = Result.get_ok (Evidence.Stimulus.Effort.make ~exercise ~load:o.load ~reps:o.reps ~outcome:Evidence.Stimulus.Positive_failure) in Evidence.Observation.make ~exercise ~effort ~performed_at:o.performed_at) p.observations in { batch_id; workout_id = id; revision = p.revision; title = p.title; started_at = p.started_at; ended_at = p.ended_at; observations; provenance = { WI.batch_id; workout_id = id; source_rows = p.source_rows; normal_confirmation = p.normal_confirmation; revision = p.revision; }; } | _ -> Alcotest.fail "expected one promoted workout in the snapshot" let record_tests = [ ( "a promoted record round-trips, preserving special characters", `Quick, fun () -> let r = sample_record () in let encoded = Import_codec.encode_record r in match Import_codec.decode_record encoded with | Ok d -> Alcotest.(check string) "title verbatim" nasty d.Repository.title; Alcotest.(check int) "revision" r.revision d.revision; Alcotest.(check int) "same observation count" (List.length r.observations) (List.length d.observations); Alcotest.(check (option string)) "normal confirmation verbatim" (Some nasty) d.provenance.normal_confirmation; Alcotest.(check (list int)) "source rows" r.provenance.source_rows d.provenance.source_rows | Error e -> Alcotest.failf "decode failed: %a" Import_codec.pp_error e ); ( "truncated record framing is reported, not raised", `Quick, fun () -> let r = sample_record () in let encoded = Import_codec.encode_record r in (* Drop the final byte: the framing no longer lines up. *) let truncated = String.sub encoded 0 (String.length encoded - 1) in match Import_codec.decode_record truncated with | Error _ -> () | Ok _ -> Alcotest.fail "expected a decode error on truncated text" ); ] let suite = [ ("import_codec", codec_tests); ("import_record", record_tests) ]