Files
vu/tests/hours/resolve-common.test.ts
T
will ea15e909df
Deploy / deploy (push) Successful in 55s
Resolve 15 "unwinnable" stubs' Benedictus antiphons from source directly
Direct feedback on the previous commit's framing: "unwinnable" was
describing an artifact of Divinum Officium's own single-winner
precedence model, not a real content gap -- this app already renders
commemorated saints on their own terms and doesn't need a saint to win
a live query to have real content. Fixed by reading each of the 15
saints' own already-validated fileref directly (the exact same fileref
their collect was already sourced from during the Kalendaria-table
pull) for its [Ant 1] section, falling back to whatever Common its own
[Rule]/[Rank] "vide"/"ex CXX" reference names when there isn't one --
the same proper-then-Common shape getDayCollect/collectCommon already
uses, just applied to the Benedictus antiphon too.

14 of the 15 had no [Ant 1] of their own at all -- clean Common
fallback, and every one of their C-numbers falls inside a family
already live-query-verified in the previous commit, so
minorHoursCommon/benedictusCommon were wired directly with full
confidence: St. Marcus, St. Felix (Presbyter), St. Hermes, St.
Saturninus, St. Prisca, St. Thecla, St. Catherine of Siena, Ss.
Machabees, Ss. Marius/Martha/Audifax/Abachum, Ss. Twelve Brothers, Ss.
Alexander/Eventius/Theodulus/Juvenal, Ss. Felix/Simplicius/
Faustinus/Beatrix, Ss. Nabor and Felix. St. Felicitas needed one new
category (C7), but it turned out to be a cross-reference resolving to
text byte-identical to the already-verified common-of-a-virgin.

The 1 remaining (Ss. Euphemia, Lucy, and Geminian) does have her own
real [Ant 1] -- but marked draft, not verified, on principle: St.
Alexius's own file also has a genuine [Ant 1] that his actual
live-rendered Benedictus antiphon doesn't use (common-of-an-abbot's
text instead), so a raw [Ant 1] read alone isn't sufficient proof, and
for a saint that can never be live-checked at all that uncertainty
can't be resolved either way -- used the text anyway, honestly flagged
rather than claimed as confirmed.

St. Frances of Rome and St. Bibiana (the older, pre-Kalendaria-table
109-saint pass) are good candidates for this same fix but weren't
touched here, since their own fileref was never derived by this
session's work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 09:38:12 -04:00

