Files
vu/tests/hours/lauds.test.ts
T
will 30b3971190 Give Corpus Christi and Sacred Heart their own hymnody, all octave
Same gap and same generic mechanism fix as Ascension/Pentecost --
just needed the ids added to ALWAYS_OVERRIDE_TEMPORAL_IDS plus real
content. Both feasts have 3 genuinely different hymns, one per hour:
Corpus Christi's own are widely known outside this context too
("Sacris solémniis" at Matins, "Verbum supérnum pródiens" at Lauds
whose last 2 stanzas are "O Salutaris Hostia", "Pange, lingua,
gloriósi Córporis mystérium" at Vespers whose last 2 stanzas are
"Tantum Ergo").

Sacred Heart needed one source-track correction: Monastic Tridentinum
1617 doesn't observe this feast on this date at all (shows St.
Barnabas instead) -- used Divino Afflatu 1954 instead, this project's
own stated rubric track. Also live-verified a genuine structural
difference: Sacred Heart's own Lauds/Vespers have no responsory at
all in the source (straight chapter to hymn) -- not an authoring gap.
Fixed lauds.ts's and vespers.ts's own resolveOffice to omit the
responsory part entirely when its status is missing in both
languages, instead of always rendering an empty section.

Authored matins-hymn-{corpus-christi,sacred-heart}.yml,
lauds-{capitulum,responsory,hymn,versicle}-corpus-christi.yml,
vespers-{responsory,hymn,versicle}-corpus-christi.yml,
lauds-{capitulum,hymn,versicle}-sacred-heart.yml, and
vespers-{hymn,versicle}-sacred-heart.yml -- all live-verified,
2026-06-04 and 2026-06-12.

Fixed 2 pre-existing tests whose own dates now land inside these
newly-modeled octaves and were asserting the old fall-through
behavior as if correct: lauds.test.ts's FERIAL_MONDAY (moved off
2026-06-08) and vespers.test.ts's St. Margaret of Scotland test
(2026-06-09) -- its own title already anticipated this fix, just had
the wrong expected values until now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 12:54:55 -04:00

