from datetime import date def schedule_payload(title="日程", **overrides): payload = {"title": title, "date": "2026-08-01", "time": "22:00", "recurrence": "none", "priority": 3, "state": "active"} payload.update(overrides) return payload def test_schedule_crud_lifecycle(client): created = client.post("/api/schedules", json=schedule_payload("第一个日程")) assert created.status_code == 201, created.text schedule = created.json() schedule_id = schedule["scheduleId"] assert schedule["version"] == 1 and schedule["completions"] == {} listed = client.get("/api/schedules").json() assert len(listed["items"]) == 1 fetched = client.get(f"/api/schedules/{schedule_id}") assert fetched.status_code == 200 and fetched.json()["title"] == "第一个日程" updated = client.put( f"/api/schedules/{schedule_id}", json=schedule_payload("改名日程", expectedVersion=1), ) assert updated.status_code == 200 and updated.json()["version"] == 2 deleted = client.delete(f"/api/schedules/{schedule_id}?expectedVersion=2") assert deleted.status_code == 200 assert client.get("/api/schedules").json()["items"] == [] restored = client.post(f"/api/schedules/{schedule_id}/restore?expectedVersion=3") assert restored.status_code == 200 assert len(client.get("/api/schedules").json()["items"]) == 1 def test_schedule_update_conflict_and_purge(client): created = client.post("/api/schedules", json=schedule_payload("冲突日程")).json() schedule_id = created["scheduleId"] conflict = client.put(f"/api/schedules/{schedule_id}", json=schedule_payload("冲突", expectedVersion=99)) assert conflict.status_code == 409 client.delete(f"/api/schedules/{schedule_id}?expectedVersion=1") assert client.get(f"/api/schedules/{schedule_id}").status_code == 404 purged = client.post(f"/api/schedules/{schedule_id}/purge", json={"confirmation": schedule_id}) assert purged.status_code == 200 assert client.get(f"/api/schedules/{schedule_id}?includeDeleted=true").status_code == 404 def test_schedule_complete_toggle(client): schedule = client.post("/api/schedules", json=schedule_payload("打卡日程")).json() schedule_id = schedule["scheduleId"] done = client.post( f"/api/schedules/{schedule_id}/complete", json={"date": "2026-08-01", "completed": True, "expectedVersion": 1}, ) assert done.status_code == 200 today = client.get("/api/schedules/today?date=2026-08-01").json() assert today["items"][0]["completed"] is True undone = client.post( f"/api/schedules/{schedule_id}/complete", json={"date": "2026-08-01", "completed": False, "expectedVersion": 2}, ) assert undone.status_code == 200 today = client.get("/api/schedules/today?date=2026-08-01").json() assert today["items"][0]["completed"] is False def test_schedule_recurrence_resolution(client): client.post("/api/schedules", json=schedule_payload("单次", date="2026-08-01")) client.post("/api/schedules", json=schedule_payload("每天", date="2026-08-01", recurrence="daily")) client.post("/api/schedules", json=schedule_payload("每周", date="2026-08-03", recurrence="weekly")) # 2026-08-03 是周一 client.post("/api/schedules", json=schedule_payload("每月", date="2026-08-15", recurrence="monthly")) def titles(day): return {item["title"] for item in client.get(f"/api/schedules/today?date={day}").json()["items"]} # 单次:只有当天 assert titles("2026-08-01") == {"单次", "每天"} assert "单次" not in titles("2026-08-02") # 每天:起始日之后每天都出现 assert "每天" in titles("2026-08-20") assert "每天" not in titles("2026-07-31") # 每周:起始日之后的每个周一出现 assert "每周" in titles("2026-08-10") assert "每周" not in titles("2026-08-04") # 周二 # 每月:起始日之后的每月 15 号出现 assert "每月" in titles("2026-09-15") assert "每月" not in titles("2026-09-16") def test_schedule_archived_not_shown(client): client.post("/api/schedules", json=schedule_payload("归档日程", state="archived")) today = client.get("/api/schedules/today?date=2026-08-01").json() assert today["items"] == [] def test_schedule_calendar_endpoint(client): client.post("/api/schedules", json=schedule_payload("八月日程", date="2026-08-15")) calendar = client.get("/api/schedules/calendar?month=2026-08").json() assert calendar["year"] == 2026 and calendar["month"] == 8 assert len(calendar["days"]) == 31 day_15 = next(item for item in calendar["days"] if item["date"] == "2026-08-15") assert [item["title"] for item in day_15["schedules"]] == ["八月日程"] assert client.get("/api/schedules/calendar?month=bad").status_code == 422 def test_schedule_validation(client): bad_time = client.post("/api/schedules", json=schedule_payload("坏时间", time="25:99")) assert bad_time.status_code == 422 missing_date = client.post("/api/schedules", json={"title": "无日期", "recurrence": "none"}) assert missing_date.status_code == 422 bad_recurrence = client.post("/api/schedules", json=schedule_payload("坏重复", recurrence="yearly")) assert bad_recurrence.status_code == 422 def test_agent_overview_token_auth(client, monkeypatch): client.post("/api/schedules", json=schedule_payload("今日日程", date="2026-08-01")) client.post("/api/tasks", json={"name": "任务", "startDate": "2026-08-01", "metrics": [{"name": "完成", "kind": "checkin", "period": "day"}]}) # 未配置 token → 503 assert client.get("/api/agent/overview").status_code == 503 monkeypatch.setenv("PPM_API_TOKEN", "tok") # 无 / 错误 token → 401 assert client.get("/api/agent/overview").status_code == 401 assert client.get("/api/agent/overview", headers={"Authorization": "Bearer wrong"}).status_code == 401 # 正确 token → 200,含日程与任务进度 response = client.get("/api/agent/overview?date=2026-08-01", headers={"Authorization": "Bearer tok"}) assert response.status_code == 200, response.text body = response.json() assert body["date"] == "2026-08-01" assert [item["title"] for item in body["schedules"]] == ["今日日程"] assert "tasks" in body["taskProgress"] task_info = next(iter(body["taskProgress"]["tasks"].values())) assert task_info["name"] == "任务" def test_agent_create_schedule_write(client, monkeypatch): monkeypatch.setenv("PPM_API_TOKEN", "tok") # 无 token → 401 assert client.post("/api/agent/schedules", json=schedule_payload("通过API添加")).status_code == 401 # 带 token → 201,且出现在列表里 created = client.post( "/api/agent/schedules", json=schedule_payload("通过API添加"), headers={"Authorization": "Bearer tok"}, ) assert created.status_code == 201, created.text schedule_id = created.json()["scheduleId"] assert any(item["scheduleId"] == schedule_id for item in client.get("/api/schedules").json()["items"]) # 非法 payload → 422 assert client.post("/api/agent/schedules", json={"title": "x"}, headers={"Authorization": "Bearer tok"}).status_code == 422 def test_schedule_survives_export_import(client): client.post("/api/schedules", json=schedule_payload("导出日程")) exported = client.get("/api/export").json() assert any(s["title"] == "导出日程" for s in exported.get("schedules", [])) imported = client.post("/api/admin/import", json=exported) assert imported.status_code == 200, imported.text listed = client.get("/api/schedules").json()["items"] assert any(s["title"] == "导出日程" for s in listed) assert client.get("/api/admin/stats").json()["scheduleCount"] == 1