import { describe, expect, it } from 'vitest'; import { resolveOrdo } from '../../src/hours'; // Clean ferial dates, one per weekday — see data/psalter-distribution.yml // and src/hours/lauds.ts's own generation-script provenance comments for // why these particular dates. const FERIAL_SUNDAY = '2026-06-21'; const FERIAL_MONDAY = '2026-06-08'; // not 08-31 -- St. Raymond Nonnatus's own day (common-of-a-confessor) const FERIAL_TUESDAY = '2026-06-16'; const LAWRENCE_OCTAVE_DAY = '2026-08-11'; // Semiduplex -- exercises isDoubleOrHigher's false branch differently than a plain feria only in rank, not in psalms (Commune override not modeled, see below) describe('resolveOrdo("lauds", ...)', () => { it('is implemented', () => { expect(resolveOrdo('lauds', FERIAL_TUESDAY).notImplemented).toBeUndefined(); }); it('opens with Ps 66 and gives it no antiphon at all, unlike every other psalm here', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const psalms = ordo.parts.filter((p) => p.kind === 'psalm'); expect(psalms[0]?.kind === 'psalm' ? psalms[0].psalmNumber : undefined).toBe(66); expect(psalms[0]?.kind === 'psalm' ? psalms[0].antiphon : 'should be undefined').toBeUndefined(); }); it("resolves Tuesday's weekday psalmody: Ps 50, 42, 56, the Canticle of Ezechias, then 148-150", () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const psalmNumbers = ordo.parts .filter((p) => p.kind === 'psalm') .map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); expect(psalmNumbers).toEqual([66, 50, 42, 56, 148, 149, 150]); const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus'); expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-ezechiae'); expect(canticle?.kind === 'canticle' ? canticle.text.text.la : undefined).toContain('In dimídio diérum meórum'); }); it("gives each variable psalm group its own opening antiphon (incipit, below Double) and a full repeat right after", () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const ps50Index = ordo.parts.findIndex((p) => p.kind === 'psalm' && p.psalmNumber === 50); const ps50 = ordo.parts[ps50Index]; expect(ps50?.kind === 'psalm' ? ps50.antiphon?.text.la : undefined).toBe('Ant. Dele, Dómine.'); const repeat = ordo.parts[ps50Index + 1]; expect(repeat?.kind).toBe('antiphon'); expect(repeat?.kind === 'antiphon' ? repeat.text.text.la : undefined).toBe('Ant. Dele, Dómine, * iniquitátem meam.'); }); it("resolves Sunday's own psalmody: Ps 50+117 sharing one antiphon, then 62, then the Canticle of the Three Young Men", () => { const ordo = resolveOrdo('lauds', FERIAL_SUNDAY); const psalmNumbers = ordo.parts .filter((p) => p.kind === 'psalm') .map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); expect(psalmNumbers).toEqual([66, 50, 117, 62, 148, 149, 150]); const ps117 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 117); expect(ps117?.kind === 'psalm' ? ps117.antiphon : 'should be undefined -- shares Ps 50s opening antiphon').toBeUndefined(); const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus'); expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum'); }); it("resolves Saturday's own psalmody: only one weekday psalm (142), and the Canticle of Moses said in two pieces (RB 13's own division), one shared antiphon framing both", () => { // Needs a Saturday that ISN'T Our Lady's Saturday, which now wins // (and substitutes its own psalmody) on every ordinary-feria Saturday // — see calendar/index.ts's applyMarianSaturday and tests/calendar/ // marian-saturday.test.ts. A privileged-feria Saturday (Lenten Ember // Saturday, 2026-02-28) has real standing of its own that Marian // Saturday doesn't touch, so it still shows the plain weekday default. const ordo = resolveOrdo('lauds', '2026-02-28'); 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 canticles = ordo.parts.filter((p) => p.kind === 'canticle' && p.canticleId === 'canticum-moysis-deuteronomii'); expect(canticles.length).toBe(2); const [part1, part2] = canticles; expect(part1?.kind === 'canticle' ? part1.antiphon?.text.la : undefined).toBe('Ant. Date magnitúdinem'); expect(part2?.kind === 'canticle' ? part2.antiphon : 'should be undefined -- shares part 1s opening antiphon').toBeUndefined(); expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Audíte, cæli'); expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Ignis succénsus est'); // Each part gets its own Gloria Patri. expect(part1?.kind === 'canticle' ? part1.gloriaPatri?.text.la : undefined).toContain('Glória Patri'); expect(part2?.kind === 'canticle' ? part2.gloriaPatri?.text.la : undefined).toContain('Glória Patri'); const repeatIndex = ordo.parts.indexOf(part2!) + 1; const repeat = ordo.parts[repeatIndex]; expect(repeat?.kind).toBe('antiphon'); expect(repeat?.kind === 'antiphon' ? repeat.text.text.la : undefined).toBe('Ant. Date magnitúdinem * Deo nostro.'); }); it('gives the Laudate psalms (148-150) one shared antiphon, opening before 148 and repeated in full after 150', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const ps148Index = ordo.parts.findIndex((p) => p.kind === 'psalm' && p.psalmNumber === 148); const ps149 = ordo.parts[ps148Index + 1]; const ps150 = ordo.parts[ps148Index + 2]; expect(ps149?.kind === 'psalm' ? ps149.antiphon : 'should be undefined').toBeUndefined(); expect(ps150?.kind === 'psalm' ? ps150.antiphon : 'should be undefined').toBeUndefined(); const repeat = ordo.parts[ps148Index + 3]; expect(repeat?.kind).toBe('antiphon'); expect(repeat?.kind === 'antiphon' ? repeat.text.text.en : undefined).toContain( 'Praise ye the Lord * from the heavens, all His angels', ); }); it("resolves the weekday chapter/responsory/hymn/versicle bundle, sharing the same capitulum Mon-Fri", () => { const tue = resolveOrdo('lauds', FERIAL_TUESDAY); const mon = resolveOrdo('lauds', FERIAL_MONDAY); const tueChapter = tue.parts.find((p) => p.kind === 'chapter'); const monChapter = mon.parts.find((p) => p.kind === 'chapter'); expect(tueChapter?.kind === 'chapter' ? tueChapter.text.citation?.en : undefined).toBe('Rom 13:12-13'); expect(tueChapter?.kind === 'chapter' ? tueChapter.text.text.la : undefined).toBe( monChapter?.kind === 'chapter' ? monChapter.text.text.la : undefined, ); const responsory = tue.parts.find((p) => p.kind === 'responsory'); expect(responsory?.kind === 'responsory' ? responsory.text.text.la : undefined).toContain('Sana ánimam meam'); const hymn = tue.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Ales diéi núntius'); }); it("uses the Marian Sabbato hymn as Saturday's own default (explicit choice, since a genuinely unprivileged Saturday isn't achievable)", () => { const ordo = resolveOrdo('lauds', '2026-06-20'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.status.la : undefined).toBe('verified'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('O gloriósa Dómina'); }); it('resolves the Benedictus as a canticle with an opening antiphon and a full repeat, same shape as Compline\'s Nunc Dimittis', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const canticleIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'benedictus'); const canticle = ordo.parts[canticleIndex]; expect(canticle?.kind === 'canticle' ? canticle.text.text.en : undefined).toContain('Blessed be the Lord'); const repeat = ordo.parts[canticleIndex + 1]; expect(repeat?.kind).toBe('antiphon'); }); it('resolves a real Benedictus antiphon on a plain temporal day, now that every temporal id has one', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus'); expect(canticle?.kind === 'canticle' ? canticle.antiphon?.status?.la : undefined).toBe('verified'); }); it('resolves a real Benedictus antiphon for christmas-octave-sunday too, pulled from the reference engine\'s own dedicated Tempora/Nat1-0 proper rather than any individual octave-day saint\'s page', () => { // 2029-12-30 specifically: the year Dec 30 itself is the real Sunday // within the Octave, so nothing gets bumped there by // calendar/index.ts's applyChristmasOctaveSunday (see its own tests) -- // a genuinely clean case for this content, unlike most years. const ordo = resolveOrdo('lauds', '2029-12-30'); const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus'); expect(canticle?.kind === 'canticle' ? canticle.antiphon?.text.la : undefined).toContain('Dum médium siléntium'); expect(canticle?.kind === 'canticle' ? canticle.antiphon?.status?.la : undefined).toBe('verified'); }); it("resolves a real Benedictus antiphon on St. Lawrence's own day, from the saint's already-authored proper", () => { const ordo = resolveOrdo('lauds', '2026-08-10'); // St. Lawrence's own day const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus'); expect(canticle?.kind === 'canticle' ? canticle.antiphon?.text.la : undefined).toContain('In cratícula'); }); it("uses Common-of-a-Martyr-Bishop's real office bundle on St. Zephyrinus's own day, not the ferial default", () => { // St. Zephyrinus (Aug 26, Simplex, common-of-a-martyr-bishop) has no // proper Lauds content of his own; before this Common's Lauds bundle // was authored, the missing chapter made the whole bundle resolve to // undefined and fall through to the plain ferial default -- see // lauds-hymn-common-of-a-martyr-bishop.yml's header. const ordo = resolveOrdo('lauds', '2026-08-26'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Jas 1:12'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Martyr Dei, qui únicum'); }); it("uses Common-of-a-Confessor-Bishop's real office bundle on St. Ubald's own day, not the ferial default", () => { // St. Ubald (May 16, Semiduplex, common-of-a-confessor-bishop) has no // proper Lauds content of his own -- see vespers-hymn-common-of-a- // confessor-bishop.yml's header. const ordo = resolveOrdo('lauds', '2026-05-16'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 44:16-17'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu Redémptor ómnium'); }); it("uses common-of-a-confessor's real office bundle on St. Joseph's own day, not the ferial default", () => { // St. Joseph (Mar 19, `common: common-of-a-confessor`) -- see // lauds-capitulum-common-of-a-confessor.yml's header for the finding // that this plain id is really Common-of-a-Confessor-Not-Bishop // under a different label in this app's own data. const ordo = resolveOrdo('lauds', '2026-03-19'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 31:8-9'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu coróna célsior'); }); it("uses common-of-an-abbot's real office bundle on St. Maurus's own day, not the ferial default", () => { const ordo = resolveOrdo('lauds', '2026-01-15'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 31:8-9'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu coróna célsior'); }); it("uses common-of-a-virgin-martyr's real office bundle on St. Apollonia's own clean day, not the ferial default", () => { // 2026-02-09 collides (St. John of Matha wins outright, Apollonia // merely commemorated), so this uses her own already-verified clean // year (2029-02-09, per her saint file's own comment). const ordo = resolveOrdo('lauds', '2029-02-09'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('2 Cor 10:17-18'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu, coróna Vírginum'); }); it("uses common-of-a-confessor-doctor's real office bundle on St. John Damascene's own day", () => { // Live-verified 2026-08-26 to be Common-of-a-Confessor-Bishop's own // content -- see lauds-capitulum-common-of-a-confessor-doctor.yml's // header. const ordo = resolveOrdo('lauds', '2026-03-27'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 44:16-17'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu Redémptor ómnium'); }); it("uses the Commemoration of St. Paul's own proper chapter, with Common-of-an-Apostle's hymn", () => { // 2026-06-30 is the Commemoration of St. Paul's own real day (own // chapter 2 Tim 4:7-8 -- "Bonum certámen certávi" -- wins over the // Common's Eph 2:19-20, per the usual own-proper-always-wins-over- // Common precedence); the hymn is genuinely the shared Common one -- // see lauds-capitulum-commemoration-of-st-paul.yml's header. const ordo = resolveOrdo('lauds', '2026-06-30'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('2 Tim 4:7-8'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Exsúltet cælum láudibus'); }); it("uses common-of-a-holy-woman's real office bundle on St. Anne's own day, and common-of-a-widow's identical bundle on St. Monica's", () => { // Live-verified against Divinum Officium (Monastic Tridentinum 1617) // that the two categories share real content -- see lauds-capitulum- // common-of-a-holy-woman.yml's header. const anne = resolveOrdo('lauds', '2026-07-26'); const monica = resolveOrdo('lauds', '2026-05-04'); for (const ordo of [anne, monica]) { const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Prov 31:10-11'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Fortem viríli péctore'); } }); it("resolves the Guardian Angels' and Archangels' own real chapter/hymn, reused from St. Michael's Apparition", () => { // Their own saint files' [Rank] says "vide C4," but the Monastic // source's own SanctiM/10-02.txt cross-references St. Michael's // Apparition (Sancti/05-08) directly for the chapter/responsory at // every hour -- real proper content, not a Common fallback. See // lauds-capitulum-guardian-angels.yml's header. for (const [date, id] of [ ['2026-10-02', 'guardian-angels'], ['2026-03-24', 'st-gabriel-archangel'], ['2026-10-24', 'st-raphael-archangel'], ] as const) { const ordo = resolveOrdo('lauds', date); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Rev 1:1-2'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined, id).toContain('Christe sanctórum decus Angelórum'); } }); it("uses common-of-several-confessors's real office bundle (= Confessor-Not-Bishop content) on Ss. Seven Founders' own day", () => { // Read directly from the source (Sancti/02-12.txt): its own [Rule] // says plain "vide C5" -- see lauds-capitulum-common-of-several- // confessors.yml's header. const ordo = resolveOrdo('lauds', '2026-02-12'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 31:8-9'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu coróna célsior'); }); it("uses common-of-the-bvm's real office bundle on Our Lady of Mount Carmel's own day", () => { // Read directly from the source: Sancti/07-16.txt, 02-11.txt, // 05-31.txt, 09-24.txt, 10-11.txt (the 5 saints/feasts in this // category) all say plain "ex C11"/"vide C11" -- see lauds- // capitulum-common-of-the-bvm.yml's header. const ordo = resolveOrdo('lauds', '2026-07-16'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 24:14'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('O gloriósa Dómina'); }); it("resolves St. Gregory the Great's own proper chapter/hymn, not a Common fallback", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2033-03-12 (clean year). See lauds-capitulum-st-gregory- // the-great.yml's header. const ordo = resolveOrdo('lauds', '2033-03-12'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 50:1'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Mella cor obdulcántia'); }); it("resolves St. Benedict's own proper chapter/hymn, with the Confessor-Bishop responsory/versicle (not his own common-of-an-abbot's)", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2033-03-21 (clean year), cross-checked against SanctiM/ // 03-21.txt directly. See lauds-capitulum-st-benedict.yml's header. const ordo = resolveOrdo('lauds', '2033-03-21'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 50:6-7'); const responsory = ordo.parts.find((p) => p.kind === 'responsory'); expect(responsory?.kind === 'responsory' ? responsory.text.text.la : undefined).toContain('Amávit eum Dóminus'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Inter ætérnas súperum corónas'); }); it("resolves St. Agatha's own proper chapter, with Common-of-a-Virgin-Martyr's hymn", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2026-02-05 (her file's own noted clean year, 2029, turned // out to collide with St. Andrew Corsini in this app's own // calendar -- not investigated further, out of scope here). See // lauds-capitulum-st-agatha.yml's header. const ordo = resolveOrdo('lauds', '2026-02-05'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 51:1-3'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu, coróna Vírginum'); }); it("resolves St. Agnes's own proper chapter (shared verbatim with St. Agatha's), with Common-of-a-Virgin-Martyr's hymn", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2029-01-21 (clean year). See lauds-capitulum-st-agnes.yml's // header. const ordo = resolveOrdo('lauds', '2029-01-21'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 51:1-3'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Jesu, coróna Vírginum'); }); it("resolves the Annunciation's own proper chapter, with Common-of-the-BVM's hymn", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2026-03-25 (fixed date, always winnable). See lauds- // capitulum-annunciation.yml's header. const ordo = resolveOrdo('lauds', '2026-03-25'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Isa 7:14-15'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('O gloriósa Dómina'); }); it("resolves the Purification's own proper chapter, with Common-of-the-BVM's hymn", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2026-02-02 (fixed date, always winnable). See lauds- // capitulum-purification.yml's header. const ordo = resolveOrdo('lauds', '2026-02-02'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Mal 3:1'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('O gloriósa Dómina'); }); it("resolves the Finding and Exaltation of the Holy Cross's own proper chapter/hymn (shared content, two ids)", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617): Finding, 2027-05-03 (2026 collides with a privileged // Sunday); Exaltation, 2026-09-14 (its own real day). See lauds- // capitulum-finding-of-the-holy-cross.yml's and -proper-to-the- // finding-of-the-holy-cross.yml's headers. const finding = resolveOrdo('lauds', '2027-05-03'); const findingChapter = finding.parts.find((p) => p.kind === 'chapter'); expect(findingChapter?.kind === 'chapter' ? findingChapter.text.citation?.en : undefined).toBe('Phil 2:5-7'); const findingHymn = finding.parts.find((p) => p.kind === 'hymn'); expect(findingHymn?.kind === 'hymn' ? findingHymn.text.text.la : undefined).toContain('Lustris sex qui jam peráctis'); const exaltation = resolveOrdo('lauds', '2026-09-14'); const exaltationHymn = exaltation.parts.find((p) => p.kind === 'hymn'); expect(exaltationHymn?.kind === 'hymn' ? exaltationHymn.text.text.la : undefined).toContain('Lustris sex qui jam peráctis'); }); it("resolves the Transfiguration's own proper chapter/hymn", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2026-08-06 (fixed date). See lauds-capitulum- // transfiguration.yml's header. const ordo = resolveOrdo('lauds', '2026-08-06'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Phil 3:20-21'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Amor Jesu dulcíssime'); }); it("resolves All Saints of the Benedictine Order's own proper chapter/hymn, with Common-of-Several-Martyrs's responsory", () => { // Live-verified against Divinum Officium (Monastic Tridentinum // 1617), 2025-11-13 (clean year). See lauds-capitulum-all-saints- // of-the-benedictine-order.yml's header. const ordo = resolveOrdo('lauds', '2025-11-13'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Isa 8:18'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Salvéte, cedri Líbani'); }); it('includes the Kyrie + Our Father lead-in right after the Benedictus, before the day collect (either form)', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const litanyIndex = ordo.parts.findIndex((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison')); const benedictusIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'benedictus'); const collectIndex = ordo.parts.findIndex((p) => p.kind === 'prayer'); expect(litanyIndex).toBeGreaterThan(benedictusIndex); expect(collectIndex).toBeGreaterThan(litanyIndex); }); it('uses the fuller Tridentine 1906/1910 ferial Preces on a plain ferial day, not the short Sunday/feast litany', () => { // Psalm 129 (De profundis), present in the live render, is deliberately // dropped from this app's own text (2026-08) — 'Ego dixi' is the next // versicle after where the psalm used to sit, still unique to the // fuller ferial form vs. the short litany. const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison')); expect(preces?.kind === 'preces' ? preces.text.text.la : undefined).toContain('Ego dixi: Dómine, miserére mei'); expect(preces?.kind === 'preces' ? preces.text.text.en : undefined).toContain('I said: Lord, be merciful'); }); it('uses the short Sunday/feast litany, without Psalm 129, on a Sunday and on a real saint\'s own winning day', () => { const sunday = resolveOrdo('lauds', '2026-06-28'); // plain temporal Sunday const sundayPreces = sunday.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison')); expect(sundayPreces?.kind === 'preces' ? sundayPreces.text.text.la : undefined).not.toContain('Psalmus 129'); const saintsDay = resolveOrdo('lauds', '2026-08-10'); // St. Lawrence, Duplex II. classis const saintsDayPreces = saintsDay.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison')); expect(saintsDayPreces?.kind === 'preces' ? saintsDayPreces.text.text.la : undefined).not.toContain('Psalmus 129'); }); it('still uses the fuller ferial Preces on a Vigil, even though a Vigil is a sanctoral winner (unlike isSundayOrFeast elsewhere)', () => { const ordo = resolveOrdo('lauds', '2028-08-09'); // Vigil of St. Lawrence wins outright this year (not a Sunday) const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison')); expect(preces?.kind === 'preces' ? preces.text.text.la : undefined).toContain('Ego dixi: Dómine, miserére mei'); }); it('resolves just the winners own collect on a plain day with nothing commemorated', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const collects = ordo.parts.filter((p) => p.kind === 'prayer'); expect(collects.length).toBe(1); }); it('resolves both the winners collect and a commemorated saints collect on a day with a real commemoration', () => { // 2025-11-30: Advent I (the winner) with St. Andrew, Duplex II. classis, // commemorated alongside it (see tests/calendar/day-label.test.ts) -- // he has only a bare collect authored (no st-andrew-commemoration.yml // Ant+V/R bundle yet), so this renders as a labeled 'preces' block // (see resolve-common.ts's sanctoralCommemorationPart), not a second // unlabeled 'prayer' the way it used to. const ordo = resolveOrdo('lauds', '2025-11-30'); const collects = ordo.parts.filter((p) => p.kind === 'prayer'); expect(collects.length).toBe(1); const andrewCommemoration = ordo.parts.find((p) => p.kind === 'preces' && p.label?.includes('Andrew')); expect(andrewCommemoration?.kind === 'preces' ? andrewCommemoration.text.text.en : undefined).toContain( 'Andrew', ); }); it("gives St. Lawrence's octave day 2 the full Lawrence office (per resolveOfficeWinner), no Clare commemoration since she's only assigned Aug 12 specifically", () => { const ordo = resolveOrdo('lauds', LAWRENCE_OCTAVE_DAY); // 2026-08-11, day 2 const collects = ordo.parts.filter((p) => p.kind === 'prayer'); expect(collects.length).toBe(1); expect(collects[0]?.kind === 'prayer' ? collects[0].text.text.en : undefined).toContain('Lawrence'); expect(ordo.parts.some((p) => p.kind === 'preces' && p.label?.includes('Clare'))).toBe(false); }); it("gives St. Lawrence's octave day 3 (Aug 12) St. Clare's own commemoration bundle alongside Lawrence's own office", () => { const ordo = resolveOrdo('lauds', '2026-08-12'); const collects = ordo.parts.filter((p) => p.kind === 'prayer'); expect(collects.length).toBe(1); expect(collects[0]?.kind === 'prayer' ? collects[0].text.text.en : undefined).toContain('Lawrence'); const clareCommemoration = ordo.parts.find((p) => p.kind === 'preces' && p.label?.includes('Clare')); expect(clareCommemoration?.kind === 'preces' ? clareCommemoration.text.text.en : undefined).toContain( 'kingdom of heaven', ); }); it('includes all five suffrages, in order, on an ordinary ferial day -- Cross from Tridentine 1906/1910, the other four from Monastic 1617', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const labels = ordo.parts.filter((p) => p.kind === 'preces' && p.label).map((p) => (p.kind === 'preces' ? p.label : undefined)); expect(labels).toEqual([ 'Of the Holy Cross', 'Of the Blessed Virgin Mary', 'Of St. Joseph', 'Of the Holy Apostles Peter and Paul', 'For Peace', 'Salve Regina', ]); }); it('drops "Of the Holy Cross" -- not a bare ferial office -- on a plain Sunday and on a low-rank saint\'s own winning day, unlike an ordinary feria', () => { // St. Clare (Simplex) doesn't win outright (Lawrence's octave keeps // her to a commemoration -- see calendar/index.ts's applyOctaves), so // use St. Lawrence's own vigil day instead, or just check a Sunday. const sunday = resolveOrdo('lauds', '2026-06-28'); // a plain temporal Sunday, nothing else contesting const sundayLabels = sunday.parts .filter((p) => p.kind === 'preces' && p.label) .map((p) => (p.kind === 'preces' ? p.label : undefined)); expect(sundayLabels).not.toContain('Of the Holy Cross'); expect(sundayLabels).toContain('Of the Blessed Virgin Mary'); expect(sundayLabels).toContain('Of St. Joseph'); }); it('omits the suffrages on a Double-or-higher feast', () => { const ordo = resolveOrdo('lauds', '2026-08-10'); // St. Lawrence, Duplex II. classis const crossSuffrage = ordo.parts.find((p) => p.kind === 'preces' && p.label === 'Of the Holy Cross'); expect(crossSuffrage).toBeUndefined(); }); it('omits the suffrages entirely during Advent, Christmastide, and Passiontide -- confirmed against Monastic Tridentinum 1617 directly, not just Tridentine 1906/1910', () => { const advent = resolveOrdo('lauds', '2026-12-03'); // plain Advent feria const christmastide = resolveOrdo('lauds', '2026-12-30'); // plain day within the Christmas Octave const passiontide = resolveOrdo('lauds', '2026-03-23'); // Monday of Passion Week for (const ordo of [advent, christmastide, passiontide]) { expect(ordo.parts.some((p) => p.kind === 'preces' && p.label === 'Of the Holy Cross')).toBe(false); expect(ordo.parts.some((p) => p.kind === 'preces' && p.label === 'For Peace')).toBe(false); } }); it('shows the suffrages on an ordinary Lent feria (not Passiontide) -- only the last two weeks of Lent are excluded, not all of it', () => { const ordo = resolveOrdo('lauds', '2026-02-20'); // Friday after Ash Wednesday, week 1 of Lent expect(ordo.parts.some((p) => p.kind === 'preces' && p.label === 'For Peace')).toBe(true); }); it('omits the suffrages entirely on any day within an active octave, however low the octave\'s own rank is', () => { const ordo = resolveOrdo('lauds', LAWRENCE_OCTAVE_DAY); // day 2 of St. Lawrence's own (Semiduplex) octave expect(ordo.parts.some((p) => p.kind === 'preces' && p.label === 'For Peace')).toBe(false); }); it('closes with the Conclusio, the closing versicle ("may the Lord grant us his peace... and life everlasting"), and the Marian antiphon -- without spelling out the silent Our Father', () => { const ordo = resolveOrdo('lauds', FERIAL_TUESDAY); const closingVersicle = ordo.parts.find( (p) => p.kind === 'versicle' && p.text.text.en?.includes('grant us his peace'), ); expect(closingVersicle?.kind === 'versicle' ? closingVersicle.text.text.en : undefined).toContain( 'life everlasting', ); expect(closingVersicle?.kind === 'versicle' ? closingVersicle.text.text.en : undefined).not.toContain( 'Our Father', ); const last = ordo.parts[ordo.parts.length - 1]; expect(last?.kind).toBe('preces'); expect(last?.kind === 'preces' ? last.label : undefined).toBe('Salve Regina'); }); it("renders Advent's seasonal chapter/hymn/versicle instead of the plain weekday default", () => { // 2025-12-02 is a Tuesday in Advent (St. Bibiana, Simplex, merely // commemorated — the day's own winner stays temporal either way). const ordo = resolveOrdo('lauds', '2025-12-02'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Isa 2:3'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Vox clara ecce íntonat'); const versicle = ordo.parts.find((p) => p.kind === 'versicle' && p.text.text.la?.includes('Vox clamántis')); expect(versicle).toBeDefined(); }); it("renders Paschaltide's seasonal office, sharing its chapter with Vespers", () => { // 2026-04-20 is in eastertide (Easter 2026-04-05). const ordo = resolveOrdo('lauds', '2026-04-20'); const chapter = ordo.parts.find((p) => p.kind === 'chapter'); expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Rom 6:9-10'); const hymn = ordo.parts.find((p) => p.kind === 'hymn'); expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Auróra lucis rútilat'); }); }); describe('resolveOrdo("lauds", ...) responsory Gloria omission', () => { // Real Passiontide already routes to a wholesale different proper-of- // season responsory (lauds-responsory-passiontide.yml, "Érue a frámea") // that was already transcribed without a Gloria line -- this just // confirms the shared mechanism (resolve-common.ts's resolveResponsory) // doesn't break that, and still applies correctly on an ordinary date. // Same Passiontide-wide, Sancti-exempt rule as Compline's -- see // tests/hours/compline.test.ts's own describe block for the live- // verification dates. it('has no Gloria on a ferial Passiontide day, has one on an ordinary date', () => { const passiontideResponsory = resolveOrdo('lauds', '2027-03-15').parts.find((p) => p.kind === 'responsory'); const ordinaryResponsory = resolveOrdo('lauds', '2026-08-20').parts.find((p) => p.kind === 'responsory'); expect(passiontideResponsory?.kind === 'responsory' ? passiontideResponsory.text.text.la : undefined).not.toContain( 'Glória Patri', ); expect(ordinaryResponsory?.kind === 'responsory' ? ordinaryResponsory.text.text.la : undefined).toContain('Glória Patri'); }); });