From ee43270b2bc039b49bb7cd1e755c8ab83bcfde51 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 27 Aug 2026 02:28:27 +0200 Subject: [PATCH] Move the rename-stability test to where it can fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 2's version called occurrenceId twice with identical arguments, which any pure function satisfies — occurrenceId never takes a title, so it could not fail the thing it claimed to pin. Task 5 passes the whole rule into asOccurrenceEvent, so a rename is a real input there and the assertion has something to bite on. Found by the Task 2 review. Co-Authored-By: Claude Opus 5 (1M context) --- .../plans/2026-08-27-recurring-custom-events.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/superpowers/plans/2026-08-27-recurring-custom-events.md b/docs/superpowers/plans/2026-08-27-recurring-custom-events.md index 6250fcb..eaa7037 100644 --- a/docs/superpowers/plans/2026-08-27-recurring-custom-events.md +++ b/docs/superpowers/plans/2026-08-27-recurring-custom-events.md @@ -1116,6 +1116,21 @@ describe("asOccurrenceEvent", () => { expect(isCustomEventId(row.id)).toBe(true); }); + test("renaming a rule does not move its occurrence ids", () => { + // The token is random precisely so fixing a typo never costs the marks + // attached to an occurrence. Exercised here rather than against + // occurrenceId, which never takes a title and so could not fail it: this + // path passes the whole rule, so a rename is a real input to the result. + const rule = repeating(); + const renamed = { ...rule, title: "Abyss, actually" }; + const now = new Date("2026-09-15T12:00:00").getTime(); + const before = asOccurrenceEvent(rule, nextOccurrences(rule, now, 1)[0]!); + const after = asOccurrenceEvent(renamed, nextOccurrences(renamed, now, 1)[0]!); + + expect(after.id).toBe(before.id); + expect(after.title).toBe("Abyss, actually"); + }); + test("a derived end is a real end, so the clock counts down to it", () => { // The rule stores endsAt: null; the occurrence resolves it. A row reaching // a view must never carry the unresolved form, or it renders as