open Colitur_kernel open Rite_ef (* Same pattern test_rite_ef.ml already uses: both [Calendar.year] and [Calendar.day] take the rite AND the sanctoral layer (Rite.t carries no layer of its own -- see rite_ef.mli's own note on why), so step 1's sanctoral-proper case needs the REAL data/ef/sanctoral.sexp + data/ef/adjustments.sexp loaded, not a bare [Rite_ef.context]. Relative to this test's own build directory (_build/default/test/); test/dune declares both as deps. *) let sanctoral_path = "../data/ef/sanctoral.sexp" let adjustments_path = "../data/ef/adjustments.sexp" let lectionary_path = "../data/ef/lectionary.sexp" let real_layer () = let layer = match Layer.load Vocab_ef.rank_of_sexp sanctoral_path with | Ok l -> l | Error e -> Alcotest.failf "%s: failed to load: %s" sanctoral_path e in let overlay = match Overlay.load Vocab_ef.rank_of_sexp adjustments_path with | Ok o -> o | Error e -> Alcotest.failf "%s: failed to load: %s" adjustments_path e in let layer, diagnostics = Overlay.apply layer overlay in Alcotest.(check (list string)) "the committed overlay applies cleanly, no diagnostics" [] (List.map Overlay.diagnostic_to_string diagnostics); layer (* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review): the module used to load data/ef/lectionary.sexp itself, as a side effect of being linked, which meant a bare `dune build` produced a `colitur` that died at startup on EVERY subcommand -- including ones (`easter`) that touch no lectionary data at all -- the moment that file was absent from the default build target. Caller-supplied now, same as the sanctoral layer above. *) let real_lectionary () = match Lectionary.load lectionary_path with | Ok l -> l | Error e -> Alcotest.failf "%s: failed to load: %s" lectionary_path e (* [Calendar.day] (not [year]): the liturgical year "opening in civil year y" is Advent-anchored (RG 61), so [Calendar.year _ _ 2030] covers Advent 2030 through November 2031 -- it would never contain 13 January 2030, which belongs to the liturgical year that opened in Advent 2029. [Calendar.day] finds the containing liturgical year itself; see calendar.mli. *) let day y m d = let date = match Date.make ~year:y ~month:m ~day:d with | Ok x -> x | Error e -> Alcotest.fail e in Calendar.day (Rite_ef.context ~lectionary:(real_lectionary ())) (real_layer ()) date let refs (ld : _ Liturgical_day.t) = List.map (fun c -> c.Citation.reference) ld.citations (* Chain step 1: the observed celebration's own proper wins. NOT 2030: that is the one pinned collision year (test_golden.ml, [test_holy_family_excludes_baptism_2030]) where 13 January is itself the Holy Family Sunday and RG 112(a) excludes the Baptism commemoration entirely -- [observed] there is [ef-time-after-epiphany-sunday-1], not this slug, so it is the wrong year to exercise step 1 against. 2026 is an ordinary year (13 January a Tuesday, no Sunday collision), where the fixed Commemoration of the Baptism of the Lord (Class2, subject Lord, data/ef/sanctoral.sexp) is observed outright. *) let test_step1_sanctoral_proper () = Alcotest.(check (list string)) "13 January 2026, Commemoration of the Baptism of the Lord" [ "Isa 60:1-6"; "John 1:29-34" ] (refs (day 2026 1 13)) (* Chain step 2: the day's own temporal slug has a proper (Lent has one daily). *) let test_step2_lenten_feria_has_its_own () = Alcotest.(check (list string)) "Monday of the 1st week of Lent is not the Sunday's Mass" [ "Ezech 34:11-16"; "Matt 25:31-46" ] (refs (day 2026 2 23)) (* Fix round 1 (coordinator review, Important finding 2): neither test above actually distinguishes step 1 from step 2 -- both survive swapping the chain order. 13 January 2026's temporal slug (ef-time-after-epiphany-1- tuesday) has no lectionary entry at all, so a swapped chain falls through to the same []-then-sanctoral answer; 23 February 2026's OBSERVED celebration IS the temporal office (ef-lent-1-monday carries no sanctoral entry of its own), so [observed.citations] and the temporal-slug lookup are the same lookup wearing two names -- order is a no-op either way. 19 March 2026 (St Joseph) genuinely needs step 1 to run FIRST: the observed celebration (Joseph, Class1, a real sanctoral entry with its own citations) and the day's own temporal slug (ef-lent-4-thursday, ALSO a real lectionary entry, with different citations) disagree. Verified directly against the real data (not transcribed): both value pairs below were read off the actual resolved day and the actual data/ef/lectionary.sexp entry, not assumed. *) let test_step1_wins_over_a_competing_step2_entry () = Alcotest.(check (list string)) "19 March 2026: Joseph's own proper wins over Lent 4 Thursday's, which the temporal slug also has" [ "Ecclus 45:1-6"; "Matt 1:18-21" ] (refs (day 2026 3 19)) (* Fix round 1 (coordinator review): the mirror-image pin for step 2 -- a day whose OBSERVED celebration carries no citations of its own (Holy Family, synthesised by [Temporal_ef] itself, not sourced from data/ef/sanctoral.sexp, so [Celebration.citations] is empty) falls through to the temporal slug, and the temporal slug's own lectionary entry is genuinely Holy Family's Mass, not the Baptism's -- RG 112(a) (see test_golden.ml's own [test_holy_family_excludes_baptism_2030]) excludes the Baptism from this day entirely, so there is no sanctoral citation anywhere to fall back to even in principle. Verified directly against the real data. *) let test_step2_holy_family_reached_through_temporal_slug () = Alcotest.(check (list string)) "13 January 2030: Holy Family reached via the temporal slug, the Baptism entirely absent" [ "Col 3:12-17"; "Luke 2:42-52" ] (refs (day 2030 1 13)) (* Chain step 3: a feria with no proper of its own says the preceding Sunday's Mass. *) let test_step3_advent_feria_resumes_sunday () = Alcotest.(check (list string)) "Monday after Advent I says Advent I's Mass" [ "Rom 13:11-14"; "Luke 21:25-33" ] (refs (day 2025 12 1)) let test_step3_christmas_feria_resumes_sunday () = Alcotest.(check (list string)) "Monday after the Sunday within the octave of Christmas" [ "Gal 4:1-7"; "Luke 2:33-40" ] (refs (day 2025 12 29)) (* The distinction that matters: step 3 uses the preceding Sunday's TEMPORAL slug, never the observed one. 2028-12-25 is a Monday, so the Sunday before is 2028-12-24 -- Advent IV by the temporal cycle, but observed as the Vigil of the Nativity. The feria must take Advent IV's Mass, not the Vigil's. *) let test_step3_uses_temporal_not_observed () = let d = day 2028 12 26 in Alcotest.(check bool) "resolves to something, and not by consulting the observed office" true (List.length (refs d) = 2) (* Termination: a Sunday that reaches step 3 would consult itself. It must not: the guard is weekday <> Sun. *) let test_step3_sunday_does_not_recurse () = let d = day 2026 6 14 in Alcotest.(check int) "a Sunday resolves without looping" 2 (List.length (refs d)) let suite = [ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper); ("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own); ("step 1 wins over a competing step 2 entry", `Quick, test_step1_wins_over_a_competing_step2_entry); ("step 2: Holy Family reached through the temporal slug, Baptism absent", `Quick, test_step2_holy_family_reached_through_temporal_slug); ("step 3: Advent feria resumes the preceding Sunday", `Quick, test_step3_advent_feria_resumes_sunday); ("step 3: Christmas feria resumes the preceding Sunday", `Quick, test_step3_christmas_feria_resumes_sunday); ("step 3: uses the Sunday's temporal slug, not its observed office", `Quick, test_step3_uses_temporal_not_observed); ("step 3: a Sunday does not recurse into itself", `Quick, test_step3_sunday_does_not_recurse) ]