397f31f4fc
Deploy / deploy (push) Successful in 52s
When more than one octave is active on a day with no temporal standing of
its own (St. Lawrence's and the Assumption's genuinely overlap every Aug
16-17), which one governs the day's content/label is now decided by rank
comparison instead of "whichever started first" (an accident of
insertion order). Per direct instruction:
- Highest effective rank wins outright; every other active octave still
gets commemorated, same as a single octave already outranking a weak
rival saint.
- A rank tie goes to whichever octave started more recently -- day 1 of
a new octave needs to be fully present, the whole point of it
starting. No real tied-rank case exists yet to verify this
empirically, unlike everything else here -- documented in TODO.md as
a stated decision, not a live finding.
New OctaveConfig.closingDayRank (default duplex): an octave's own final
day ("in Octava") is elevated above its ordinary in-between rank --
live-verified as a real, general pattern (both St. Lawrence's Aug 17 and
the Assumption's Aug 22 show as Duplex, above their otherwise-Semiduplex
ordinary days), not a one-off. This elevation is *why* Lawrence's octave
beats the Assumption's on their one real overlap day despite the
Assumption being the far higher-ranked feast overall. Feeds both the
existing rival-saint threshold and the new octave-vs-octave comparison.
calendar/octaves.ts gains resolveActiveOctave (+ pickWinningOctave, the
comparison itself factored out for direct unit testing against synthetic
data, since no real tied-rank overlap exists to test against yet).
hours/resolve-common.ts's resolveOfficeWinner and calendar/day-label.ts
both now call it instead of each keeping their own "activeOctavesFor(...)
[0]" logic.
That consolidation surfaced a real, independent bug: getDayLabel never
checked temporalCategory at all before choosing an octave name, unlike
resolveOfficeWinner -- found while testing the real Aug 16 overlap (a
Sunday that year, where the temporal Sunday has standing and should win
outright). Live-verified counterexample: the Christmas Octave's own
stack (Dec 30) was wrongly labeled "3rd Day within the Octave of The Holy
Innocents" instead of the correct plain temporal label -- the real title
never names any of the four stacked octaves there. Fixed by sharing the
exact same ordinary-feria gate resolveOfficeWinner already had, so the
two can no longer disagree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
117 lines
6.1 KiB
TypeScript
117 lines
6.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveDay } from '../../src/calendar';
|
|
import { getDayLabel } from '../../src/calendar/day-label';
|
|
import type { LiturgicalDay } from '../../src/calendar/types';
|
|
|
|
describe('getDayLabel — ordinal temporal label', () => {
|
|
it("labels an anchor's own partial week as \"Weekday after {Anchor}\"", () => {
|
|
// Trinity Sunday 2026 is May 31.
|
|
expect(getDayLabel(resolveDay('2026-06-01'))).toBe('Monday after Trinity Sunday');
|
|
expect(getDayLabel(resolveDay('2026-01-07'))).toBe('Wednesday after Epiphany'); // Epiphany 2026 is a Tuesday
|
|
});
|
|
|
|
it('names the anchor day itself, not "day after itself"', () => {
|
|
// Trinity Sunday no longer carries a sanctoral commemoration of its
|
|
// own (St. Felix I, May 30, is now commemorated in place on his own
|
|
// day within Pentecost's octave instead of transferring here — see
|
|
// calendar/octaves.ts and tests/calendar/transfer.test.ts), so this
|
|
// is back to a plain temporal label; the "feast — temporal" combined
|
|
// form is covered separately below with synthetic data.
|
|
expect(getDayLabel(resolveDay('2026-05-31'))).toBe('Trinity Sunday');
|
|
expect(getDayLabel(resolveDay('2026-04-05'))).toBe('Easter');
|
|
expect(getDayLabel(resolveDay('2026-02-18'))).toBe('Ash Wednesday');
|
|
});
|
|
|
|
it('gives the correct week-after-Trinity ordinal', () => {
|
|
// Trinity Sunday 2026 is May 31; Jul 6 falls 5 full weeks after the
|
|
// first Sunday-after-Trinity (Jun 7).
|
|
expect(getDayLabel(resolveDay('2026-07-06'))).toBe('Monday in the 5th week after Trinity');
|
|
});
|
|
|
|
it("Advent's own anchor Sunday is already week 1, not a separate anchor-week case", () => {
|
|
// 2025-11-30 is also St. Andrew's own day (Duplex II. classis) — since
|
|
// the November sanctoral pull, he's correctly commemorated alongside
|
|
// Advent I rather than simply absent (confirmed against the live
|
|
// reference engine, which shows the same pairing), so the label
|
|
// reflects both, feast name first, same pattern as Trinity Sunday's
|
|
// own St. Felix I case above.
|
|
expect(getDayLabel(resolveDay('2025-11-30'))).toBe('St. Andrew, Apostle — The 1st Sunday of Advent');
|
|
expect(getDayLabel(resolveDay('2025-12-01'))).toBe('Monday in the 1st week of Advent');
|
|
});
|
|
|
|
it('falls back to weekday + season name for seasons with no ordinal convention modeled', () => {
|
|
expect(getDayLabel(resolveDay('2026-05-15'))).toContain('in Ascensiontide');
|
|
});
|
|
});
|
|
|
|
describe('getDayLabel — active octave', () => {
|
|
it('shows the octave day itself as the primary label, plus a lesser saint commemorated alongside it', () => {
|
|
// St. Lawrence's own octave (Aug 10-17); Aug 12 is day 3, and also St.
|
|
// Clare's own day -- Simplex, below the octave's semiduplex threshold,
|
|
// so she's commemorated rather than winning. Matches the live engine's
|
|
// own title for this date directly (see saints/st-clare.yml).
|
|
expect(getDayLabel(resolveDay('2026-08-12'))).toBe('3rd Day within the Octave of St. Lawrence, Martyr — St. Clare, Virgin');
|
|
});
|
|
|
|
it('shows the octave day alone when nothing is separately commemorated that day', () => {
|
|
// Aug 17 is day 8 (the last day) of St. Lawrence's octave, with no
|
|
// sanctoral candidate of its own in this app's calendar.
|
|
expect(getDayLabel(resolveDay('2026-08-17'))).toBe('8th Day within the Octave of St. Lawrence, Martyr');
|
|
});
|
|
|
|
it("a season's own named anchor day outranks an active octave, even though Trinity Sunday is technically also day 8 of Pentecost's own octave", () => {
|
|
expect(getDayLabel(resolveDay('2026-05-31'))).toBe('Trinity Sunday');
|
|
});
|
|
|
|
it("a temporal day with real standing of its own (not ordinary-feria) never shows an octave name at all, even with one active -- live-verified counterexample: the Christmas Octave's own stack (Dec 30) titles itself off the Sunday, never any of the four octaves layered on top of it", () => {
|
|
const day = resolveDay('2033-12-30');
|
|
expect(day.temporalCategory).toBe('privileged-feria-minor');
|
|
expect(getDayLabel(day)).not.toContain('Octave');
|
|
expect(getDayLabel(day)).toBe('Friday in Christmastide');
|
|
});
|
|
|
|
it("picks the higher-ranked of two genuinely overlapping octaves for the label -- St. Lawrence's own elevated closing day (Aug 17) over the Assumption's ordinary day 3, even though the Assumption's octave started later and the Assumption is the higher-ranked feast overall", () => {
|
|
expect(getDayLabel(resolveDay('2026-08-17'))).toBe('8th Day within the Octave of St. Lawrence, Martyr');
|
|
expect(getDayLabel(resolveDay('2026-08-18'))).toBe('4th Day within the Octave of The Assumption of the Blessed Virgin Mary');
|
|
});
|
|
|
|
it('a real Sunday with standing of its own wins over both octaves entirely, showing the plain ordinal Sunday label -- not an octave name at all', () => {
|
|
// 2026-08-16 is a Sunday genuinely within both St. Lawrence's and the
|
|
// Assumption's overlapping octave windows.
|
|
const day = resolveDay('2026-08-16');
|
|
expect(day.weekday).toBe('sunday');
|
|
expect(getDayLabel(day)).not.toContain('Octave');
|
|
});
|
|
});
|
|
|
|
describe('getDayLabel — feast name combination', () => {
|
|
const base: LiturgicalDay = {
|
|
date: '2026-06-15',
|
|
weekday: 'monday',
|
|
season: 'trinitytide',
|
|
temporalCategory: 'ordinary-feria',
|
|
winner: { kind: 'temporal', id: 'post-pentecost-02' },
|
|
commemorations: [],
|
|
};
|
|
|
|
it('shows just the feast name when it wins outright', () => {
|
|
const day: LiturgicalDay = {
|
|
...base,
|
|
winner: { kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex' },
|
|
};
|
|
expect(getDayLabel(day)).toBe('St. Ereden');
|
|
});
|
|
|
|
it('shows both, feast first, when the feast is merely commemorated', () => {
|
|
const day: LiturgicalDay = {
|
|
...base,
|
|
commemorations: [{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex-2-classis' }],
|
|
};
|
|
expect(getDayLabel(day)).toBe('St. Ereden — Monday in the 2nd week after Trinity');
|
|
});
|
|
|
|
it('shows just the temporal label when nothing is commemorated at all', () => {
|
|
expect(getDayLabel(base)).toBe('Monday in the 2nd week after Trinity');
|
|
});
|
|
});
|