feat: 新增日程面板、复盘页面与 Agent 读写接口
- 日程:单次/每日/每周/每月重复,JSON 存储,月历按优先级着色圆点 + 悬停提示 - 复盘页:可自定义折线图卡片(SVG) + 每日心得总览(按日期/任务展开) - Agent 接口:GET /api/agent/overview 读日程+进度,POST /api/agent/schedules 建日程(PPM_API_TOKEN Bearer 鉴权) - 导出/导入/统计纳入日程与图表;47 项测试全绿
This commit is contained in:
167
tests/test_schedules.py
Normal file
167
tests/test_schedules.py
Normal file
@@ -0,0 +1,167 @@
|
||||
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
|
||||
Reference in New Issue
Block a user