143244203d
Deploy / deploy (push) Successful in 52s
Real, multi-part bug found by looking closely at today's rendering (day 3
of St. Lawrence's octave, St. Clare commemorated): the psalmody
antiphons, hymn, chapter/responsory/versicle, Benedictus antiphon, and
day collect were all keyed off `day.winner` directly, which stays the
plain temporal identity (post-pentecost-11) throughout an octave --
St. Lawrence's own already-authored psalmody override and collect never
engaged on any day but his actual feast day. Live-verified (Tridentine
1910, 2026-08-12) that the whole office on an ordinary octave day
actually comes from the octave's own feast ("{ex Commune aut Festo}" /
"{ex Proprio Sanctorum}"), with a *weaker* commemorated saint (Clare)
getting a separate Ant+V/R+collect block of her own alongside it.
Fixed with a new resolveOfficeWinner(day) in hours/resolve-common.ts,
used everywhere getDayCollect/getDayCollects/getBenedictusAntiphon/
getPsalmodyOverrideFor used to read day.winner directly. Deliberately
gated on temporalCategory === 'ordinary-feria' (the day has no standing
of its own) rather than "any active octave" -- the Christmas Octave's
own stacked octaves are the live-verified counterexample where the
*temporal* day keeps the office instead, each octave becoming its own
separate commemoration block (not yet modeled, flagged in TODO.md).
Also fixes the ferial Preces gate (added in the previous commit) to
exclude active octaves the same way the suffrages already do --
live-verified "Preces Feriales{omittitur}" on the same date.
getDayCollects now renders a sanctoral commemoration as a real
Ant+V/R+collect bundle when authored (${propers}-commemoration.yml),
not just a bare, unlabeled collect -- the old rendering was indeed the
confusing "PRAYER / (translation pending)" block spotted today, which
was St. Clare's placeholder with no indication whose it was or why.
Authored her real commemoration bundle and standalone collect from the
same live query rather than leaving it pending.
One correction to a guess floated mid-session: the Benedictus antiphon
does NOT go to the commemorated saint (Clare) -- live-verified it stays
with the octave's own feast (Lawrence's "In cratícula"), same as
everything else in the primary office; only the separate commemoration
block is Clare's.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
5.4 KiB
TypeScript
100 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveOrdo } from '../../src/hours';
|
|
|
|
describe('Lauds psalmody override (duplex-majus+ sanctoral, and Marian Saturday)', () => {
|
|
it("substitutes St. Lawrence's own proper psalmody on his own day (Duplex II. classis, above the threshold)", () => {
|
|
const ordo = resolveOrdo('lauds', '2026-08-10');
|
|
const psalmNumbers = ordo.parts
|
|
.filter((p) => p.kind === 'psalm')
|
|
.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
|
expect(psalmNumbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
|
|
|
|
const ps92 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 92);
|
|
expect(ps92?.kind === 'psalm' ? ps92.antiphon?.text.en : undefined).toContain('Lawrence went in to be a martyr');
|
|
|
|
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus');
|
|
expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum');
|
|
});
|
|
|
|
it("doesn't override a saint below duplex-majus (e.g. Semiduplex) -- plain weekday psalmody still used", () => {
|
|
// 2026-09-19: St. Januarius, Semiduplex (below duplex-majus) -- see
|
|
// tests/calendar/marian-saturday.test.ts for confirming he wins
|
|
// outright as the day's winner despite it being a Saturday.
|
|
const ordo = resolveOrdo('lauds', '2026-09-19');
|
|
const psalmNumbers = ordo.parts
|
|
.filter((p) => p.kind === 'psalm')
|
|
.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
|
// Saturday's own plain default (Ps 50 + 142), not the Common-shaped
|
|
// override -- no lauds-psalmody-overrides/st-januarius.yml exists.
|
|
expect(psalmNumbers).toEqual([66, 50, 142, 148, 149, 150]);
|
|
});
|
|
|
|
it("substitutes Our Lady's Saturday own proper psalmody, unconditionally, whenever it wins", () => {
|
|
const ordo = resolveOrdo('lauds', '2026-10-24'); // a clean, otherwise-empty Marian Saturday
|
|
const psalmNumbers = ordo.parts
|
|
.filter((p) => p.kind === 'psalm')
|
|
.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
|
expect(psalmNumbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
|
|
|
|
const ps92 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 92);
|
|
expect(ps92?.kind === 'psalm' ? ps92.antiphon?.text.la : undefined).toContain('Dum esset Rex');
|
|
|
|
const benedictus = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus');
|
|
expect(benedictus?.kind === 'canticle' ? benedictus.antiphon?.text.la : undefined).toContain('Beáta Dei Génitrix');
|
|
|
|
const collect = ordo.parts.find((p) => p.kind === 'prayer');
|
|
expect(collect?.kind === 'prayer' ? collect.text.text.en : undefined).toContain('glorious intercession of the Blessed Mary');
|
|
});
|
|
|
|
it("drops the Holy Cross and Blessed Virgin Mary suffrages on Our Lady's Saturday (the latter redundant with the day's already-Marian office; the former because Marian Saturday isn't a bare ferial office either), keeping Joseph, Apostles, and Peace", () => {
|
|
const ordo = resolveOrdo('lauds', '2026-10-24');
|
|
const labels = ordo.parts.filter((p) => p.kind === 'preces' && p.label).map((p) => (p.kind === 'preces' ? p.label : undefined));
|
|
expect(labels).toEqual(['Of St. Joseph', 'Of the Holy Apostles Peter and Paul', 'For Peace', 'Salve Regina']);
|
|
});
|
|
|
|
it("substitutes Christ the King's own proper psalmody and office bundle, and omits the suffrages entirely (Duplex I. classis)", () => {
|
|
const ordo = resolveOrdo('lauds', '2026-10-25');
|
|
const psalmNumbers = ordo.parts
|
|
.filter((p) => p.kind === 'psalm')
|
|
.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
|
expect(psalmNumbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
|
|
|
|
const ps92 = ordo.parts.find((p) => p.kind === 'psalm' && p.psalmNumber === 92);
|
|
expect(ps92?.kind === 'psalm' ? ps92.antiphon?.text.en : undefined).toContain('The God of heaven');
|
|
|
|
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
|
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Col 1:12-13');
|
|
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
|
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Vexílla Christus ínclita');
|
|
|
|
const benedictus = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus');
|
|
expect(benedictus?.kind === 'canticle' ? benedictus.antiphon?.text.en : undefined).toContain(
|
|
'He hath made us',
|
|
);
|
|
|
|
// One extra collect (the commemorated Sunday's own, still an
|
|
// unlabeled 'prayer' -- a temporal commemoration, not a sanctoral
|
|
// one) plus the Simplex saint already commemorated under that Sunday
|
|
// before Christ the King displaced it too, now a labeled 'preces'
|
|
// block rather than a third bare 'prayer' -- see
|
|
// tests/calendar/christ-the-king.test.ts.
|
|
const collects = ordo.parts.filter((p) => p.kind === 'prayer');
|
|
expect(collects.length).toBe(2);
|
|
const sanctoralCommemoration = ordo.parts.find(
|
|
(p) => p.kind === 'preces' && p.label?.startsWith('Commemoration of'),
|
|
);
|
|
expect(sanctoralCommemoration).toBeDefined();
|
|
|
|
const SUFFRAGE_LABELS = new Set([
|
|
'Of the Holy Cross',
|
|
'Of the Blessed Virgin Mary',
|
|
'Of St. Joseph',
|
|
'Of the Holy Apostles Peter and Paul',
|
|
'For Peace',
|
|
]);
|
|
const suffrages = ordo.parts.filter((p) => p.kind === 'preces' && p.label && SUFFRAGE_LABELS.has(p.label));
|
|
expect(suffrages).toEqual([]);
|
|
});
|
|
});
|