feat: 新增日程面板、复盘页面与 Agent 读写接口

- 日程:单次/每日/每周/每月重复,JSON 存储,月历按优先级着色圆点 + 悬停提示
- 复盘页:可自定义折线图卡片(SVG) + 每日心得总览(按日期/任务展开)
- Agent 接口:GET /api/agent/overview 读日程+进度,POST /api/agent/schedules 建日程(PPM_API_TOKEN Bearer 鉴权)
- 导出/导入/统计纳入日程与图表;47 项测试全绿
This commit is contained in:
MobKBK
2026-09-04 14:46:41 +08:00
parent 5f50eb54ed
commit 8340d1dbbd
13 changed files with 1027 additions and 19 deletions

167
tests/test_schedules.py Normal file
View 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