diff options
| -rw-r--r-- | test/test_rite_ef.ml | 140 |
1 files changed, 139 insertions, 1 deletions
diff --git a/test/test_rite_ef.ml b/test/test_rite_ef.ml index bfc97dc..c150a6d 100644 --- a/test/test_rite_ef.ml +++ b/test/test_rite_ef.ml @@ -18,7 +18,12 @@ module Date = Colitur_kernel.Date module Date_spec = Colitur_kernel.Date_spec module Cel = Colitur_kernel.Celebration module Colour = Colitur_kernel.Colour +module Subject = Colitur_kernel.Subject +module P = Colitur_kernel.Precedence +module Comp = Colitur_kernel.Computus module V = Rite_ef.Vocab_ef +module T = Rite_ef.Temporal_ef +module PE = Rite_ef.Precedence_ef (* Relative to this test's own build directory (_build/default/test/), same convention test_sanctoral_ef.ml uses -- test/dune declares both as deps @@ -119,9 +124,142 @@ let test_transfer_search_does_not_raise_at_domain_ceiling () = with a recorded reason) -- never silently dropped" true impeder_placed_or_recorded +(* Task 11's re-review finding, closed here: [Precedence_ef.transfer_target]'s + Annunciation/Easter condition (and, more generally, RG 96's whole "not I or + II class" test) is correct only because [Temporal_ef] happens to make + every day from Easter Sunday through Low Sunday (Easter+0..+7) blocking -- + Easter itself and Low Sunday via [named], every day between via + [privileged_feria]. Nothing in the type system enforces that; a future + edit narrowing [privileged_feria]'s Easter-octave range would silently let + the RG 96 walk land a translated feast inside the octave. This tests the + CONSEQUENCE (no day in that window ever receives one), not the mechanism + ([privileged_feria] itself), so it stays sensitive to any way that + consequence could break, not only the one code path that currently + protects it. *) + +let easter_offset (d : Date.t) = + let easter = Comp.gregorian_easter (Date.year d) in + Date.to_rata d - Date.to_rata easter + +let in_easter_octave d = let off = easter_offset d in off >= 0 && off <= 7 + +(* 2005-2050: the project's own differential-testing window (CLAUDE.md), + reused here as a deterministic, non-trivial sample -- 46 liturgical years, + each with several genuine transfers (All Souls onto a Sunday, impeded + universal feasts, and so on; test_precedence_ef.ml's own 21k-day manual + review already confirmed "all 7 slugs that ever transfer have a verified + rubrical cause" over a similar span), so this is not a vacuous sweep over + years where nothing ever transfers. *) +let sample_years = + let rec range a b = if a > b then [] else a :: range (a + 1) b in + range 2005 2050 + +(* Property 1: no day in the resolved output, across the whole sample, is + EVER a transfer's landing point inside [Easter, Easter+7] -- checked two + ways. [transferred_out]'s own recorded target is what [transfer_target] + itself returned (calendar.ml's [assignment], stored verbatim), so this is + the more direct signal; [transferred_in] is also checked, in case some + future Calendar change ever let the two disagree. *) +let test_no_transfer_lands_in_easter_octave () = + let layer = real_layer () in + let violations = ref [] in + List.iter + (fun y -> + let days = Cal.year Rite_ef.context layer y in + Array.iter + (fun (d : (V.season, V.rank) LD.t) -> + (match d.LD.transferred_in with + | Some c when in_easter_octave d.LD.date -> + violations := + (Printf.sprintf "%s transferred_in on %s (Easter+%d)" (slug_of c) + (Date.to_iso8601 d.LD.date) (easter_offset d.LD.date)) + :: !violations + | _ -> ()); + List.iter + (fun (c, target) -> + if in_easter_octave target then + violations := + Printf.sprintf "%s transferred_out from %s to %s (Easter+%d)" (slug_of c) + (Date.to_iso8601 d.LD.date) (Date.to_iso8601 target) (easter_offset target) + :: !violations) + d.LD.transferred_out) + days) + sample_years; + Alcotest.(check (list string)) + "no day in [Easter, Easter+7] is ever a transfer's target, 2005-2050" [] (List.rev !violations) + +(* Property 2, and the LIVE case: [PE.transfer_target] called directly, with + an origin that genuinely starts the search INSIDE Holy Week -- Holy + Thursday 2026 (Easter - 3), a date no real sanctoral entry in + data/ef/sanctoral.sexp occupies (Holy Week carries none), so this is + deliberately constructed, not found. [occupant] is the REAL + [Temporal_ef.temporal] (not a synthetic stand-in), so the search is driven + by the actual blocking shape [privileged_feria] produces, not a + hand-picked one -- this is genuinely live: search_from walks origin+1 + (Good Friday, Easter-2) forward through every remaining day of Holy Week, + all of Easter through Low Sunday (Easter+0..+7, all Class1), and only + stops at Easter+8 (the Monday after Low Sunday), which [named] and + [ferial_rank]/[privileged_feria] agree is Class4 -- confirmed below by + checking the OCCUPANT's own rank there, not asserted blind. Without an + origin inside the window itself, [search_from] could stop before ever + reaching it and this test would prove nothing (the vacuity trap the task + brief names explicitly) -- [test_search_genuinely_enters_the_window] pins + that it does not stop early. *) +let holy_week_origin_2026 = + let easter_2026 = Comp.gregorian_easter 2026 in + Date.add_days easter_2026 (-3) + +let real_occupant d = (T.temporal d).Colitur_kernel.Temporal.office + +let test_transfer_target_skips_the_whole_easter_octave () = + let easter_2026 = Comp.gregorian_easter 2026 in + let c = + { P.cel = + Cel.make ~slug:(Slug.of_string_exn "test-impeded-in-holy-week") ~rank:V.Class1 + ~colour:Colour.White ~subject:Subject.Temporal ~layer:PE.universal_layer (); + origin = P.Sanctoral } + in + let target = PE.transfer_target c holy_week_origin_2026 real_occupant in + Alcotest.(check string) "lands on Easter + 8 (Monday after Low Sunday), past the entire octave" + (Date.to_iso8601 (Date.add_days easter_2026 8)) (Date.to_iso8601 target); + Alcotest.(check bool) "strictly after origin (rite.mli's own obligation)" true + (Date.compare target holy_week_origin_2026 > 0); + Alcotest.(check bool) "not inside [Easter, Easter+7]" false + (let off = Date.to_rata target - Date.to_rata easter_2026 in + off >= 0 && off <= 7) + +(* The vacuity check itself: proves the search genuinely walked THROUGH the + window rather than [search_from] having some other reason to stop before + it (e.g. an off-by-one that happened to also land past the octave). Reads + the real occupant's own rank at every day from the origin through + Easter+7 and requires every one of them to be blocking (Class1 or + Class2) -- if any single one were not, [search_from] would have stopped + there instead of at Easter+8, and the test above would be passing for the + wrong reason. *) +let test_search_genuinely_enters_the_window () = + let easter_2026 = Comp.gregorian_easter 2026 in + let rec days_from a b = if Date.compare a b > 0 then [] else a :: days_from (Date.add_days a 1) b in + let walked = days_from (Date.add_days holy_week_origin_2026 1) (Date.add_days easter_2026 7) in + Alcotest.(check bool) + "every day from origin+1 through Easter+7 (the whole span search_from must cross) is blocking" + true + (List.for_all + (fun d -> + match (real_occupant d).Cel.rank with V.Class1 | V.Class2 -> true | V.Class3 | V.Class4 -> false) + walked); + Alcotest.(check bool) + "the walked span is at least 8 days -- the octave alone (Easter..Easter+7), not a one-day hop" + true (List.length walked >= 8) + let suite = ( "Rite_ef (real data: overlay-in-effect, domain-ceiling)", [ Alcotest.test_case "the overlay suppression is observably in effect" `Quick test_vigil_of_christmas_suppressed; Alcotest.test_case "RG96 search does not raise at the domain ceiling (real data)" `Quick - test_transfer_search_does_not_raise_at_domain_ceiling ] ) + test_transfer_search_does_not_raise_at_domain_ceiling; + Alcotest.test_case "no transfer ever lands inside [Easter, Easter+7], 2005-2050" `Quick + test_no_transfer_lands_in_easter_octave; + Alcotest.test_case "transfer_target skips the whole Easter octave from inside Holy Week" `Quick + test_transfer_target_skips_the_whole_easter_octave; + Alcotest.test_case "the search genuinely enters the window (not vacuous)" `Quick + test_search_genuinely_enters_the_window ] ) |
