Fix flexible-wish double-booking, extend calendar cache to 2055, add journey tests
Second real bug found by driving the running app while the suites were green. Flexible wishes were funded twice across propose -> apply -> re-propose. Because planned_leave_day carries no wish_id, the engine could not tell that applied days were that wish's own prior occurrence; it treated them as occupied space, routed the candidate search around them, and funded the year again on shifted dates while reporting weeks_funded 1.0. Applying twice turned "one week each spring" into 18 paid days instead of 9. Fixed wishes were unaffected and stay unaffected. Fix (allocation_service.py), per the approved approach of counting already-applied days inside the bounding range rather than adding a wish_id column: - _Ledger._persisted_lookup snapshots existing days BEFORE this run folds in its own freshly-computed windows, so a fixed wish's new placement is never mistaken for a flexible wish's history - _reuse_already_funded_window finds this wish's persisted dates within its bound and reuses the window containing them, via the ordinary fund_window path - full funding therefore yields zero new days and partial funding tops up exactly the remainder, with no duplication and no false shortfall - with no persisted dates, or if the reused window is no longer collision-free, it falls through to the original search, so an unsatisfied year is never frozen Calendar cache extended 2026-2035 -> 2026-2055 (39KB -> 116KB) and the allocator switched to strict, so an uncached year raises UncachedYearError -> ValidationError -> HTTP 422 instead of silently treating Julafton, Nyarsafton and Midsommarafton as working days. Also fixed normalize_day dropping the second name when two holidays share a date (Nationaldagen was being lost). 2049 and 2055 legitimately have 17 red days, not 18: Pingstdagen coincides with Nationaldagen. New backend/tests/integration/test_user_journey.py drives the real app over HTTP across seven journeys: the double-booking guard, the fixed-wish control, apply idempotency, fixed-never-displaced-by-flexible, flexible wishes tracking Easter per year, age-4 arithmetic self-consistency, and a recursive check that no Swedish text reaches the wire now that everything is message_key + data. New backend/scripts/seed_demo.py seeds a realistic workspace through the real service layer for manual testing: demo@example.com / DemoPass123! Backend 414 -> 435 tests. Frontend unchanged at 705. Still not verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -102,9 +102,9 @@ class TestYearWindowPagination:
|
||||
async def test_a_middle_page_that_would_overshoot_is_also_clamped(
|
||||
self, calendar_service: CalendarService
|
||||
) -> None:
|
||||
"""Same clamp, exercised one page before the very last year (the
|
||||
`start_year=2034, years=4` case from real paging: 2034+3=2037 would
|
||||
overshoot the 2026-2035 cache by two years)."""
|
||||
"""Same clamp, exercised one page before the very last year (e.g. the
|
||||
`start_year=max_year-1, years=4` case from real paging, which would
|
||||
overshoot the cache's end by two years without the clamp)."""
|
||||
bounds = cached_year_bounds()
|
||||
assert bounds is not None
|
||||
_, max_year = bounds
|
||||
@@ -178,13 +178,30 @@ class TestNoDegradedDataAcrossTheServedRange:
|
||||
async def test_red_day_count_is_consistent_across_every_served_year(
|
||||
self, calendar_service: CalendarService
|
||||
) -> None:
|
||||
"""No year may be silently poorer than its neighbours (the fallback's
|
||||
signature: 18 real red days in-cache vs. 13 once it kicks in).
|
||||
|
||||
The one legitimate exception in a ~30-year range: Pingstdagen
|
||||
(Easter + 49 days) occasionally lands on Sveriges nationaldag (fixed
|
||||
June 6), e.g. 2049 and 2055. That's 17 *distinct* red dates, not a
|
||||
degradation — both holidays genuinely share one calendar date, and
|
||||
the API itself reports 17 for that year. Assert the exception is
|
||||
exactly this coincidence, by name, rather than loosely allowing any
|
||||
two counts.
|
||||
"""
|
||||
by_year = await self._all_cached_years_red_days(calendar_service)
|
||||
counts = {year: len(red_days) for year, red_days in by_year.items()}
|
||||
# No year may be silently poorer than its neighbours (the fallback's
|
||||
# signature: 18 real red days in-cache vs. 13 once it kicks in).
|
||||
assert len(set(counts.values())) == 1, (
|
||||
f"red-day counts differ across years, one may be degraded: {counts}"
|
||||
)
|
||||
for year, red_days in by_year.items():
|
||||
names_by_date = {d: name.lower() for d, name in red_days.items()}
|
||||
june_6_name = next(
|
||||
(n for d, n in names_by_date.items() if d.month == 6 and d.day == 6), ""
|
||||
)
|
||||
pingst_meets_nationaldag = "pingstdagen" in june_6_name and "nationaldag" in june_6_name
|
||||
expected = 17 if pingst_meets_nationaldag else 18
|
||||
assert len(red_days) == expected, (
|
||||
f"{year}: expected {expected} red dates "
|
||||
f"({'Pingstdagen/Nationaldagen coincide' if pingst_meets_nationaldag else 'normal year'}), "
|
||||
f"got {len(red_days)} — a year may be degraded: {sorted(red_days)}"
|
||||
)
|
||||
|
||||
|
||||
class TestOverrideMerging:
|
||||
|
||||
Reference in New Issue
Block a user