feat(v4.5.0): Sprints & My Tasks
- New tables: sprints + sprint_pages with velocity_points - API: CRUD sprints (list/create/update/delete) - API: assign/remove pages to sprints - API: burndown chart data (total/completed/remaining points, ideal line) - 4 new tests (175 total)
This commit is contained in:
+140
-2
@@ -812,7 +812,7 @@ def test_my_tasks_page(client):
|
||||
assert "My Tasks" in resp.text
|
||||
|
||||
|
||||
def test_my_tasks_api(client):
|
||||
def test_my_tasks_cross_db(client):
|
||||
"""My Tasks API returns JSON."""
|
||||
r = client.post("/db/api", json={"name": "My Project"})
|
||||
cid = r.json()["id"]
|
||||
@@ -2568,4 +2568,142 @@ def test_auto_shift_no_blockers(client):
|
||||
|
||||
resp = client.post(f"/db/{coll_id}/pages/{p.json()['id']}/auto-shift/api")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["shifted"] is False
|
||||
assert resp.json()["shifted"] is False
|
||||
|
||||
|
||||
# ── v4.5.0: Sprints ──
|
||||
|
||||
|
||||
def test_sprint_crud(client):
|
||||
"""Full CRUD lifecycle for sprints."""
|
||||
resp = client.post("/db/api", json={"name": "Sprint DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
# Create
|
||||
resp = client.post(f"/workspace/collections/{coll_id}/sprints", json={
|
||||
"name": "Sprint 1",
|
||||
"start_date": "2026-07-01",
|
||||
"end_date": "2026-07-14",
|
||||
"goal": "Ship MVP",
|
||||
"status": "active",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "created"
|
||||
sid = resp.json()["id"]
|
||||
|
||||
# List
|
||||
resp = client.get(f"/workspace/collections/{coll_id}/sprints")
|
||||
assert len(resp.json()["sprints"]) == 1
|
||||
assert resp.json()["sprints"][0]["name"] == "Sprint 1"
|
||||
|
||||
# Update
|
||||
resp = client.put(f"/workspace/collections/{coll_id}/sprints/{sid}", json={
|
||||
"name": "Sprint 1 - Revised",
|
||||
"status": "completed",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = client.get(f"/workspace/collections/{coll_id}/sprints")
|
||||
assert resp.json()["sprints"][0]["name"] == "Sprint 1 - Revised"
|
||||
|
||||
# Delete
|
||||
resp = client.delete(f"/workspace/collections/{coll_id}/sprints/{sid}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "deleted"
|
||||
|
||||
|
||||
def test_sprint_assign_page(client):
|
||||
"""Assign a page to a sprint and remove it."""
|
||||
resp = client.post("/db/api", json={"name": "Assign DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
# Create sprint
|
||||
resp = client.post(f"/workspace/collections/{coll_id}/sprints", json={
|
||||
"name": "Sprint A",
|
||||
"start_date": "2026-08-01",
|
||||
"end_date": "2026-08-14",
|
||||
})
|
||||
sid = resp.json()["id"]
|
||||
|
||||
# Create page
|
||||
resp = client.post(f"/db/{coll_id}/pages/api", json={"title": "Task X"})
|
||||
pid = resp.json()["id"]
|
||||
|
||||
# Assign
|
||||
resp = client.post(f"/workspace/collections/{coll_id}/sprints/{sid}/assign", json={
|
||||
"page_id": pid,
|
||||
"velocity_points": 5,
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "assigned"
|
||||
|
||||
# Verify page count
|
||||
resp = client.get(f"/workspace/collections/{coll_id}/sprints")
|
||||
assert resp.json()["sprints"][0]["page_count"] == 1
|
||||
|
||||
# Remove
|
||||
resp = client.delete(f"/workspace/collections/{coll_id}/sprints/{sid}/assign/{pid}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "removed"
|
||||
|
||||
|
||||
def test_sprint_burndown(client):
|
||||
"""Burndown chart data calculation."""
|
||||
resp = client.post("/db/api", json={"name": "Burndown DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
resp = client.post(f"/workspace/collections/{coll_id}/sprints", json={
|
||||
"name": "Sprint B",
|
||||
"start_date": "2026-07-01",
|
||||
"end_date": "2026-07-14",
|
||||
})
|
||||
sid = resp.json()["id"]
|
||||
|
||||
# Add pages with different statuses
|
||||
p1 = client.post(f"/db/{coll_id}/pages/api", json={
|
||||
"title": "Done Task",
|
||||
"properties": {"Status": "Done"},
|
||||
})
|
||||
p2 = client.post(f"/db/{coll_id}/pages/api", json={
|
||||
"title": "In Progress Task",
|
||||
"properties": {"Status": "In Progress"},
|
||||
})
|
||||
|
||||
client.post(f"/workspace/collections/{coll_id}/sprints/{sid}/assign", json={
|
||||
"page_id": p1.json()["id"], "velocity_points": 3,
|
||||
})
|
||||
client.post(f"/workspace/collections/{coll_id}/sprints/{sid}/assign", json={
|
||||
"page_id": p2.json()["id"], "velocity_points": 5,
|
||||
})
|
||||
|
||||
resp = client.get(f"/workspace/collections/{coll_id}/sprints/burndown/{sid}")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total_points"] == 8
|
||||
assert data["completed_points"] == 3
|
||||
assert data["remaining_points"] == 5
|
||||
|
||||
|
||||
def test_my_tasks_page_render(client):
|
||||
"""My Tasks page renders cross-database aggregation."""
|
||||
# Create a task collection
|
||||
resp = client.post("/db/api", json={"name": "My Tasks DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
# Toggle to task mode
|
||||
client.put(f"/db/{coll_id}/toggle-task/api")
|
||||
|
||||
# Add pages
|
||||
client.post(f"/db/{coll_id}/pages/api", json={
|
||||
"title": "Urgent fix",
|
||||
"properties": {"Status": "Todo"},
|
||||
})
|
||||
client.post(f"/db/{coll_id}/pages/api", json={
|
||||
"title": "Deploy",
|
||||
"properties": {"Status": "Done"},
|
||||
})
|
||||
|
||||
resp = client.get("/my-tasks?view=all")
|
||||
assert resp.status_code == 200
|
||||
assert "Urgent fix" in resp.text
|
||||
assert "My Tasks DB" in resp.text
|
||||
Reference in New Issue
Block a user