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 } }).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); }); 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 }; }[]; 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 } }).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 } }[]; // Opening Deus-in-adjutorium versicle + one per nocturn (1, 2, 3). expect(versicles).toHaveLength(4); 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.'); }); 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 } }[]; // 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 } }[]; 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("falls through to its Common category's own generic psalmody for a 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, confirmed clean (no // Sunday collision) per his own saint-file comment. // getMatinsPsalmodyOverride's Common tier resolves // common-of-a-martyr-bishop via common-of-a-martyr.yml's own // `aliases` (the reference engine's own Matins psalmody for // Martyr-Bishop redirects outright to plain Martyr) -- real, // verified Common text, not the plain ferial table. const common = resolveOrdo('matins', '2029-02-01'); const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber); // Same Nocturn 1 as St. Lawrence's own (1,2,4,5,8,10), but Nocturn 2 // uses the Common default's Ps 64, not Lawrence's own proper Ps 16. expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 20, 23, 63, 64, 91]); expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true); expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true); const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record } }[]; const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber)); // The generic Common antiphon text is real and verified, even though // it is not proper to Ignatius himself. for (const p of nocturnPsalms) { expect(p.antiphon?.status.la).toBe('verified'); expect(p.antiphon?.status.en).toBe('verified'); } }); it("falls through to Common-of-a-Confessor's own generic psalmody for a Duplex+ weekday winner with no proper antiphon authored", () => { // St. Francis of Paola (Duplex, Common of a Confessor Not a Bishop, // no proper antiphon of his own) -- 2030-04-02, confirmed clean (no // Sunday collision, safely outside Holy Week/Easter octave that year) // per his own saint-file comment. 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, 1, 2, 4, 5, 8, 10, 14, 20, 23, 95, 96, 97]); expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true); expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true); const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record } }[]; const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber)); for (const p of nocturnPsalms) { expect(p.antiphon?.status.la).toBe('verified'); expect(p.antiphon?.status.en).toBe('verified'); } }); it("falls through to Common-of-a-Confessor-Bishop's own generic psalmody -- same psalm numbers as Common of a Confessor, different Nocturn versicles", () => { // 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, confirmed clean per his own saint-file // comment. const common = resolveOrdo('matins', '2035-06-14'); const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber); // Same psalm numbers in every slot as Common of a Confessor Not a // Bishop above -- the two Commons only differ in their versicles. expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 20, 23, 95, 96, 97]); expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true); expect(common.parts.some((p) => p.kind === 'canticle')).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', () => { // Ss. Fabian and Sebastian (Duplex, Common of Several Martyrs -- no // matins-psalmody-overrides category authored yet for either tier) // -- 2029-01-20, a Saturday, confirmed clean (no Sunday collision) // per their own saint-file comment. const fallback = resolveOrdo('matins', '2029-01-20'); const psalms = fallback.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber); // Ps 3 + 94, then Saturday's own 9-psalm ferial table // (psalter-distribution.yml), not any Common-of-Several-Martyrs scheme. expect(psalms[0]).toBe(3); expect(psalms[1]).toBe(94); expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'saturday').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 } }[]; 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 } }[]; 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 } }).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- 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); } } }); });