4adbb71ddf
Deploy / deploy (push) Successful in 1m27s
Following the Ascensiontide fix, swept every remaining unverified Sunday/ feria precedence tier against the live reference engine: - commemorations.ts's privileged-sunday rule was itself wrong: a duplex-majus+ candidate was given a bare commemoration instead of transferring, never actually verified. Live-checked the Immaculate Conception (transfers off Advent II) and the Annunciation (transfers off Palm/Easter Sunday) - confirmed correct by direct instruction. privileged-sunday now transfers every rank unconditionally. - eastertide's own Sunday tier was backwards (privileged-sunday instead of ordinary-sunday) for the ordinary Sundays after Easter - live- verified twice (Finding of the Holy Cross, St. Mark both win outright). Easter Day itself still needs the stronger tier, now special-cased directly since Sundays never consult the offset table. Six existing tests updated to reflect real, live-verified behavior - each had the old wrong "commemorated in place" assumption baked into its expected value. Remaining unverified entries (Vigil of Pentecost/ Christmas, Corpus Christi/Sacred Heart) documented as such rather than silently assumed correct. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
202 lines
11 KiB
TypeScript
202 lines
11 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveOrdo } from '../../src/hours';
|
|
|
|
const MONDAY = '2026-06-15';
|
|
const SUNDAY = '2026-06-14';
|
|
|
|
describe('resolveOrdo("compline", ...)', () => {
|
|
it('is implemented, with fixed psalms 4, 90, 133 regardless of weekday', () => {
|
|
const monday = resolveOrdo('compline', MONDAY);
|
|
const sunday = resolveOrdo('compline', SUNDAY);
|
|
expect(monday.notImplemented).toBeUndefined();
|
|
|
|
const psalmNumbers = (ordo: typeof monday) =>
|
|
ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
|
expect(psalmNumbers(monday)).toEqual([4, 90, 133]);
|
|
expect(psalmNumbers(sunday)).toEqual([4, 90, 133]);
|
|
});
|
|
|
|
it('gives the psalms no antiphon (real ferial Compline has none)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm');
|
|
for (const psalm of psalms) {
|
|
expect(psalm.kind === 'psalm' ? psalm.antiphon : 'should be undefined').toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('resolves the opening versicle and the pre-psalm Confiteor (before the psalms, unlike Prime)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const firstPsalmIndex = ordo.parts.findIndex((p) => p.kind === 'psalm');
|
|
const confiteorIndex = ordo.parts.findIndex(
|
|
(p) => p.kind === 'preces' && p.text.text.la?.includes('Confíteor Deo'),
|
|
);
|
|
const openingIndex = ordo.parts.findIndex((p) => p.kind === 'versicle');
|
|
expect(confiteorIndex).toBeGreaterThanOrEqual(0);
|
|
expect(openingIndex).toBeGreaterThan(confiteorIndex);
|
|
expect(firstPsalmIndex).toBeGreaterThan(openingIndex);
|
|
});
|
|
|
|
it("appends the hymn's own (per-annum) doxology, distinct from Prime's", () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
|
expect(hymn && hymn.kind === 'hymn' ? hymn.text.text.en : undefined).toContain(
|
|
'O Father, that we ask be done',
|
|
);
|
|
});
|
|
|
|
it('gives the capitulum its scripture citation (fixed, not Sunday/feria-split)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
|
expect(chapter && chapter.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Jer. 14:9');
|
|
});
|
|
|
|
it('resolves the Nunc Dimittis as a canticle with an opening antiphon, plus a full antiphon reprise', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const canticle = ordo.parts.find((p) => p.kind === 'canticle');
|
|
expect(canticle && canticle.kind === 'canticle' ? canticle.canticleId : undefined).toBe('nunc-dimittis');
|
|
expect(canticle && canticle.kind === 'canticle' ? canticle.text.text.en : undefined).toContain(
|
|
'Now ✠ thou dost dismiss thy servant',
|
|
);
|
|
expect(canticle && canticle.kind === 'canticle' ? canticle.antiphon?.text.en : undefined).toBe(
|
|
'Ant. Protect us.',
|
|
);
|
|
|
|
const canticleIndex = ordo.parts.indexOf(canticle!);
|
|
const closingAntiphon = ordo.parts[canticleIndex + 1];
|
|
expect(closingAntiphon?.kind).toBe('antiphon');
|
|
// The "*" chant mark is preserved in the full (closing) form.
|
|
expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.en : undefined).toBe(
|
|
'Ant. Protect us, * Lord, while we are awake and safeguard us while we sleep; that we may keep watch with Christ, and rest in peace.',
|
|
);
|
|
});
|
|
|
|
it('includes the fuller Tridentine 1906 Preces (the triple "Blessed art thou" exchange Monastic drops)', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
|
|
expect(preces && preces.kind === 'preces' ? preces.text.text.la : undefined).toContain('Benedíctus es, Dómine');
|
|
expect(preces && preces.kind === 'preces' ? preces.text.text.la : undefined).toContain('Miserére nostri');
|
|
});
|
|
|
|
it('substitutes the short Kyrie/Pater litany for the fuller Preces on a Double-or-higher, rather than omitting it entirely', () => {
|
|
// 2026-08-15, the Assumption (Duplex I. classis), keeps her own
|
|
// evening outright. Real practice (both Monastic and secular tracks)
|
|
// drops Compline's Preces to nothing on a Double -- vu deliberately
|
|
// substitutes the plain short litany instead, so a short litany +
|
|
// Our Father is always said. See hours/compline.ts's 'preces' case
|
|
// and data/hours/compline.yml's own header.
|
|
const ordo = resolveOrdo('compline', '2026-08-15');
|
|
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
|
|
expect(preces).toBeDefined();
|
|
const la = preces && preces.kind === 'preces' ? preces.text.text.la : undefined;
|
|
expect(la).toContain('Pater noster');
|
|
expect(la).toContain('Dómine, exáudi oratiónem meam');
|
|
expect(la).not.toContain('Benedíctus es, Dómine');
|
|
expect(la).not.toContain('Miserére nostri');
|
|
});
|
|
|
|
it('closes with the Salve Regina (the per-annum default Marian antiphon), labeled by name', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const last = ordo.parts[ordo.parts.length - 1];
|
|
expect(last?.kind).toBe('preces');
|
|
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Salve Regina');
|
|
expect(last?.kind === 'preces' ? last.text.text.en : undefined).toContain('Hail holy Queen');
|
|
});
|
|
|
|
it('anticipates Advent I: the Saturday evening before switches to the Alma Redemptoris Mater and shows the anticipated day label', () => {
|
|
// Nov 30, 2025 is Advent I Sunday -- also St. Andrew's own day
|
|
// (Duplex II. classis), but a privileged Sunday transfers him
|
|
// entirely rather than commemorating him in place (live-verified
|
|
// 2026-09-01, see tests/calendar/day-label.test.ts). This evening
|
|
// also anticipates Advent I from Nov 29, itself the Vigil of St.
|
|
// Andrew -- calendar/vespers.ts's own isVigilOfTomorrow check only
|
|
// suppresses the Vigil's own commemoration when tomorrow's page
|
|
// *also* shows St. Andrew (which would be redundant); since he no
|
|
// longer does (he's transferred away, not commemorated), both pieces
|
|
// of information are genuinely distinct and both show.
|
|
const eve = resolveOrdo('compline', '2025-11-29');
|
|
const last = eve.parts[eve.parts.length - 1];
|
|
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
|
|
expect(eve.dayLabel?.en).toBe(
|
|
'The 1st Sunday of Advent (Semiduplex) — Vigil of St. Andrew (of today) — St. Andrew, Apostle (transferred away)',
|
|
);
|
|
|
|
const dayBefore = resolveOrdo('compline', '2025-11-28');
|
|
const lastBefore = dayBefore.parts[dayBefore.parts.length - 1];
|
|
expect(lastBefore?.kind === 'preces' ? lastBefore.label : undefined).toBe('Salve Regina');
|
|
});
|
|
|
|
it('switches to the Ave Regina Caelorum from Candlemas through the day before Maundy Thursday', () => {
|
|
const marianLabel = (date: string) => {
|
|
const ordo = resolveOrdo('compline', date);
|
|
const last = ordo.parts[ordo.parts.length - 1];
|
|
return last?.kind === 'preces' ? last.label : undefined;
|
|
};
|
|
// Easter 2026 is Apr 5, so Maundy Thursday is Apr 2.
|
|
expect(marianLabel('2026-01-31')).toBe('Salve Regina'); // well before Candlemas
|
|
// Candlemas (Feb 2) has First Vespers — Compline on Feb 1 evening
|
|
// already anticipates it, same as Advent I and Easter do (see
|
|
// calendar/vespers.ts).
|
|
expect(marianLabel('2026-02-01')).toBe('Ave Regina Caelorum'); // eve of Candlemas, anticipated
|
|
expect(marianLabel('2026-02-02')).toBe('Ave Regina Caelorum'); // Candlemas itself
|
|
expect(marianLabel('2026-03-10')).toBe('Ave Regina Caelorum'); // mid-Lent
|
|
expect(marianLabel('2026-04-01')).toBe('Ave Regina Caelorum'); // eve of Maundy Thursday
|
|
expect(marianLabel('2026-04-02')).not.toBe('Ave Regina Caelorum'); // Maundy Thursday itself
|
|
});
|
|
|
|
it('closes the Short Reading with "Deo gratias", via the shared lesson-close proper', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const shortReadingIndex = ordo.parts.findIndex((p) => p.kind === 'lesson' && p.label === 'Short Reading');
|
|
const closing = ordo.parts[shortReadingIndex + 1];
|
|
expect(closing?.kind).toBe('preces');
|
|
expect(closing?.kind === 'preces' ? closing.text.text.en : undefined).toContain('Thanks be to God');
|
|
});
|
|
|
|
it('includes the "In manus tuas" responsory between the capitulum and the versicle', () => {
|
|
const ordo = resolveOrdo('compline', MONDAY);
|
|
const chapterIndex = ordo.parts.findIndex((p) => p.kind === 'chapter');
|
|
const responsoryIndex = ordo.parts.findIndex((p) => p.kind === 'responsory');
|
|
const versicleIndex = ordo.parts.findIndex((p) => p.kind === 'versicle' && p.text.text.la?.includes('Custódi nos'));
|
|
expect(responsoryIndex).toBeGreaterThan(chapterIndex);
|
|
expect(versicleIndex).toBeGreaterThan(responsoryIndex);
|
|
const responsory = ordo.parts[responsoryIndex];
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.en : undefined).toContain(
|
|
'Into thy hands, O Lord',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('resolveOrdo("compline", ...) responsory Gloria omission', () => {
|
|
// Passiontide (Passion Sunday through Holy Saturday) is a wider window
|
|
// than the psalm/canticle Gloria's own Triduum-only rule, and doesn't
|
|
// apply on a Sancti feast's own day even within that window --
|
|
// live-verified: 2027-03-19 is St. Joseph (Duplex I), landing inside
|
|
// Passiontide that year, keeping "In manus tuas"'s Gloria; 2027-03-15
|
|
// is a plain ferial in the same window, and omits it.
|
|
const FERIAL_PASSIONTIDE = '2027-03-15';
|
|
const FEAST_IN_PASSIONTIDE = '2027-03-19'; // St. Joseph, Duplex I.
|
|
const ORDINARY_DATE = '2026-08-20';
|
|
|
|
it('omits the Gloria on a ferial Passiontide day', () => {
|
|
const ordo = resolveOrdo('compline', FERIAL_PASSIONTIDE);
|
|
const responsory = ordo.parts.find((p) => p.kind === 'responsory');
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.la : undefined).not.toContain('Glória Patri');
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.en : undefined).not.toContain(
|
|
'Glory be to the Father',
|
|
);
|
|
// Everything else about the responsory is untouched.
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.en : undefined).toContain('Into thy hands, O Lord');
|
|
});
|
|
|
|
it("keeps the Gloria on a Sancti feast's own day, even inside Passiontide", () => {
|
|
const ordo = resolveOrdo('compline', FEAST_IN_PASSIONTIDE);
|
|
const responsory = ordo.parts.find((p) => p.kind === 'responsory');
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.la : undefined).toContain('Glória Patri');
|
|
});
|
|
|
|
it('keeps the Gloria on an ordinary date outside Passiontide', () => {
|
|
const ordo = resolveOrdo('compline', ORDINARY_DATE);
|
|
const responsory = ordo.parts.find((p) => p.kind === 'responsory');
|
|
expect(responsory?.kind === 'responsory' ? responsory.text.text.la : undefined).toContain('Glória Patri');
|
|
});
|
|
});
|