fix(domain): review round 1 — name the cost, cover the anchored throw, pin two reachable-but-untested paths

C1 (Critical): a month-only startDate on an anchored everyNMonths cost with
no charges yet reached addMonthsToDate directly and threw a bare parsing
error naming no cost, several frames below foldBalances. anchoredOccurrences
now checks its starting cursor's granularity before the walk begins and
throws a cost-named, actionable error instead. No new validation at either
write boundary — that stays out of scope here. stepFrom's docstring no longer
asserts a guarantee ("required to carry a day-granular startDate") that
nothing in the code enforces; it now says what actually protects it.

I1 (Important): the anchored walk's own termination throw had zero test
coverage — both surviving "termination" tests use the cost() fixture, whose
default anchoring is calendar, so they only ever exercised gridOccurrences.
Proven by mutation (replacing the anchored throw with break left every test
passing). Added a test that actually walks anchoredOccurrences, and corrected
the inline comment that falsely claimed coverage existed.

I2 (Important): confirmationsFor's throw is reachable from data the JSON
importer currently accepts (a calendar-cost purchase missing occurrenceDate),
not just a hypothetical programming error — its docstring said "programming
error upstream," which isn't true yet. Docstring corrected, and the throw is
now pinned by a test instead of only asserted in prose.

I3 (Important): nothing asserted that editing a charge's date backwards
re-anchors the series instead of throwing — exactly the behaviour deleting
requireConfirmationAdvances made structurally safe, and exactly the gap left
by deleting round-1's "a date more than one cycle before the slot it claims
is refused" test. Added the inverse: same fixture, but asserting the edit now
succeeds, the fold still folds cleanly, and the series re-anchors to the
edited date.

Minor: round-1 deleted ExpectedBand.test.ts's "stays suppressed once X3 no
longer applies" (X1-only), leaving its underlying scenario (day-granular
anchored cost, slot beyond the displayed month, X3 already satisfied)
completely untested even though still reachable. Added a test pinning what
expectedGroups actually does with it now that X1 is gone: the row is offered,
not suppressed — so a future reintroduction of suppression here is a visible,
deliberate change instead of a silent one.

Left untouched, as instructed: ExpectedBand.tsx's stale X1 docstring and the
Settings anchoring dropdown (both belong to later tasks).
This commit is contained in:
Lucas Winther committed 2026-09-05 01:38:50 +02:00
1 parent 3649a37cfa
commit c49224d4f7
4 files changed
+174 -18

No files matched your search

+27
View File
@@ -298,6 +298,33 @@ describe("expectedGroups — X3 (a cost with an outstanding slot never also show
// throws — is still true, and now trivially so: nothing in the anchored walk
// can throw from a confirmation's date any more.
describe("expectedGroups — coming-up confirmations never break the fold (formerly X1)", () => {
test("REVIEW ROUND 2 (Minor): a day-granular anchored cost's future slot IS now offered once this month is settled — X1's suppression is gone, not replaced", () => {
// This is deleted test #8's scenario ("stays suppressed even once X3 no
// longer applies") — still perfectly reachable, and now completely
// untested. Pinning what actually happens today rather than leaving the
// gap silent: September's slot is settled on time, so X3 has nothing left
// to suppress, and there is no anchored-model X1 replacement, so the
// October row (39 days from `today`, more than one 30-day cycle) IS
// offered. If a future task reintroduces some suppression here, this test
// will fail loudly and force that change to be deliberate.
const data = baseDataset();
data.recurring = [
cost({
id: "car",
name: "Car insurance",
amount: { amount: 349, currency: "DKK" },
startDate: "2026-08-14",
recurrence: { kind: "everyNDays", n: 30 },
anchoring: "lastCharge",
}),
];
confirmOccurrence(data, "car", "2026-09-13", { date: "2026-09-13" });
const groups = expectedGroups(data, "2026-09", "2026-09-04");
expect(groups.pending).toEqual([]);
expect(groups.comingUp.map((o) => o.date)).toEqual(["2026-10-13"]);
});
test("regression: whatever expectedGroups offers as 'coming up', confirming it all today and folding never throws (demo Car insurance shape)", () => {
const data = baseDataset();
data.recurring = [