aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 02:12:12 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 02:12:12 +0200
commit51f8a25da4f0f939e0a77ce89b5c4b769c6bfe48 (patch)
tree547fed6ce28b8797335d67d523bbcc1489129faf /test
parent633306c8a5ac1854f30749f498498104ebc84edc (diff)
downloadcolitur-51f8a25da4f0f939e0a77ce89b5c4b769c6bfe48.tar.gz
colitur-51f8a25da4f0f939e0a77ce89b5c4b769c6bfe48.zip
test(rite_ef): pin no transfer ever lands inside the Easter octave
Task 11's re-review found that Precedence_ef.transfer_target's Annunciation/Easter condition -- and RG 96's general walk more broadly -- is correct only because Temporal_ef happens to make every day from Easter Sunday through Low Sunday (Easter+0..+7) blocking (Class1/2): Easter and Low Sunday via named, every day between via privileged_feria. Nothing enforces that; a future edit narrowing the octave's blocking range could let a translated feast land inside it, silently. Add a test for the consequence, not the mechanism: over 2005-2050 (real data, the project's own differential window), no day in [Easter, Easter+7] is ever a transfer's target, checked against both transferred_in and transferred_out's own recorded target dates. Plus a direct, deliberately-constructed case that starts the RG 96 search inside Holy Week itself (Holy Thursday 2026, real occupant), pinning that the search lands on Easter+8 -- and a companion test that reads the real occupant's own rank across the whole [origin+1, Easter+7] span to confirm the search genuinely walked through the window rather than stopping early for an unrelated reason. Mutation-verified: narrowing privileged_feria's post-Easter range (6 to 4 days) makes all three new tests fail, including a real natural occurrence the fixture did not anticipate (St Joseph, 19 March, transferring into Easter+5 in 2008/2035/2046) -- not just the deliberately constructed Holy Week case.
Diffstat (limited to 'test')
-rw-r--r--test/test_rite_ef.ml140
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 ] )