fix(recurring): reject calendar-impossible startDate, not just wrongly-shaped (C1 residual)
requireStartDateGranularity (store/actions.ts) and its import-path twin (export/json.ts) checked date SHAPE with a regex, not date VALIDITY, so "2026-09-31" (September has 30 days) passed both and threw later out of foldBalances instead of at the write boundary. isValidStartDate (RecurringSection.tsx) had the same hole for a day-shaped value under everyNMonths. Separately, monthOf's regex (domain/months.ts) was unanchored at the end, so "2026-091" was silently read as September 2026. Route day-shaped values through toDayOrdinal and month-shaped values through monthOf (now anchored to accept only "YYYY-MM" or "YYYY-MM-DD" in full, never a prefix) at all three write boundaries, converting whatever they throw into each file's own error style. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
405cf0bf2d
commit
042bc8597d
@@ -348,6 +348,43 @@ describe("recurring cost validation", () => {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
// Residual C1: PURCHASE_DATE/DAY_GRANULAR_DATE check shape and range
|
||||
// ("31" is a valid day-of-month digit pair), not calendar validity.
|
||||
// September has 30 days, so "2026-09-31" passed both regexes and the
|
||||
// fold later threw "Invalid day in IsoDate" out of addDays/toDayOrdinal.
|
||||
test("an impossible day on a day-granular kind is refused, not just shape-checked (residual C1)", () => {
|
||||
expect(() =>
|
||||
parseDatasetJson(
|
||||
withRecurring([
|
||||
{ ...valid, startDate: "2026-02-30", recurrence: { kind: "everyNDays", n: 1 } },
|
||||
]),
|
||||
),
|
||||
).toThrow(/calendar-impossible/);
|
||||
});
|
||||
|
||||
test("an impossible day is refused even under everyNMonths (residual C1)", () => {
|
||||
expect(() =>
|
||||
parseDatasetJson(withRecurring([{ ...valid, startDate: "2026-09-31" }])),
|
||||
).toThrow(/calendar-impossible/);
|
||||
});
|
||||
|
||||
test("leap-year and non-leap-year Februaries are accepted (residual C1)", () => {
|
||||
expect(
|
||||
parseDatasetJson(
|
||||
withRecurring([
|
||||
{ ...valid, startDate: "2024-02-29", recurrence: { kind: "everyNDays", n: 1 } }, // 2024 is a leap year
|
||||
]),
|
||||
).recurring.length,
|
||||
).toBe(1);
|
||||
expect(
|
||||
parseDatasetJson(
|
||||
withRecurring([
|
||||
{ ...valid, startDate: "2026-02-28", recurrence: { kind: "everyNDays", n: 1 } }, // 2026 is not
|
||||
]),
|
||||
).recurring.length,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test("an unknown recurrence kind is refused", () => {
|
||||
expect(() => parseDatasetJson(withRecurring([{ ...valid, recurrence: { kind: "everyFullMoon", n: 1 } }])))
|
||||
.toThrow(/unknown kind/);
|
||||
|
||||
Reference in New Issue
Block a user