[verified] feat: support monthly completion recurrence
ci / gitleaks (push) Successful in 23s
ci / docker (push) Successful in 7m16s

This commit is contained in:
2026-09-20 21:04:31 +08:00
parent e6d720b85c
commit 3155155f53
13 changed files with 193 additions and 39 deletions
+29 -2
View File
@@ -48,7 +48,7 @@ def test_dst_gap_rolls_forward_and_ambiguous_time_uses_first_fold():
due_has_time=True,
)
gap_due = recurrence_service._after_completion_due(
gap_task, datetime(2026, 3, 7, 15, tzinfo=UTC), 1, user
gap_task, datetime(2026, 3, 7, 15, tzinfo=UTC), 1, "days", user
)
assert gap_due == datetime(2026, 3, 8, 7, 30, tzinfo=UTC) # local 03:30 after gap
@@ -60,7 +60,7 @@ def test_dst_gap_rolls_forward_and_ambiguous_time_uses_first_fold():
due_has_time=True,
)
fold_due = recurrence_service._after_completion_due(
fold_task, datetime(2026, 10, 31, 15, tzinfo=UTC), 1, user
fold_task, datetime(2026, 10, 31, 15, tzinfo=UTC), 1, "days", user
)
assert fold_due == datetime(2026, 11, 1, 5, 30, tzinfo=UTC) # first 01:30, fold=0
@@ -89,6 +89,30 @@ def test_user_timezone_is_the_calendar_contract_for_after_completion(client, mon
assert datetime.fromisoformat(completed.json()["due_at"]) == datetime(2026, 3, 9, 9, 30, tzinfo=UTC)
def test_month_interval_uses_calendar_month_clamping(client, monkeypatch):
inbox = boot(client)
task = create_after_completion_task(
client,
inbox,
due_at="2026-01-31T01:30:00Z",
after_completion_days=1,
after_completion_unit="months",
).json()
monkeypatch.setattr(
"backend.recurrence_service.utcnow",
lambda: datetime(2026, 1, 30, 16, 30, tzinfo=UTC), # 2026-01-31 00:30 Asia/Shanghai
)
completed = client.patch(
f"/api/v1/tasks/{task['id']}", json={"completed": True, "version": task["version"]}
)
assert completed.status_code == 200
assert datetime.fromisoformat(completed.json()["due_at"]) == datetime(2026, 2, 28, 1, 30, tzinfo=UTC)
recurrence = client.get(f"/api/v1/tasks/{task['id']}/recurrence").json()
assert recurrence["after_completion_unit"] == "months"
def test_atomic_task_create_and_read_after_completion_recurrence(client):
inbox = boot(client)
@@ -105,6 +129,7 @@ def test_atomic_task_create_and_read_after_completion_recurrence(client):
"ends_at": None,
"trigger_mode": "after_completion",
"after_completion_days": 2,
"after_completion_unit": "days",
"last_completed_at": None,
}
@@ -122,6 +147,8 @@ def test_after_completion_configuration_validation(client):
{"title": "零天", "list_id": inbox["id"], "due_at": "2026-03-08T01:30:00Z", "trigger_mode": "after_completion", "after_completion_days": 0},
{"title": "太长", "list_id": inbox["id"], "due_at": "2026-03-08T01:30:00Z", "trigger_mode": "after_completion", "after_completion_days": 3651},
{"title": "子任务", "list_id": inbox["id"], "parent_id": child["id"], "due_at": "2026-03-08T01:30:00Z", "trigger_mode": "after_completion", "after_completion_days": 1},
{"title": "只有单位", "list_id": inbox["id"], "due_at": "2026-03-08T01:30:00Z", "after_completion_unit": "months"},
{"title": "定期带月份", "list_id": inbox["id"], "due_at": "2026-03-08T01:30:00Z", "trigger_mode": "scheduled", "rrule": "FREQ=DAILY", "after_completion_unit": "months"},
]
for payload in invalid_payloads:
assert client.post("/api/v1/tasks", json=payload).status_code in {400, 422}