759 lines
47 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { resolveOrdo } from '../../src/hours';
import { getLaudsCommonOverride } from '../../src/hours/lauds-psalmody-overrides';
// 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-09-07'; // not 2026-06-08 -- that fell inside Corpus Christi's own octave once its own hymnody was modeled 2026-09-04, see TODO.md; not 08-31 either -- St. Raymond Nonnatus's own day (common-of-a-confessor)
const FERIAL_TUESDAY = '2033-09-06'; // not 2026-06-16 -- that fell inside Sacred Heart's own octave once modeled 2026-08-26, see TODO.md
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');
// Its non-contiguous liturgical citation (last verse said out of
// numeric order) must come through as authored, not get mangled by
// a first/last-verse-number formula the way a naive fix would.
expect(canticle?.kind === 'canticle' ? canticle.text.citation?.la : undefined).toBe('Dan 3:57-88,56');
});
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');
// Regression test: each half must cite its own verse range, not the
// whole canticle's "Deut 32:1-65" on both pieces.
expect(part1?.kind === 'canticle' ? part1.text.citation?.la : undefined).toBe('Deut 32:1-32');
expect(part2?.kind === 'canticle' ? part2.text.citation?.la : undefined).toBe('Deut 32:33-65');
// 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. Robert Bellarmine's own day, not the ferial default", () => {
// Was St. Ubald (May 16, Semiduplex, common-of-a-confessor-bishop) --
// moved to St. Robert Bellarmine (May 13, Duplex, same Common) once
// Ascension's own octave was modeled (2026-08-26, see TODO.md):
// Ubald now loses his own day outright to the octave (`octave.wins:
// duplex`, and Semiduplex Ubald falls short of it), so his own
// Common-of-a-Confessor-Bishop content no longer surfaces there at
// all. Bellarmine sits safely before the octave's own window (May
// 14-21) and has no proper Lauds content of his own either -- see
// vespers-hymn-common-of-a-confessor-bishop.yml's header.
const ordo = resolveOrdo('lauds', '2026-05-13');
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. St. Gabriel is NOT
// part of this group (see below) -- his own chapter is genuinely
// proper, not shared, despite sharing the same hymn.
for (const [date, id] of [
['2026-10-02', 'guardian-angels'],
['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("resolves St. Gabriel's own genuinely proper chapter (Dan 9:21-22), not Apparition's", () => {
// lauds-capitulum-st-gabriel-archangel.yml previously carried
// Apparition's own capitulum ("Significávit Deus...") under
// Gabriel's id by mistake -- a copy-paste bug found while authoring
// his Matins psalmody (2026-08-28). His real Lauds/Vespers capitulum,
// read directly from Sancti/03-24.txt's own [Capitulum Laudes]
// block, is Daniel's account of the angel's visitation -- shares the
// hymn with the other Archangel feasts, but not this chapter.
const ordo = resolveOrdo('lauds', '2026-03-24');
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Dan 9:21-22');
expect(chapter?.kind === 'chapter' ? chapter.text.text.la : undefined).toContain('Ecce vir Gábriel');
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).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("uses Common-of-the-BVM's own psalmody (92, 99, 62 antiphons + Trium Puerorum) on Our Lady of Mount Carmel's own day, not the ferial default", () => {
// Live-verified against Divinum Officium (Monastic Tridentinum 1617,
// votive=C11), 2026-08-28 -- see data/hours/lauds-psalmody-overrides/
// categories/common-of-the-bvm.yml's own header. Same date as the
// office-bundle test just above; she has no per-feast Lauds psalmody
// override authored, so this is her Common's own tier.
const ordo = resolveOrdo('lauds', '2026-07-16');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'canticum-trium-puerorum');
expect(canticle).toBeDefined();
});
it("uses Common-of-an-Abbot's own psalmody on St. Benedict's own day, not the ferial default (his proper chapter/hymn is separate, see below)", () => {
// St. Benedict (duplex-1-classis, common-of-an-abbot) has his own
// proper chapter/hymn (see the test below) but no per-feast Lauds
// psalmody override, so his psalm numbers/antiphons fall to his
// Common's own tier -- live-verified against Divinum Officium
// (Monastic Tridentinum 1617, votive=C5), 2026-08-28. Same clean year
// as the proper-chapter test below.
const ordo = resolveOrdo('lauds', '2033-03-21');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
});
it("uses Common-of-Several-Martyrs' own psalmody on Ss. Placid and Companions' own day, not the ferial default", () => {
// 2026-10-05, Duplex II. classis, common-of-several-martyrs, no
// per-feast Lauds psalmody override authored -- same date as
// vespers.test.ts's own office-bundle proof for this Common.
// Live-verified against Divinum Officium (Monastic Tridentinum 1617,
// votive=C3), 2026-08-28.
const ordo = resolveOrdo('lauds', '2026-10-05');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
});
it('stays on the plain ferial default for a below-threshold feast (Duplex), even with a Common that has its own category psalmody', () => {
// St. Jerome (Sept 30, plain Duplex, common-of-a-confessor-not-bishop)
// is below the duplex-majus+ override threshold -- confirmed live
// against Divinum Officium, which keeps this date on the plain
// ferial Wednesday set, not his Common's own psalmody.
const ordo = resolveOrdo('lauds', '2026-09-30');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([66, 50, 63, 64, 148, 149, 150]);
});
it("uses Common-of-an-Angel's own psalmody (reusing St. Michael's own proper text) on Guardian Angels' own day", () => {
// Guardian Angels (Oct 2, Duplex Majus) has no proper text of her own
// -- live-verified (2026-08-28) that her real Lauds antiphons are St.
// Michael's own, byte-for-byte, which is exactly what this category's
// content is (see data/hours/lauds-psalmody-overrides/categories/
// common-of-an-angel.yml's own header). Psalm numbers stay the usual
// fixed Duplex-majus+ set (92, 99, 62), unlike this Common's Vespers
// psalmody, which substitutes "Psalmi Dominica" instead.
const ordo = resolveOrdo('lauds', '2026-10-02');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
const secondPsalm = ordo.parts.filter((p) => p.kind === 'psalm')[1];
expect(secondPsalm?.kind === 'psalm' ? secondPsalm.antiphon?.text.la : undefined).toContain('Stetit Angelus');
});
it('aliases common-of-an-evangelist onto common-of-an-apostle for Lauds psalmody', () => {
// Every Evangelist on this calendar (St. John Apostle, St. Luke, St.
// Mark) already has their own per-feast Lauds override authored, so
// the Common-category tier can never actually surface via
// resolveOrdo/a real date for Lauds specifically (unlike Vespers, see
// vespers.test.ts's own St. Mark test) -- test the alias wiring
// directly instead. St. Mark's and St. Luke's own reference-engine
// rule files both say "ex C1a", and votive=C1a returned byte-identical
// Lauds psalmody to plain votive=C1 (2026-08-28).
const override = getLaudsCommonOverride('st-mark');
expect(override?.id).toBe('common-of-an-apostle');
expect(override?.groups.map((g) => g.psalms)).toEqual([[92], [99], [62]]);
});
it('aliases common-of-several-women-martyrs onto common-of-a-holy-woman for Lauds psalmody', () => {
// No majus+ saint on this calendar actually carries this Common yet
// (Ss. Perpetua and Felicity, the only one, are plain Duplex -- below
// this override's threshold), so this can't be exercised via
// resolveOrdo/a real date; test the alias wiring directly instead.
// Ss. Perpetua and Felicity's own reference-engine file says "vide
// C7b", and votive=C7b returned these same groups/canticle/Laudate
// antiphons as plain votive=C7 (2026-08-28) -- only the Benedictus
// antiphon differs, a separate mechanism from this psalmody table.
const override = getLaudsCommonOverride('ss-perpetua-and-felicity');
expect(override?.id).toBe('common-of-a-holy-woman');
expect(override?.groups.map((g) => g.psalms)).toEqual([[92], [99], [62]]);
});
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', () => {
// 2029-01-14: post-Epiphany II (the winner, `ordinary-sunday`) with
// St. Felix Presbyter, Simplex, commemorated alongside it — live-
// verified 2026-09-01. (Not 2025-11-30/St. Andrew as this test used
// to: a privileged Sunday transfers *everything*, no rank exception
// — Andrew doesn't get a bare commemoration there at all, see
// tests/calendar/day-label.test.ts. `ordinary-sunday`'s own Simplex
// branch is the real case that still commemorates a saint in place.)
const ordo = resolveOrdo('lauds', '2029-01-14');
const collects = ordo.parts.filter((p) => p.kind === 'prayer');
expect(collects.length).toBe(1);
const felixCommemoration = ordo.parts.find((p) => p.kind === 'preces' && p.label?.includes('Felix'));
expect(felixCommemoration).toBeDefined();
expect(felixCommemoration?.kind === 'preces' ? felixCommemoration.text.text.en : undefined).toContain(
'palm tree',
);
});
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('gives Ss. Felix and Adauctus their own commemoration antiphon+versicle on a Sunday, not just a bare collect (fixed 2026-08-30, ss-felix-and-adauctus-commemoration.yml live-verified against that date)', () => {
const ordo = resolveOrdo('lauds', '2026-08-30');
const felixCommemoration = ordo.parts.find((p) => p.kind === 'preces' && p.label?.includes('Felix'));
expect(felixCommemoration?.kind === 'preces' ? felixCommemoration.text.text.en : undefined).toContain(
'hairs of your head',
);
});
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');
});
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', () => {
// 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');
});
});