fix: recurrence ghost exactness and calendar dedup
ci / docker (push) Successful in 3m38s

This commit is contained in:
2026-09-05 21:04:56 +08:00
parent 32d6fe9424
commit 7ab5896dc0
2 changed files with 168 additions and 30 deletions
+114
View File
@@ -154,6 +154,120 @@ def test_recurrence_mutations_reject_ghost_occurrences(client):
assert len([row for row in calendar if row.get("recurrence_id") == recurrence["id"]]) == 3
def test_calendar_does_not_duplicate_moved_exceptions_when_both_in_range(client):
inbox = boot(client)
task = client.post(
"/api/v1/tasks",
json={"title": "每周一", "list_id": inbox["id"], "due_at": "2026-09-14T09:00:00Z"},
).json()
recurrence = client.post(
"/api/v1/recurrences", json={"task_id": task["id"], "rrule": "FREQ=WEEKLY;BYDAY=MO;COUNT=4"}
).json()
# Series: 9/14, 9/21, 9/28, 10/5. Move the 9/21 occurrence to 9/23.
client.patch(
f"/api/v1/recurrences/{recurrence['id']}",
params={"scope": "this", "occurrence_at": "2026-09-21T09:00:00Z"},
json={"due_at": "2026-09-23T09:00:00Z"},
)
calendar = client.get(
"/api/v1/calendar",
params={"start": "2026-09-01", "end": "2026-09-30", "timezone": "UTC"},
).json()
matches = [row for row in calendar if row.get("recurrence_id") == recurrence["id"]]
keys = [(row["occurrence_at"], row["due_at"]) for row in matches]
assert len(keys) == len(set(keys))
assert len(matches) == 3
due_days = [row["due_at"][:10] for row in matches]
assert due_days == ["2026-09-14", "2026-09-23", "2026-09-28"]
assert not any(row["due_at"].startswith("2026-09-21") for row in matches)
def test_recurrence_rejects_wrong_time_of_day_and_alternate_offset(client):
inbox = boot(client)
task = client.post(
"/api/v1/tasks",
json={"title": "九点站会", "list_id": inbox["id"], "due_at": "2026-09-07T09:00:00Z"},
).json()
recurrence = client.post(
"/api/v1/recurrences", json={"task_id": task["id"], "rrule": "FREQ=WEEKLY;BYDAY=MO;COUNT=3"}
).json()
wrong_hour = client.patch(
f"/api/v1/recurrences/{recurrence['id']}",
params={"scope": "this", "occurrence_at": "2026-09-07T10:00:00Z"},
json={"title": "幽灵"},
)
assert wrong_hour.status_code == 422
same_instant = client.patch(
f"/api/v1/recurrences/{recurrence['id']}",
params={"scope": "this", "occurrence_at": "2026-09-07T17:00:00+08:00"},
json={"title": "等价时刻"},
)
assert same_instant.status_code == 200
def test_recurrence_rejects_occurrence_after_cutoff(client):
inbox = boot(client)
task = client.post(
"/api/v1/tasks",
json={"title": "每天重复", "list_id": inbox["id"], "due_at": "2026-09-01T09:00:00Z"},
).json()
recurrence = client.post(
"/api/v1/recurrences", json={"task_id": task["id"], "rrule": "FREQ=DAILY;COUNT=10"}
).json()
# Cut the series off at 9/3 (ends_at = 9/3T08:59:59.999999Z)
client.patch(
f"/api/v1/recurrences/{recurrence['id']}",
params={"scope": "this-and-future", "occurrence_at": "2026-09-03T09:00:00Z"},
json={},
)
after_cutoff = client.patch(
f"/api/v1/recurrences/{recurrence['id']}",
params={"scope": "this", "occurrence_at": "2026-09-05T09:00:00Z"},
json={"title": "晚于截止"},
)
assert after_cutoff.status_code == 422
def test_calendar_accepts_legacy_timezone_offset(client):
inbox = boot(client)
client.post(
"/api/v1/tasks",
json={"title": "上海边界任务", "list_id": inbox["id"], "due_at": "2026-09-01T16:00:00Z"},
)
calendar = client.get(
"/api/v1/calendar",
params={"start": "2026-09-01", "end": "2026-09-30", "timezone_offset": 480},
).json()
assert any(row["title"] == "上海边界任务" for row in calendar)
def test_calendar_does_not_duplicate_moved_exception_when_original_and_moved_in_range(client):
inbox = boot(client)
task = client.post(
"/api/v1/tasks",
json={"title": "每周一", "list_id": inbox["id"], "due_at": "2026-09-14T09:00:00Z"},
).json()
recurrence = client.post(
"/api/v1/recurrences", json={"task_id": task["id"], "rrule": "FREQ=WEEKLY;BYDAY=MO;COUNT=3"}
).json()
# Series: 9/14, 9/21, 9/28 — all inside the requested month.
# Move the 9/21 occurrence to 9/23 (also inside). Regression: this used to render twice.
client.patch(
f"/api/v1/recurrences/{recurrence['id']}",
params={"scope": "this", "occurrence_at": "2026-09-21T09:00:00Z"},
json={"due_at": "2026-09-23T09:00:00Z"},
)
calendar = client.get(
"/api/v1/calendar",
params={"start": "2026-09-01", "end": "2026-09-30", "timezone": "UTC"},
).json()
matches = [row for row in calendar if row.get("recurrence_id") == recurrence["id"]]
due_days = sorted(row["due_at"][:10] for row in matches)
assert due_days == ["2026-09-14", "2026-09-23", "2026-09-28"]
def test_habit_logs_support_date_range_filter(client):
boot(client)
habit = client.post("/api/v1/habits", json={"name": "跑步", "kind": "boolean", "schedule_type": "daily"}).json()