From d77c9ca68fe0aabe7ce3718849e2dcf3ac436e43 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Sat, 29 Aug 2026 10:27:39 -0400 Subject: [PATCH] Add regression tests for Decollation's ferial-numbers/proper-antiphons split Confirms 2026-08-29 Lauds and 2026-08-28 First Vespers both keep the ferial psalm numbers (Duplex, below duplex-majus) while still layering on the feast's own proper antiphons -- and that an override group with no matching ferial slot (Puellae saltanti / Arguebat) is dropped, not substituted in. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX --- tests/hours/lauds.test.ts | 38 +++++++++++++++++++++++++++++++++++++ tests/hours/vespers.test.ts | 26 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tests/hours/lauds.test.ts b/tests/hours/lauds.test.ts index dd98361..85deec6 100644 --- a/tests/hours/lauds.test.ts +++ b/tests/hours/lauds.test.ts @@ -680,6 +680,44 @@ describe('resolveOrdo("lauds", ...)', () => { const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Auróra lucis rútilat'); }); + + it("keeps Saturday's ferial psalm numbers for the Beheading of St. John the Baptist (Duplex, below duplex-majus), while still using his own proper antiphons", () => { + // Regression test: decollation-of-st-john-baptist.yml's own psalm + // numbers (92/99/62) used to leak in below duplex-majus once + // getPsalmodyProperOverrideId (f3cf30c) made the antiphon tier + // rank-agnostic -- the psalm numbers themselves must stay gated at + // duplex-majus+ and fall back to Saturday's ferial cycle (50, split + // 142 canticle) instead, with the proper antiphons layered on top by + // group index. + const ordo = resolveOrdo('lauds', '2026-08-29'); + const psalmNumbers = ordo.parts + .filter((p) => p.kind === 'psalm') + .map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); + expect(psalmNumbers).toEqual([66, 50, 142, 148, 149, 150]); + + const ps50 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 50); + expect(ps50?.kind === 'psalm' ? ps50.antiphon?.text.la : undefined).toBe( + 'Ant. Heródes enim ténuit * et ligávit Joánnem et pósuit in cárcerem propter Herodíadem.', + ); + const ps142 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 142); + expect(ps142?.kind === 'psalm' ? ps142.antiphon?.text.la : undefined).toBe( + 'Ant. Dómine mi rex, * da mihi in disco caput Joánnis Baptístæ.', + ); + + // decollation's own third group (Ps 62, "Puéllæ saltánti") has no + // matching slot in Saturday's own 2-group ferial default and is + // simply unused -- confirmed with the user 2026-08-29. + expect(psalmNumbers).not.toContain(62); + const puellaeSaltanti = ordo.parts.some( + (p) => p.kind === 'antiphon' && p.text.text.la?.includes('Puéllæ saltánti'), + ); + expect(puellaeSaltanti).toBe(false); + + const laudate = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 148); + expect(laudate?.kind === 'psalm' ? laudate.antiphon?.text.la : undefined).toBe( + 'Ant. Da mihi in disco * caput Joánnis Baptístæ. Et contristátus est rex propter jusjurándum.', + ); + }); }); describe('resolveOrdo("lauds", ...) responsory Gloria omission', () => { diff --git a/tests/hours/vespers.test.ts b/tests/hours/vespers.test.ts index 2457fdc..00bf1b0 100644 --- a/tests/hours/vespers.test.ts +++ b/tests/hours/vespers.test.ts @@ -487,6 +487,32 @@ describe('resolveOrdo("vespers", ...)', () => { expect(psalms.some((p) => p.antiphon?.text.la?.includes('Cóllocet eum'))).toBe(false); }); + it("keeps Friday's ferial psalm numbers for the Beheading of St. John the Baptist's First Vespers (Duplex, below duplex-majus), while still using his own proper antiphons", () => { + // Regression test, mirrors the equivalent Lauds test: decollation-of- + // st-john-baptist.yml's own psalm numbers (109/111/112/115) must stay + // gated at duplex-majus+ and fall back to the ferial cycle for the + // evening's own weekday (2026-08-28 is a Friday) instead, with the + // proper's antiphons layered on top by group index. His 4th group + // ("Arguébat", keyed to Ps 115) has no matching slot in Friday's own + // 3-group ferial default and is simply unused. + const ordo = resolveOrdo('vespers', '2026-08-28'); + const psalmNumbers = ordo.parts + .filter((p) => p.kind === 'psalm') + .map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); + expect(psalmNumbers).not.toEqual(expect.arrayContaining([109, 111, 112, 115])); + + const psalms = ordo.parts.filter((p) => p.kind === 'psalm') as { + psalmNumber: number; + antiphon?: { text: Record }; + }[]; + expect(psalms[0]?.antiphon?.text.la).toBe( + 'Ant. Heródes enim ténuit * et ligávit Joánnem et pósuit in cárcerem propter Herodíadem.', + ); + + const arguebat = ordo.parts.some((p) => p.kind === 'antiphon' && p.text.text.la?.includes('Arguébat')); + expect(arguebat).toBe(false); + }); + it("no longer gives St. Margaret of Scotland First Vespers now that Corpus Christi's own octave is modeled", () => { // St. Margaret of Scotland (Jun 10, Semiduplex, common-of-a-widow -- // content itself live-verified 2026-08-26 to be real, identical to