f55cd46272
Deploy / deploy (push) Successful in 1m23s
resolveMatinsInvitatoryText's Common-category tier existed but only had Common-of-an-Apostle authored, so every other Common category fell all the way through to the plain ferial invitatory antiphon regardless of rank (e.g. St. Joseph Calasanctius, 2026-08-27, Confessor). Adds the Confessor family (5 duplicated ids, matching the hymn's own precedent), Martyr family (2 ids + Several Martyrs), Virgin family (2 ids), and Dedication — all read directly from the reference engine's own Commune/C-number [Invit] lines. Holy Women (C7) needs a name-template mechanism this project doesn't have yet for invitatory antiphons, and General/All Saints (C9) turned out to be a mislabeled backlog item (C9 is actually the Office of the Dead) — both logged in TODO.md instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AVXS5iTrk3c2anSYVGwxS
697 lines
41 KiB
TypeScript
697 lines
41 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveOrdo } from '../../src/hours';
|
|
import { getPsalmsFor } from '../../src/psalter/distribution';
|
|
|
|
// Two clean proof dates, one per branch — see hours/matins.ts's own header
|
|
// and TODO.md for why this is a mechanism build with a small content slice,
|
|
// not full-calendar content. Both live-verified against the reference
|
|
// engine (Monastic Tridentinum 1617) before authoring.
|
|
const FERIAL_DATE = '2026-12-01'; // Tuesday of Advent I — plain ferial, no feast collision.
|
|
// Not Dec 15 (formerly used here): that's actually the Immaculate
|
|
// Conception's own octave closing day (Duplex majus per Divino Afflatu
|
|
// 1954), which now correctly wins outright over Advent's own
|
|
// privileged-feria-minor ferias — see calendar/octaves.ts's
|
|
// octaveGoverningPrivilegedDay — so it's no longer a clean ferial date.
|
|
const SUNDAY_DATE = '2026-09-06'; // 14th Sunday after Trinity — plain Sunday, no feast collision.
|
|
|
|
describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
|
|
const ordo = resolveOrdo('matins', FERIAL_DATE);
|
|
|
|
it('is no longer notImplemented', () => {
|
|
expect(ordo.notImplemented).toBeUndefined();
|
|
});
|
|
|
|
it('opens with the Deus in adjutorium versicle, then Psalm 3, then the Invitatory (Psalm 94)', () => {
|
|
expect(ordo.parts[0]?.kind).toBe('versicle');
|
|
expect(ordo.parts[1]).toMatchObject({ kind: 'psalm', psalmNumber: 3 });
|
|
expect(ordo.parts[2]).toMatchObject({ kind: 'section-heading', label: 'Invitatory' });
|
|
expect(ordo.parts[3]).toMatchObject({ kind: 'psalm', psalmNumber: 94 });
|
|
const invitatoryAntiphon = (ordo.parts[3] as { antiphon?: { text: Record<string, string> } }).antiphon;
|
|
expect(invitatoryAntiphon?.text.la).toContain('Veníte');
|
|
// Real practice repeats the invitatory antiphon between verse groups —
|
|
// deliberately not modeled that way here (see hours/matins.ts's own
|
|
// header): confirm there's exactly one standalone repeat, not several.
|
|
const standaloneAntiphons = ordo.parts.filter((p) => p.kind === 'antiphon');
|
|
expect(standaloneAntiphons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('has exactly 9 ferial psalms, no 3-nocturn structure, and no Te Deum', () => {
|
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
// Ps 3 and 94 (opening + invitatory) plus Tuesday's own 9-psalm ferial table.
|
|
expect(psalms).toEqual([3, 94, 43, 44, 47, 48, 49, 54, 55, 58, 59]);
|
|
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(false);
|
|
expect(ordo.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
|
// "Te decet laus" is sung only right after the Te Deum on a 3-nocturn
|
|
// day (2026-08) — absent here along with the Te Deum itself.
|
|
expect(ordo.parts.some((p) => p.kind === 'versicle' && p.text.text.la?.includes('Te decet laus'))).toBe(false);
|
|
});
|
|
|
|
it("gives every nocturn psalm its own antiphon and includes Tuesday's own versicle (data/hours/matins-ferial-antiphons.yml)", () => {
|
|
// The nocturn's own psalms start at index 4 (after versicle/Ps3/heading/Ps94).
|
|
const nocturnPsalms = ordo.parts.slice(4).filter((p) => p.kind === 'psalm') as {
|
|
psalmNumber: number;
|
|
antiphon?: { text: Record<string, string> };
|
|
}[];
|
|
expect(nocturnPsalms.map((p) => p.psalmNumber)).toEqual([43, 44, 47, 48, 49, 54, 55, 58, 59]);
|
|
// Groups: [43,44] [47,48] [49] [54,55] [58] [59] — antiphon only on
|
|
// each group's first psalm.
|
|
const withAntiphon = nocturnPsalms.filter((p) => p.antiphon !== undefined).map((p) => p.psalmNumber);
|
|
expect(withAntiphon).toEqual([43, 47, 49, 54, 58, 59]);
|
|
for (const p of nocturnPsalms) {
|
|
if (p.antiphon) {
|
|
expect(p.antiphon.text.la).toBeTruthy();
|
|
expect(p.antiphon.text.en).toBeTruthy();
|
|
}
|
|
}
|
|
const nocturnVersicles = ordo.parts.slice(4).filter((p) => p.kind === 'versicle');
|
|
expect(nocturnVersicles).toHaveLength(1);
|
|
const versicleText = (nocturnVersicles[0] as { text: { text: Record<string, string> } }).text.text;
|
|
expect(versicleText.la).toContain('Ímmola Deo sacrifícium laudis');
|
|
expect(versicleText.la).toContain('Et redde Altíssimo vota tua');
|
|
});
|
|
|
|
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
|
|
// Plain 2 bible-plan readings on this particular clean ferial (no
|
|
// commemorated saint here) — see the next test for the 3-reading case.
|
|
expect(lessons).toHaveLength(2);
|
|
const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en);
|
|
expect(citations).toEqual(['Isa 6; Isa 7', 'Sap 2']);
|
|
// Real content isn't imported yet (Vulgate/Douay-Rheims bulk import is
|
|
// a deferred follow-on) — honest `missing`, not fabricated text.
|
|
expect((lessons[0] as { text: { status: { en?: string } } }).text.status.en).toBe('missing');
|
|
});
|
|
|
|
it("adds a 3rd reading for a commemorated saint, generously surfaced per the 'commemorations get their own reading' design (see hours/matins.ts's own header)", () => {
|
|
// Not FERIAL_DATE: needs an ordinary Advent ferial with a real
|
|
// commemorated saint and no active octave. St. Bibiana (Simplex) is
|
|
// the exact case calendar/commemorations.ts's own header cites as
|
|
// live-verified: merely commemorated under an Advent feria, never
|
|
// winning outright (Simplex is below privileged-feria-minor's own
|
|
// Semiduplex threshold). 2025-12-02, a Tuesday, keeps the same 9-psalm
|
|
// ferial shape as the other tests in this block.
|
|
const commemoratedOrdo = resolveOrdo('matins', '2025-12-02');
|
|
const lessons = commemoratedOrdo.parts.filter((p) => p.kind === 'lesson');
|
|
expect(lessons).toHaveLength(3);
|
|
const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en);
|
|
expect(citations).toEqual(['Isa 6; Isa 7', 'Sap 2', undefined]);
|
|
expect((lessons[0] as { text: { status: { en?: string } } }).text.status.en).toBe('missing');
|
|
});
|
|
|
|
it("matches the first reading (Isaiah) against the seeded per-book responsory pool", () => {
|
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
|
|
const first = lessons[0] as { responsory?: { text: { la?: string } } };
|
|
expect(first.responsory?.text.la).toContain('Ægýpte');
|
|
});
|
|
|
|
it('closes with the fixed ferial capitulum, then the day collect', () => {
|
|
const chapterIndex = ordo.parts.findIndex((p) => p.kind === 'chapter');
|
|
const prayerIndex = ordo.parts.findIndex((p) => p.kind === 'prayer');
|
|
expect(chapterIndex).toBeGreaterThan(-1);
|
|
expect(prayerIndex).toBeGreaterThan(chapterIndex);
|
|
const chapter = ordo.parts[chapterIndex] as { text: { citation?: { en?: string } } };
|
|
expect(chapter.text.citation?.en).toBe('1 Cor 16:13-14');
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => {
|
|
const ordo = resolveOrdo('matins', SUNDAY_DATE);
|
|
|
|
it('has the full 12-psalm Sunday psalmody (Ps 20-31) plus 3 canticles, and a Te Deum', () => {
|
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
// Ps 3 + 94 (opening/invitatory) + 20-31 (Sunday's fixed 12).
|
|
expect(psalms).toEqual([3, 94, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]);
|
|
const canticles = ordo.parts.filter((p) => p.kind === 'canticle');
|
|
expect(canticles).toHaveLength(3);
|
|
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
});
|
|
|
|
it('renders each nocturn versicle with both its V. and R. lines (real bug: sundayPsalmNocturn/sundayCanticleNocturn used to drop the R. line entirely)', () => {
|
|
const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record<string, string> } }[];
|
|
// Opening Deus-in-adjutorium versicle + one per nocturn (1, 2, 3) +
|
|
// the closing "Te decet laus" versicle after the Te Deum (2026-08).
|
|
expect(versicles).toHaveLength(5);
|
|
const [, nocturn1, nocturn2, nocturn3] = versicles;
|
|
expect(nocturn1?.text.text.la).toBe('V. Memor fui nocte nóminis tui Dómine.\nR. Et custodívi legem tuam.');
|
|
expect(nocturn2?.text.text.la).toBe('V. Média nocte surgébam ad confiténdum tibi.\nR. Super justítia justificatiónis tuæ.');
|
|
expect(nocturn3?.text.text.la).toBe('V. Exaltáre Dómine in virtúte tua.\nR. Cantábimus et psallémus virtútes tuas.');
|
|
// The 5th versicle is "Te decet laus", right after the Te Deum (2026-08).
|
|
expect(versicles[4]?.text.text.la).toContain('Te decet laus');
|
|
});
|
|
|
|
it("resolves the user's own bible-plan reading (Tobit/Sirach) for Nocturn 1", () => {
|
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
|
|
const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en);
|
|
expect(citations).toContain('Tob 1; Tob 2');
|
|
expect(citations).toContain('Sir 45');
|
|
});
|
|
|
|
it('includes the real Nocturn 2 patristic reading (Gregory on Job) and Nocturn 3 Gospel + homily, both flagged correctly', () => {
|
|
const lessons = ordo.parts.filter(
|
|
(p) => p.kind === 'lesson',
|
|
) as { label?: string; isGospel?: boolean; text: { text: { la?: string } } }[];
|
|
const gregory = lessons.find((l) => l.label?.includes('Gregory'));
|
|
expect(gregory).toBeDefined();
|
|
expect(gregory?.isGospel).toBe(false);
|
|
|
|
const gospel = lessons.find((l) => l.isGospel);
|
|
expect(gospel).toBeDefined();
|
|
expect(gospel?.text.text.la).toContain('Naim');
|
|
|
|
const homily = lessons.find((l) => l.label?.includes('Augustine'));
|
|
expect(homily).toBeDefined();
|
|
expect(homily?.isGospel).toBe(false);
|
|
});
|
|
|
|
it('never sources a Gospel from a Common-of-Saints fallback — every Gospel-flagged reading traces to the bible-plan or a real proper', () => {
|
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { isGospel?: boolean; label?: string }[];
|
|
const gospels = lessons.filter((l) => l.isGospel);
|
|
// This Sunday's own TSV row deliberately has no Gospel (confirmed
|
|
// editorial choice) — the only Gospel-flagged reading should be the
|
|
// day's own proper Nocturn 3 Gospel, not a substituted Common one.
|
|
expect(gospels).toHaveLength(1);
|
|
});
|
|
|
|
it('orders Te Deum after every reading, and the day collect last', () => {
|
|
const teDeumIndex = ordo.parts.findIndex((p) => p.kind === 'te-deum');
|
|
const lastLessonIndex = ordo.parts.map((p) => p.kind).lastIndexOf('lesson');
|
|
const prayerIndex = ordo.parts.findIndex((p) => p.kind === 'prayer');
|
|
expect(teDeumIndex).toBeGreaterThan(lastLessonIndex);
|
|
expect(prayerIndex).toBeGreaterThan(teDeumIndex);
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunday) branch', () => {
|
|
// 2026-08-17 -- St. Lawrence's own octave closing day ("In Octava S.
|
|
// Laurentii Martyris ~ Duplex"), live-verified directly against the
|
|
// reference engine (Monastic Tridentinum 1617, command=prayMatutinum).
|
|
// The mechanism bug this proves fixed: a Duplex+ weekday feast used to
|
|
// fall through to reusing the literal *Sunday* psalmody (Ps 20-31)
|
|
// unconditionally -- confirmed wrong, since a real Duplex+ weekday
|
|
// feast has its own genuinely different psalmody (see
|
|
// hours/matins-psalmody-overrides.ts and its own st-lawrence.yml proof
|
|
// data for the full sourcing detail).
|
|
const ordo = resolveOrdo('matins', '2026-08-17');
|
|
|
|
it("uses St. Lawrence's own proper psalmody, not the Sunday scheme, across 3 nocturns plus a Te Deum", () => {
|
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
// Ps 3 + 94 (opening/invitatory), then his own Nocturn 1 (1,2,4,5,8,10)
|
|
// and Nocturn 2 (14,16,20,23,63,91) -- confirmed live, not the Sunday
|
|
// scheme's Ps 20-31 at all.
|
|
expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 16, 20, 23, 63, 91]);
|
|
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
});
|
|
|
|
it("every Nocturn 1/2 psalm carries St. Lawrence's own proper antiphon, verified in both languages", () => {
|
|
const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
|
// Skip Ps 3 (opening, no antiphon) and Ps 94 (the Invitatory, its own
|
|
// separate antiphon convention) -- the remaining 12 are his own.
|
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
|
expect(nocturnPsalms).toHaveLength(12);
|
|
for (const p of nocturnPsalms) {
|
|
expect(p.antiphon?.status.la).toBe('verified');
|
|
expect(p.antiphon?.status.en).toBe('verified');
|
|
}
|
|
});
|
|
|
|
it('has 3 real OT canticles in Nocturn 3, verified in both languages, sourced from the newly-authored Sirach/Jeremiah chapters', () => {
|
|
const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string; text: { status: Record<string, string> } }[];
|
|
expect(canticles).toHaveLength(3);
|
|
// The first canticle spans two Sirach chapters under one heading in
|
|
// the source -- kept as one canticle with two refs, not split.
|
|
expect(canticles[0]?.canticleId).toBe('sir-14-22_sir-15-3-4_sir-15-6');
|
|
for (const c of canticles) {
|
|
expect(c.text.status.la).toBe('verified');
|
|
expect(c.text.status.en).toBe('verified');
|
|
}
|
|
});
|
|
|
|
it("uses ferial psalms with its own Terce/Sext/None antiphons (one per nocturn) plus Common's own versicles, for a plain-Duplex weekday winner with no proper antiphon authored", () => {
|
|
// St. Ignatius of Antioch (Duplex, Common of a Martyr-Bishop, no
|
|
// proper antiphon of his own) -- Feb 1 2029, a Thursday, confirmed
|
|
// clean (no Sunday collision) per his own saint-file comment.
|
|
// Duplex is the floor of 3-nocturn eligibility (user, 2026-08): the
|
|
// psalm numbers are Thursday's own real ferial table
|
|
// (psalter-distribution.yml), chunked into 3 -- not Common's own
|
|
// psalm numbers, which only duplex-majus+ winners still get in full.
|
|
// Each nocturn's versicle is Common's own (getMatinsCommonOverride's
|
|
// Common-of-a-Martyr-Bishop resolves via common-of-a-martyr.yml's
|
|
// `aliases`); each nocturn's antiphon is his own SaintRecord.
|
|
// minorHoursCommon (common-of-a-martyr, live-verified identical
|
|
// across the Bishop/non-Bishop split) own Terce/Sext/None antiphon --
|
|
// real, verified, non-psalm-specific text, one distinct antiphon per
|
|
// nocturn (user, 2026-08-27: "some antiphons from the feria and some
|
|
// from the commons feels off... use those antiphons, one per
|
|
// nocturn"), not Common's own Matins Nocturn 1/2 antiphons (those are
|
|
// 6 individual per-psalm paraphrases that would misquote a different
|
|
// ferial psalm).
|
|
const common = resolveOrdo('matins', '2029-02-01');
|
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
expect(psalms).toEqual([3, 94, 76, 77, 77, 78, 79, 80, 82, 83, 84]);
|
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
// No canticles -- the ferial table has none, and this tier doesn't
|
|
// borrow Common's own (unlike a duplex-majus+ winner's full scheme).
|
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
|
|
|
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { text: Record<string, string>; status: Record<string, string> } }[];
|
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
|
// Nocturn 1 (76,77,77): Terce's own Common-of-a-Martyr antiphon.
|
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toBe('Ant. Qui séquitur me, * non ámbulat in ténebris, sed habébit lumen vitæ, dicit Dóminus.');
|
|
expect(nocturnPsalms.slice(1, 3).every((p) => p.antiphon === undefined)).toBe(true);
|
|
// Nocturn 2 (78,79,80): Sext's own, a genuinely different antiphon.
|
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toBe('Ant. Qui mihi minístrat, * me sequátur: et ubi ego sum, illic sit et miníster meus.');
|
|
expect(nocturnPsalms.slice(4, 6).every((p) => p.antiphon === undefined)).toBe(true);
|
|
// Nocturn 3 (82,83,84): None's own, different again.
|
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toBe('Ant. Volo Pater, * ut ubi ego sum, illic sit et miníster meus.');
|
|
expect(nocturnPsalms.slice(7).every((p) => p.antiphon === undefined)).toBe(true);
|
|
for (const i of [0, 3, 6]) {
|
|
expect(nocturnPsalms[i]?.antiphon?.status.la).toBe('verified');
|
|
expect(nocturnPsalms[i]?.antiphon?.status.en).toBe('verified');
|
|
}
|
|
});
|
|
|
|
it("uses ferial psalms with Common-of-an-Abbot's own Terce/Sext/None antiphons (St. Francis of Paola's own minorHoursCommon, distinct from his Matins-classification common) for a plain-Duplex weekday winner with no proper antiphon authored", () => {
|
|
// St. Francis of Paola (Duplex, Common of a Confessor Not a Bishop
|
|
// for `common`, but `minorHoursCommon: common-of-an-abbot` -- his
|
|
// own P/T/S/N draw from the Abbot common instead, live-verified per
|
|
// his own saint-file comment) -- 2030-04-02, a Tuesday, confirmed
|
|
// clean (no Sunday collision, safely outside Holy Week/Easter octave
|
|
// that year).
|
|
const common = resolveOrdo('matins', '2030-04-02');
|
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
expect(psalms).toEqual([3, 94, 43, 44, 47, 48, 49, 54, 55, 58, 59]);
|
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
|
|
|
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { text: Record<string, string>; status: Record<string, string> } }[];
|
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toBe('Ant. Euge, serve bone * in módico fidélis, intra in gáudium Dómini tui.');
|
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toBe('Ant. Fidélis servus * et prudens, quem constítuit Dóminus super famíliam suam.');
|
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toBe('Ant. Serve bone * et fidélis, intra in gáudium Dómini tui.');
|
|
for (const i of [0, 3, 6]) {
|
|
expect(nocturnPsalms[i]?.antiphon?.status.la).toBe('verified');
|
|
expect(nocturnPsalms[i]?.antiphon?.status.en).toBe('verified');
|
|
}
|
|
});
|
|
|
|
it("uses ferial psalms for a Common-of-a-Confessor-Bishop winner -- same weekday psalm numbers as any other plain-Duplex Thursday winner, since psalm numbers no longer come from Common at all", () => {
|
|
// St. Basil the Great (Duplex, live-verified as byte-identical to
|
|
// common-of-a-confessor-bishop.yml despite his own [Rule] saying
|
|
// "vide C4a") -- 2035-06-14, a Thursday, confirmed clean per his own
|
|
// saint-file comment. Same Thursday ferial psalms as St. Ignatius of
|
|
// Antioch's own test above -- the two Commons only ever differed in
|
|
// versicles/antiphon, and psalm numbers are ferial now regardless of
|
|
// which Common category applies.
|
|
const common = resolveOrdo('matins', '2035-06-14');
|
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
expect(psalms).toEqual([3, 94, 76, 77, 77, 78, 79, 80, 82, 83, 84]);
|
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
|
});
|
|
|
|
it("uses ferial psalms with Common-of-Several-Martyrs's own Terce/Sext/None antiphons (one per nocturn) for a plain-Duplex weekday winner with no proper antiphon authored", () => {
|
|
// Ss. Fabian and Sebastian (Duplex, Common of Several Martyrs, no
|
|
// proper antiphon of their own) -- 2029-01-20, a Saturday, confirmed
|
|
// clean (no Sunday collision) per their own saint-file comment.
|
|
const common = resolveOrdo('matins', '2029-01-20');
|
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
expect(psalms).toEqual([3, 94, 96, 104, 104, 105, 105, 106, 106, 107, 108]);
|
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
|
|
|
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { text: Record<string, string>; status: Record<string, string> } }[];
|
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toBe('Ant. Cum palma * ad regna pervenérunt Sancti, corónas decóris meruérunt de manu Dei.');
|
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toBe('Ant. Córpora Sanctórum * in pace sepúlta sunt: et vivent nómina eórum in ætérnum.');
|
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toBe('Ant. Mártyrum chorus * laudáte Dóminum de cælis, allelúja.');
|
|
expect(nocturnPsalms[6]?.antiphon?.status.la).toBe('verified');
|
|
expect(nocturnPsalms[6]?.antiphon?.status.en).toBe('verified');
|
|
expect(nocturnPsalms.slice(7).every((p) => p.antiphon === undefined)).toBe(true);
|
|
});
|
|
|
|
it('falls all the way back to the plain ferial weekday table, redistributed into 3 nocturns, when neither a proper nor a Common entry is authored', () => {
|
|
// St. Anthony, Abbot (Duplex, Common of an Abbot -- no
|
|
// matins-psalmody-overrides category authored yet for either tier)
|
|
// -- 2029-01-17, a Wednesday, confirmed clean (no Sunday collision).
|
|
const fallback = resolveOrdo('matins', '2029-01-17');
|
|
const psalms = fallback.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
// Ps 3 + 94, then Wednesday's own 9-psalm ferial table
|
|
// (psalter-distribution.yml), not any Common-of-an-Abbot scheme.
|
|
expect(psalms[0]).toBe(3);
|
|
expect(psalms[1]).toBe(94);
|
|
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'wednesday').map((r) => r.number));
|
|
expect(fallback.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
expect(fallback.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("matins", ...) second Duplex+ weekday-feast proof — St. Andrew, Common of an Apostle', () => {
|
|
// 2026-11-30 -- St. Andrew's own day outright ("S. Andreæ Apostoli",
|
|
// not transferred that year: Nov 30 falls on a Monday, not a Sunday),
|
|
// live-verified directly against the reference engine. Second proof
|
|
// for the matins-psalmody-overrides mechanism after St. Lawrence, this
|
|
// one for the Common-of-an-Apostle category (see
|
|
// matins-psalmody-overrides/categories/common-of-an-apostle.yml and
|
|
// its own st-andrew.yml).
|
|
const ordo = resolveOrdo('matins', '2026-11-30');
|
|
|
|
it("uses St. Andrew's own proper psalmody across 3 nocturns plus a Te Deum", () => {
|
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
|
expect(psalms).toEqual([3, 94, 18, 33, 44, 46, 60, 63, 74, 95, 96, 97, 98, 100]);
|
|
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
|
});
|
|
|
|
it("every Nocturn 1/2 psalm carries St. Andrew's own proper antiphon, verified in both languages", () => {
|
|
const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
|
expect(nocturnPsalms).toHaveLength(12);
|
|
for (const p of nocturnPsalms) {
|
|
expect(p.antiphon?.status.la).toBe('verified');
|
|
expect(p.antiphon?.status.en).toBe('verified');
|
|
}
|
|
});
|
|
|
|
it('has 3 real OT canticles in Nocturn 3, verified in both languages, sourced from the newly-authored Isaiah/Wisdom chapters', () => {
|
|
const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string; text: { status: Record<string, string> } }[];
|
|
expect(canticles).toHaveLength(3);
|
|
expect(canticles.map((c) => c.canticleId)).toEqual(['isa-61-6-9', 'sap-3-7-9', 'sap-10-17-21']);
|
|
for (const c of canticles) {
|
|
expect(c.text.status.la).toBe('verified');
|
|
expect(c.text.status.en).toBe('verified');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("matins", ...) Sacred Triduum', () => {
|
|
// Tenebrae's real rubric drops the whole opening (versicle, Ps 3,
|
|
// Invitatory, hymn) in favor of a silently-said Pater/Ave/Credo —
|
|
// live-verified against both Monastic Tridentinum 1617 and Divino
|
|
// Afflatu 1954 (Holy Thursday: "Invitatorium{omittitur}",
|
|
// "Hymnus{omittitur}", no Ps 3 or versicle either). This app never
|
|
// renders that silent block anywhere (see data/hours/prime.yml's own
|
|
// header), so the fix is a pure omission -- straight into Nocturn 1.
|
|
const HOLY_THURSDAY = '2026-04-02';
|
|
const GOOD_FRIDAY = '2026-04-03';
|
|
const HOLY_SATURDAY = '2026-04-04';
|
|
|
|
it.each([HOLY_THURSDAY, GOOD_FRIDAY, HOLY_SATURDAY])('drops the opening versicle/Ps 3/Invitatory/hymn on %s', (date) => {
|
|
const ordo = resolveOrdo('matins', date);
|
|
// The opening versicle itself is gone -- Ps 3 (which it precedes) is
|
|
// gone too, and the nocturn's own psalmody starts the ordo straight
|
|
// off (see the "goes straight into Nocturn 1" test below). A versicle
|
|
// further in belongs to the ferial nocturn's own antiphoned psalmody
|
|
// (matins-ferial-antiphons.yml), not the dropped opening block.
|
|
expect(ordo.parts[0]?.kind).toBe('psalm');
|
|
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 3)).toBe(false);
|
|
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 94)).toBe(false);
|
|
expect(ordo.parts.some((p) => p.kind === 'section-heading' && p.label === 'Invitatory')).toBe(false);
|
|
expect(ordo.parts.some((p) => p.kind === 'hymn')).toBe(false);
|
|
});
|
|
|
|
it('goes straight into Nocturn 1 psalmody as the very first part', () => {
|
|
const ordo = resolveOrdo('matins', HOLY_THURSDAY);
|
|
expect(ordo.parts[0]?.kind).toBe('psalm');
|
|
});
|
|
|
|
it("leaves an ordinary day's opening block untouched", () => {
|
|
const ordo = resolveOrdo('matins', '2026-08-20');
|
|
expect(ordo.parts[0]?.kind).toBe('versicle');
|
|
expect(ordo.parts[1]).toMatchObject({ kind: 'psalm', psalmNumber: 3 });
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("matins", ...) Assumption-octave hymn fallback (2026-08-22 fix)', () => {
|
|
// Real bug: resolveMatinsHymn's own doc comment already claimed the
|
|
// Assumption's octave "keeps the feast's own proper hymn all week," but
|
|
// the code only ever matched when the day's own winner literally *was*
|
|
// `assumption` (Aug 15 itself) -- every other octave day fell straight
|
|
// through to the plain ferial hymn instead. Fixed by adding a real
|
|
// octave-fallback tier (resolveActiveOctave -> `matins-hymn-${octave.id}`).
|
|
function hymnLatin(date: string): string | undefined {
|
|
const ordo = resolveOrdo('matins', date);
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn') as { text: { text: { la: string } } } | undefined;
|
|
return hymn?.text.text.la;
|
|
}
|
|
|
|
it("St. Bernard's own day (Aug 20, no hymn of his own authored) picks up the Assumption's octave hymn, not the plain ferial one", () => {
|
|
expect(hymnLatin('2026-08-20')).toContain('Surge');
|
|
});
|
|
|
|
it('every other interior day governed by the Assumption octave (16, 19, 21 — none with a hymn of their own) also picks up the octave hymn', () => {
|
|
for (const date of ['2026-08-16', '2026-08-19', '2026-08-21']) {
|
|
expect(hymnLatin(date)).toContain('Surge');
|
|
}
|
|
});
|
|
|
|
it("Aug 17 is instead governed by St. Lawrence's own octave (its own elevated closing day, outranking the Assumption's ordinary day here) — no hymn authored for it, so it still falls to the plain ferial one, correctly", () => {
|
|
expect(hymnLatin('2026-08-17')).toContain('Somno');
|
|
});
|
|
|
|
it('Aug 22, the octave-closing day (Immaculate Heart of Mary relocated away), also gets the Assumption hymn via the pre-existing sanctoral-substitution path', () => {
|
|
expect(hymnLatin('2026-08-22')).toContain('Surge');
|
|
});
|
|
|
|
it("a plain ferial day outside the octave still gets the plain ferial hymn, unaffected", () => {
|
|
expect(hymnLatin(FERIAL_DATE)).toContain('Somno');
|
|
});
|
|
});
|
|
|
|
// Temporal-cycle nocturn-readings sweep (2026-08): src/data/propers/
|
|
// nocturn-readings/post-pentecost-{01..24}.yml, sourced from the reference
|
|
// engine's Tempora files. post-pentecost-15 was the original mechanism
|
|
// proof date (see above); this sweep authored the other 23 post-Pentecost
|
|
// ids. Every id is keyed by temporalId, which resolveTemporalId maps
|
|
// identically for every day in that liturgical week (Sunday and its
|
|
// ferias), so one file's content is exercised regardless of which weekday
|
|
// is checked. post-pentecost-13 is the date that originally surfaced the
|
|
// gap (2026-08-23, a real Sunday). See TODO.md and memory
|
|
// vu-temporal-nocturn-sweep-progress for the full sweep record.
|
|
describe('resolveOrdo("matins", ...) post-Pentecost season nocturn-readings sweep (2026-08)', () => {
|
|
function lessonStatuses(date: string) {
|
|
const ordo = resolveOrdo('matins', date);
|
|
return ordo.parts
|
|
.filter((p) => p.kind === 'lesson')
|
|
.map((p) => (p as { text: { status: Record<string, string> } }).text.status);
|
|
}
|
|
|
|
it.each([
|
|
['2026-08-23', 'post-pentecost-13 (today, the reported gap)'],
|
|
['2027-05-23', 'post-pentecost-01, Trinity Sunday'],
|
|
['2026-06-14', 'post-pentecost-02'],
|
|
['2026-11-08', 'post-pentecost-24 (season tail)'],
|
|
])('%s (%s) resolves at least one non-missing lesson', (date) => {
|
|
const statuses = lessonStatuses(date);
|
|
expect(statuses.length).toBeGreaterThan(0);
|
|
expect(statuses.some((s) => s.la !== 'missing' && s.en !== 'missing')).toBe(true);
|
|
});
|
|
|
|
it('every post-pentecost-NN id (01-24) has an authored nocturn-readings file', async () => {
|
|
const { getNocturnReadings } = await import('../../src/propers/nocturn-readings');
|
|
for (let n = 1; n <= 24; n += 1) {
|
|
const id = `post-pentecost-${String(n).padStart(2, '0')}`;
|
|
expect(getNocturnReadings(id).length, id).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// The calendar-month/week Nocturn 2 import (2026-08): the later
|
|
// post-Pentecost Sundays' real Nocturn 2 is keyed by civil calendar
|
|
// month/week (calendar/month-week-id.ts's monthWeekId), not by
|
|
// post-pentecost-NN — which month/week id governs a given date shifts
|
|
// every year with Easter's date, so these dates deliberately span several
|
|
// different years to prove the date-driven lookup actually works, not a
|
|
// hardcoded id. All 20 month-week-*.yml files (August-November) are
|
|
// exercised across these dates. See TODO.md and memory
|
|
// vu-temporal-nocturn-sweep-progress for the full sweep record.
|
|
describe('resolveOrdo("matins", ...) calendar-month/week nocturn-readings import (2026-08)', () => {
|
|
function lessonSources(date: string) {
|
|
const ordo = resolveOrdo('matins', date);
|
|
return ordo.parts.filter((p) => p.kind === 'lesson').map((p) => (p as { label?: string }).label);
|
|
}
|
|
|
|
it.each([
|
|
['2026-08-23', '084, today (post-pentecost-13 pools alongside the month-week Sunday it also falls in)'],
|
|
['2027-08-01', '081, boundary case: Aug 1 itself is a Sunday, no week-skip'],
|
|
['2026-09-20', '094'],
|
|
['2028-10-15', '103'],
|
|
['2026-11-01', '111'],
|
|
['2026-11-08', '113 (November week II is skipped this year — backward-from-Advent renumbering)'],
|
|
['2026-11-22', '115, the last Sunday before Advent'],
|
|
])('%s (month-week %s) includes the month-week reading among its lessons', (date) => {
|
|
const sources = lessonSources(date);
|
|
expect(sources.some((label) => typeof label === 'string' && label.length > 0)).toBe(true);
|
|
});
|
|
|
|
it('every month-week-<id> file (081-085, 091-095, 101-105, 111-115) has real content', async () => {
|
|
const { getNocturnReadings } = await import('../../src/propers/nocturn-readings');
|
|
for (const month of [8, 9, 10, 11]) {
|
|
for (let week = 1; week <= 5; week += 1) {
|
|
const id = `month-week-${String(month).padStart(2, '0')}${week}`;
|
|
expect(getNocturnReadings(id).length, id).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Real bug (2026-08-24 fix): nocturnReadingIds unconditionally pooled the
|
|
// plain temporalId/month-week ids into every day's nocturn-reading pool,
|
|
// even on a day where a real feast wins outright with zero commemorations
|
|
// (decideOccurrence's `ordinary-feria` branch) -- correctly suppressing the
|
|
// temporal identity entirely. St. Bartholomew (duplex-2-classis, 2026-08-24,
|
|
// a Monday) has no Nocturn 3 content of his own, so the leftover 13th-
|
|
// Sunday-after-Pentecost (post-pentecost-13.yml) and month-week (month-
|
|
// week-084.yml) readings wrongly filled his Nocturn 3. Fixed by gating that
|
|
// pooling on whether the day's own occurrence decision actually retained
|
|
// the temporal identity (day.winner.kind === 'temporal', or a `kind:
|
|
// 'temporal'` entry in day.commemorations).
|
|
describe('resolveOrdo("matins", ...) nocturn-reading temporal/month-week suppression (2026-08-24 fix)', () => {
|
|
function lessonLabels(date: string) {
|
|
const ordo = resolveOrdo('matins', date);
|
|
return ordo.parts.filter((p) => p.kind === 'lesson').map((p) => (p as { label?: string }).label);
|
|
}
|
|
|
|
it("St. Bartholomew (2026-08-24, duplex-2-classis, zero commemorations) does not pool the suppressed Sunday/month-week readings", () => {
|
|
const labels = lessonLabels('2026-08-24');
|
|
expect(labels).not.toContain('St. Augustine, Bishop of Hippo, Book 2, Questions on the Gospels, ch. 40');
|
|
expect(labels).not.toContain('St. Gregory the Great, Moralia in Job, Book 1, ch. 10');
|
|
});
|
|
|
|
it("St. Bartholomew's own Nocturn 2 still carries his own proper vita reading", () => {
|
|
const ordo = resolveOrdo('matins', '2026-08-24');
|
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { text: { text: Record<string, string> } }[];
|
|
expect(lessons.some((l) => l.text.text.la?.includes('Bartholomǽus Apóstolus'))).toBe(true);
|
|
});
|
|
|
|
it('a plain ferial weekday with no sanctoral winner (2026-09-04, a gap day in sanctoral-calendar.yml) still pools the plain temporal/month-week readings, unaffected by the gate', () => {
|
|
const ordo = resolveOrdo('matins', '2026-09-04');
|
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { text: { status: Record<string, string> } }[];
|
|
expect(lessons.length).toBeGreaterThan(0);
|
|
expect(lessons.some((l) => l.text.status.la !== 'missing' && l.text.status.en !== 'missing')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Coverage gap fix (no bug found): the pre-existing ferial-branch test
|
|
// above only asserted the opening invitatory antiphon *contains* "Veníte,"
|
|
// true for both the incipit ("Ant. Veníte.") and full ("Ant. Veníte, *
|
|
// Exsultémus Dómino.") forms -- it never actually distinguished doubled vs.
|
|
// undoubled. isDoubleOrHigher/openingAntiphon traced out correct in source
|
|
// for every rank tested here; these tests just make the distinction real.
|
|
describe('resolveOrdo("matins", ...) invitatory antiphon doubling by rank', () => {
|
|
function openingAntiphonLatin(date: string): string | undefined {
|
|
const ordo = resolveOrdo('matins', date);
|
|
const invitatoryPsalm = ordo.parts.find((p) => p.kind === 'psalm' && (p as { psalmNumber: number }).psalmNumber === 94) as
|
|
| { antiphon?: { text: Record<string, string> } }
|
|
| undefined;
|
|
return invitatoryPsalm?.antiphon?.text.la;
|
|
}
|
|
|
|
it('a Duplex+ weekday winner opens with the FULL (not incipit-only) invitatory antiphon (St. Lawrence, 2026-08-17: Common-of-a-Martyr text, no proper of his own; St. Bartholomew, duplex-2-classis, 2026-08-24: Common-of-an-Apostle text)', () => {
|
|
// St. Lawrence has his own full proper Matins psalmody
|
|
// (matins-psalmody-overrides/saints/st-lawrence.yml) but no proper
|
|
// invitatory antiphon of his own -- resolveMatinsInvitatoryText falls
|
|
// to his common-of-a-martyr Common tier (matins-invitatory-common-of-
|
|
// a-martyr.yml, authored 2026-08-27), not the plain ferial default.
|
|
expect(openingAntiphonLatin('2026-08-17')).toContain('Regem Mártyrum Dóminum, * Veníte');
|
|
expect(openingAntiphonLatin('2026-08-24')).toContain('Regem Apostolórum Dóminum, * Veníte');
|
|
});
|
|
|
|
it('a plain ferial day opens with the incipit only', () => {
|
|
expect(openingAntiphonLatin(FERIAL_DATE)).not.toContain('Exsultémus');
|
|
});
|
|
|
|
it("a Simplex saint merely commemorated (not winning) under an Advent feria still opens with the incipit only — the winner's own rank governs, not any commemorated saint's (St. Bibiana, 2025-12-02)", () => {
|
|
expect(openingAntiphonLatin('2025-12-02')).not.toContain('Exsultémus');
|
|
});
|
|
});
|
|
|
|
// New mechanism (2026-08): resolveMatinsHymn gained a Common-of-Saints
|
|
// tier (override -> octave -> Common category -> season -> ferial), and
|
|
// the invitatory antiphon -- previously one hardcoded fixed text for every
|
|
// day of the year -- now goes through the same shape of tiering
|
|
// (resolveMatinsInvitatoryText: override -> Common category -> season ->
|
|
// ferial). Both confirmed against the reference engine's own Commune/
|
|
// C1.txt (Common of an Apostle): hymn "Ætérna Christi múnera," invitatory
|
|
// "Regem Apostolórum Dóminum." St. Bartholomew has no proper Matins hymn
|
|
// or invitatory of his own authored, so this Common tier is what actually
|
|
// renders for him.
|
|
describe('resolveOrdo("matins", ...) Common-of-an-Apostle hymn + invitatory (2026-08-24 fix)', () => {
|
|
function hymnLatin(date: string): string | undefined {
|
|
const ordo = resolveOrdo('matins', date);
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn') as { text: { text: { la: string } } } | undefined;
|
|
return hymn?.text.text.la;
|
|
}
|
|
function invitatoryFullLatin(date: string): string | undefined {
|
|
const ordo = resolveOrdo('matins', date);
|
|
const antiphonParts = ordo.parts.filter((p) => p.kind === 'antiphon') as { text: { text: Record<string, string> } }[];
|
|
return antiphonParts[0]?.text.text.la;
|
|
}
|
|
|
|
it("St. Bartholomew's own day (2026-08-24, no proper Matins hymn of his own) picks up the Common-of-an-Apostle hymn", () => {
|
|
expect(hymnLatin('2026-08-24')).toContain('Ætérna Christi múnera');
|
|
});
|
|
|
|
it("St. Bartholomew's invitatory antiphon resolves to the Common-of-an-Apostle text", () => {
|
|
expect(invitatoryFullLatin('2026-08-24')).toContain('Regem Apostolórum Dóminum');
|
|
});
|
|
|
|
it('St. Andrew (2026-11-30, also common-of-an-apostle, no proper hymn/invitatory of his own) resolves to the same Common text -- confirms the tier generalizes beyond Bartholomew', () => {
|
|
expect(hymnLatin('2026-11-30')).toContain('Ætérna Christi múnera');
|
|
expect(invitatoryFullLatin('2026-11-30')).toContain('Regem Apostolórum Dóminum');
|
|
});
|
|
|
|
it('a plain ferial day is unaffected -- still falls through to the plain ferial hymn and invitatory antiphon', () => {
|
|
expect(hymnLatin(FERIAL_DATE)).toContain('Somno');
|
|
expect(invitatoryFullLatin(FERIAL_DATE)).toContain('Veníte');
|
|
expect(invitatoryFullLatin(FERIAL_DATE)).not.toContain('Regem Apostolórum');
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("matins", ...) remaining Common categories\' Matins hymns (2026-08-26)', () => {
|
|
// Same tier this file's earlier "Common-of-an-Apostle hymn" describe
|
|
// block already proved (proper > octave > Common > season > ferial) --
|
|
// this just extends the same live-verified content to the categories
|
|
// authored alongside this session's Lauds/Vespers pass.
|
|
function hymnLatin(date: string): string | undefined {
|
|
const ordo = resolveOrdo('matins', date);
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn') as { text: { text: { la: string } } } | undefined;
|
|
return hymn?.text.text.la;
|
|
}
|
|
|
|
it('Ss. Placid and Companions (common-of-several-martyrs) resolve to "Ætérna Christi múnera, Et Mártyrum victórias" -- distinct from the Apostle hymn\'s shared opening line', () => {
|
|
expect(hymnLatin('2026-10-05')).toContain('Ætérna Christi múnera');
|
|
expect(hymnLatin('2026-10-05')).toContain('Mártyrum victórias');
|
|
});
|
|
|
|
it('St. Maurus (common-of-an-abbot) resolves to Common-of-a-Confessor-Not-Bishop\'s own hymn, same finding as the Lauds/Vespers pass', () => {
|
|
expect(hymnLatin('2026-01-15')).toContain('Iste Conféssor Dómini sacrátus');
|
|
});
|
|
|
|
it('St. Apollonia (common-of-a-virgin-martyr, her own clean year 2029-02-09) resolves to "Vírginis Proles"', () => {
|
|
expect(hymnLatin('2029-02-09')).toContain('Vírginis Proles');
|
|
});
|
|
|
|
it('the Guardian Angels, St. Michael, St. Gabriel, and St. Raphael all resolve to "Tibi Christe splendor Patris," reused from St. Michael\'s Apparition (same real proper-content-inheritance finding as Lauds/Vespers)', () => {
|
|
for (const date of ['2026-10-02', '2026-09-29', '2026-03-24', '2026-10-24']) {
|
|
expect(hymnLatin(date)).toContain('Tibi Christe splendor Patris');
|
|
}
|
|
});
|
|
|
|
it('St. Anne (common-of-a-holy-woman) and St. Monica (common-of-a-widow) resolve to the same shorter "Hujus obténtu, Deus alme" form (just the closing two stanzas of the Virgin hymn)', () => {
|
|
expect(hymnLatin('2026-07-26')).toContain('Hujus obténtu, Deus alme');
|
|
expect(hymnLatin('2026-05-04')).toContain('Hujus obténtu, Deus alme');
|
|
});
|
|
|
|
it('Our Lady of Mount Carmel (common-of-the-bvm) resolves to "Quem terra, pontus, ǽthera"', () => {
|
|
expect(hymnLatin('2026-07-16')).toContain('Quem terra, pontus, ǽthera');
|
|
});
|
|
|
|
it('the Finding and Exaltation of the Holy Cross resolve to their own proper "Pange, lingua, gloriósi" (shared between the two ids)', () => {
|
|
expect(hymnLatin('2027-05-03')).toContain('Pange, lingua, gloriósi');
|
|
expect(hymnLatin('2026-09-14')).toContain('Pange, lingua, gloriósi');
|
|
});
|
|
|
|
it('the Transfiguration resolves to its own proper "Quicúmque Christum quǽritis," byte-identical to its own Vespers hymn', () => {
|
|
expect(hymnLatin('2026-08-06')).toContain('Quicúmque Christum quǽritis');
|
|
});
|
|
});
|