171 lines
8.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { resolveDay } from '../../src/calendar';
import {
getDayCollect,
getBenedictusAntiphon,
resolveMinorHourAntiphon,
resolveMinorHourChapter,
splitNamedAntiphon,
} from '../../src/hours/resolve-common';
import type { LiturgicalDay } from '../../src/calendar/types';
function syntheticSanctoralDay(id: string): LiturgicalDay {
return {
date: '2026-01-01',
weekday: 'thursday',
season: 'trinitytide',
temporalCategory: 'ordinary-feria',
winner: { kind: 'sanctoral', id, name: id, rank: 'simplex' },
commemorations: [],
};
}
describe('getDayCollect', () => {
it('resolves a real, verified collect for an ordinary Sunday', () => {
const day = resolveDay('2026-08-09'); // post-pentecost-11
const collect = getDayCollect(day);
expect(collect.status.la).toBe('verified');
expect(collect.status.en).toBe('verified');
expect(collect.text.en).toContain('Let us pray.');
});
it('resolves a real, verified collect for a ferial day via its governing Sunday', () => {
const day = resolveDay('2026-06-14'); // post-pentecost-03's Sunday
const collect = getDayCollect(day);
expect(collect.status.la).toBe('verified');
});
it('resolves as honestly missing for a saint with neither a proper collect nor a Common fallback authored', () => {
// Every real saint in data/calendar/saints/*.yml now has either a
// proper collect or a collectCommon fallback (see TODO.md's
// Kalendaria-table pull, 2026-08) -- so this exercises the "missing"
// path with a synthetic winner rather than hunting for a real
// example that no longer exists.
const collect = getDayCollect(syntheticSanctoralDay('example-confessor'));
expect(collect.status.la).toBe('missing');
expect(collect.status.en).toBe('missing');
});
});
describe('genuinely unwinnable stubs still get a Benedictus antiphon (2026-08)', () => {
// These saints are always commemorated, never the day's own rendered
// title (see e.g. st-marcus.yml's own comment) -- so unlike the rest of
// this project's content, their own [Ant 1] (or lack of one) had to be
// pulled directly from source rather than confirmed against a live
// Lauds query, since no such query is ever possible for them. A
// synthetic winner is used here for the same reason: resolveDay would
// never actually pick one of these as day.winner.
it("resolves via its Common category when the saint's own file has no [Ant 1] at all", () => {
// St. Marcus -- [Rule] says "vide C4b" with no override; C4b is a
// whole-file inherit of C4 (just the title relabeled "of the Popes"),
// the same family already live-query-verified for
// common-of-a-confessor-bishop.yml.
const ben = getBenedictusAntiphon(syntheticSanctoralDay('st-marcus'));
expect(ben.status).toEqual({ la: 'verified', en: 'verified' });
});
it('resolves via a cross-referenced Common category (C7 -> Commune/C6:Ant 2)', () => {
// St. Felicitas -- [Rule] says "vide C7", a category this project
// hadn't touched before; C7's own [Ant 1] is itself a reference to
// Commune/C6:Ant 2, which resolves to text byte-identical to
// common-of-a-virgin's own already-verified Benedictus antiphon.
const ben = getBenedictusAntiphon(syntheticSanctoralDay('st-felicitas'));
expect(ben.status).toEqual({ la: 'verified', en: 'verified' });
});
it("resolves its own file's [Ant 1] as draft, not verified, when it can't be checked against a live rendering", () => {
// Ss. Euphemia, Lucy, and Geminian -- her own file *does* have an
// [Ant 1], but St. Alexius (a winnable saint checked earlier in this
// same 2026-08 pass) proved a real [Ant 1] doesn't always end up the
// rendered Benedictus antiphon either (his own live Lauds query uses
// common-of-an-abbot's text instead) -- so a raw-file [Ant 1] read is
// never enough on its own to claim `verified`, and even less so for a
// saint no live query can ever confirm.
const ben = getBenedictusAntiphon(syntheticSanctoralDay('ss-euphemia-lucia-and-geminianus'));
expect(ben.status).toEqual({ la: 'draft', en: 'draft' });
expect(ben.text.la).toContain('Gaudent');
});
});
describe('minorHoursCommon/benedictusCommon fallback (2026-08, the 94 propers:null saints)', () => {
it('a below-duplex-majus saint still gets a real Benedictus antiphon and P/T/S/N content via its Common category', () => {
// St. Anacletus, Semiduplex -- below the duplex-majus+ threshold that
// gates a *proper* psalmody override (hours/lauds.ts's
// getPsalmodyOverrideFor), but getMinorHourOverrideId no longer
// requires that threshold for the shared-Common fallback (see its own
// doc comment for why: live-querying confirmed even a winning Simplex
// saint keeps proper/Common minor-hour content, not the ferial
// default).
const day = resolveDay('2028-07-13');
expect(day.winner).toMatchObject({ kind: 'sanctoral', id: 'st-anacletus' });
expect(getBenedictusAntiphon(day).status).toEqual({ la: 'verified', en: 'verified' });
expect(resolveMinorHourAntiphon('prime', day, {}).status.la).toBe('verified');
expect(resolveMinorHourChapter('terce', day, 'terce-capitulum-ferial').status.la).toBe('verified');
});
it('a Duplex-II-classis saint with no proper minor-hour content of its own still resolves via the Common, not just below-majus saints', () => {
// Ss. Placid and Companions -- confirms the fallback isn't only
// reached by low ranks; a real major feast with no *proper*
// Prime/Terce/Sext/None file authored falls through to its
// minorHoursCommon (common-of-several-martyrs) exactly the same way.
const day = resolveDay('2025-10-05');
expect(day.winner).toMatchObject({ kind: 'sanctoral', id: 'ss-placid-and-companions' });
expect(resolveMinorHourAntiphon('none', day, {}).status.la).toBe('verified');
});
});
describe('the 26 propers-set saints from the 94-saint batch (2026-08)', () => {
it('a saint with a genuine proper collect can still share its Benedictus antiphon with a Common category (benedictusCommon)', () => {
// Ss. Dionysius and Companions -- own proper collect (propers is
// set), but live-verified byte-identical to common-of-several-
// martyrs's Benedictus antiphon, so getBenedictusAntiphon falls
// through to benedictusCommon rather than a nonexistent
// ss-dionysius-and-companions-antiphon.yml.
const day = resolveDay('2025-10-09');
expect(day.winner).toMatchObject({ kind: 'sanctoral', id: 'ss-dionysius-and-companions' });
expect(getBenedictusAntiphon(day).status).toEqual({ la: 'verified', en: 'verified' });
});
it("a saint with genuinely unique Benedictus antiphon text resolves its own proper file, not a Common", () => {
// St. Benedict -- own proper text ("Sanctíssime Conféssor Dómini...",
// st-benedict-antiphon.yml), sourced from the source's Latin-only
// page (no English rendering, same gap as St. Scholastica's), so
// English is a translation and stays `draft`.
const day = resolveDay('2025-03-21');
expect(day.winner).toMatchObject({ kind: 'sanctoral', id: 'st-benedict' });
const ben = getBenedictusAntiphon(day);
expect(ben.status).toEqual({ la: 'verified', en: 'draft' });
expect(ben.text.la).toContain('Benedícte');
});
});
describe('splitNamedAntiphon', () => {
it('carries a verified status through to both the incipit and full forms', () => {
const { incipit, full } = splitNamedAntiphon({
text: { la: 'Foo * bar baz.', en: 'Foo * bar baz.' },
status: { la: 'verified', en: 'verified' },
});
expect(incipit.status).toEqual({ la: 'verified', en: 'verified' });
expect(full.status).toEqual({ la: 'verified', en: 'verified' });
});
it('carries a draft status through to both forms, per language -- the bug this fixes: it used to hardcode verified on everything regardless of the source status, silently dropping the "unverified draft text" UI marker (src/ui/styles.css .text-draft) from every draft antiphon', () => {
const { incipit, full } = splitNamedAntiphon({
text: { la: 'Foo * bar baz.', en: 'Foo * bar baz.' },
status: { la: 'verified', en: 'draft' },
});
expect(incipit.status).toEqual({ la: 'verified', en: 'draft' });
expect(full.status).toEqual({ la: 'verified', en: 'draft' });
});
it("reflects St. Scholastica's own real draft-English antiphon end to end, through Lauds", async () => {
const { resolveOrdo } = await import('../../src/hours/lauds');
const ordo = resolveOrdo('2029-02-10'); // St. Scholastica, a clean year
const idx = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'benedictus');
const canticle = ordo.parts[idx];
const repeat = ordo.parts[idx + 1];
expect(canticle?.kind === 'canticle' ? canticle.antiphon?.status.en : undefined).toBe('draft');
expect(repeat?.kind === 'antiphon' ? repeat.text.status.en : undefined).toBe('draft');
});
});