Mock: let an override move an occurrence, as the server does

Confirmed live on 0.16.20 (2026-08-31): one occurrence of a weekly 09:00
series moved to 14:00 comes back with `start` at 14:00 and
`recurrenceId` still at 09:00. The slot the rule made stays put; only the
clock time moves.

The mock set `start` from the slot after merging the override, so it
clobbered any `start` the override carried and a moved occurrence did not
move. Per-occurrence *time* editing - one of the main things the feature
is for - therefore looked broken against the mock and correct against the
server, which is the wrong way round for a mock to be wrong.

It also confirms the choice of handle: `recurrenceId` is the one name for
an instance that survives both a renumbering and a move, which is why the
store re-resolves from it rather than from `start` or a cached id.
This commit is contained in:
2026-08-30 21:49:53 -07:00
parent 362bd8b282
commit 06943fd473
3 changed files with 60 additions and 2 deletions
+42
View File
@@ -166,3 +166,45 @@ describe("synthetic ids are only true until the next write", () => {
assert.equal(same!.start, occ.start);
});
});
describe("an override that moves an occurrence", () => {
/*
* Confirmed live on 0.16.20 (2026-08-31): one occurrence of a weekly 09:00
* series moved to 14:00 comes back with `start` at 14:00 and `recurrenceId`
* still at 09:00 — the slot the rule made, which the move does not touch.
*
* The mock used to clobber the override's `start` with the slot time, so a
* moved occurrence did not move. That made per-occurrence *time* editing —
* one of the main things the feature is for — look broken against the mock
* and fine against the server.
*/
const moved = () => ({
...series(),
recurrenceOverrides: { "2026-09-08T09:00:00": { start: "2026-09-08T14:00:00" } },
});
it("moves the occurrence and leaves its recurrenceId on the original slot", () => {
const [a, b] = week("2026-09-07T00:00:00", "2026-09-14T00:00:00");
const occ = expandOccurrences(moved(), a, b).find((o) => o.recurrenceId === "2026-09-08T09:00:00")!;
assert.equal(occ.start, "2026-09-08T14:00:00");
assert.equal(occ.recurrenceId, "2026-09-08T09:00:00");
});
it("shows the moved time on the occurrence a get returns", () => {
const base = moved();
const occ = expandOccurrences(base, new Date("2026-09-07T00:00:00"), new Date("2026-09-14T00:00:00"))
.find((o) => o.recurrenceId === "2026-09-08T09:00:00")!;
const view = occurrenceView(base, occ);
assert.equal(view.start, "2026-09-08T14:00:00");
assert.equal(view.recurrenceId, "2026-09-08T09:00:00");
});
it("keeps the occurrence findable by recurrenceId after the move", () => {
// This is the property the store depends on: `recurrenceId` survives both
// a renumbering and a move, so it is the handle a mutation resolves from.
const base = moved();
const all = expandOccurrences(base, new Date("2026-09-01T00:00:00"), new Date("2026-10-01T00:00:00"));
assert.equal(all.filter((o) => o.recurrenceId === "2026-09-08T09:00:00").length, 1);
});
});
+16 -2
View File
@@ -107,8 +107,22 @@ export function expandOccurrences(base: Obj, from: Date, to: Date): Occurrence[]
// An excluded date is simply gone from the expansion. Its slot is not
// reserved -- see `syntheticId` for why nothing here pretends otherwise.
if (override?.excluded === true) return true;
if (at >= from && at < to) {
out.push({ index, recurrenceId, start: recurrenceId, ...(override ? { override } : {}) });
/*
* An override may move the occurrence, and then `start` and `recurrenceId`
* are two different times: the slot it fills stays where the rule put it,
* and only the clock time moves. **Confirmed live on 0.16.20
* (2026-08-31)**: one occurrence of a weekly 09:00 series moved to 14:00
* came back `start: 2027-06-14T14:00:00` with `recurrenceId` still
* `2027-06-14T09:00:00`.
*
* Which is exactly why `recurrenceId` is what a client holds on to. It is
* the one name for this instance that neither a renumbering nor a move
* changes.
*/
const start = (typeof override?.start === "string" ? override.start : null) ?? recurrenceId;
const shown = parseLocal(start);
if (shown >= from && shown < to) {
out.push({ index, recurrenceId, start, ...(override ? { override } : {}) });
}
return at < to;
};