generated from coulomb/repo-seed
feat(tasks): adopt canonical task statuses
This commit is contained in:
@@ -35,7 +35,7 @@ async def _create_workstream(client, topic_id):
|
||||
return r.json()
|
||||
|
||||
|
||||
async def _create_task(client, workstream_id, title="Test task", status="blocked"):
|
||||
async def _create_task(client, workstream_id, title="Test task", status="wait"):
|
||||
r = await client.post("/tasks/", json={
|
||||
"workstream_id": workstream_id, "title": title,
|
||||
})
|
||||
@@ -43,7 +43,7 @@ async def _create_task(client, workstream_id, title="Test task", status="blocked
|
||||
task = r.json()
|
||||
if status != "todo":
|
||||
patch = {"status": status}
|
||||
if status == "blocked":
|
||||
if status == "wait":
|
||||
patch["blocking_reason"] = "Waiting for capability request"
|
||||
r2 = await client.patch(f"/tasks/{task['id']}", json=patch)
|
||||
assert r2.status_code == 200, r2.text
|
||||
@@ -229,7 +229,7 @@ class TestCapabilityRequestLifecycle:
|
||||
await _setup_two_domains(client)
|
||||
topic = await _create_topic(client, "custodian")
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
task = await _create_task(client, ws["id"], status="blocked")
|
||||
task = await _create_task(client, ws["id"], status="wait")
|
||||
|
||||
req = await _create_request(client, blocking_task_id=task["id"])
|
||||
|
||||
|
||||
@@ -558,11 +558,11 @@ class TestStatusOrder:
|
||||
def test_todo_is_lowest(self):
|
||||
assert STATUS_ORDER["todo"] == 0
|
||||
|
||||
def test_done_and_cancelled_are_highest(self):
|
||||
assert STATUS_ORDER["done"] == STATUS_ORDER["cancelled"] == 2
|
||||
def test_done_and_cancel_are_highest(self):
|
||||
assert STATUS_ORDER["done"] == STATUS_ORDER["cancel"] == 2
|
||||
|
||||
def test_in_progress_and_blocked_are_mid(self):
|
||||
assert STATUS_ORDER["in_progress"] == STATUS_ORDER["blocked"] == 1
|
||||
def test_progress_and_wait_are_mid(self):
|
||||
assert STATUS_ORDER["progress"] == STATUS_ORDER["wait"] == 1
|
||||
|
||||
def test_db_ahead_detected(self):
|
||||
"""done (DB) vs todo (file) — DB is ahead."""
|
||||
@@ -573,8 +573,8 @@ class TestStatusOrder:
|
||||
assert STATUS_ORDER["todo"] < STATUS_ORDER["done"]
|
||||
|
||||
def test_same_rank_treated_as_db_ahead(self):
|
||||
"""in_progress (DB) vs blocked (file) — same rank, no regression."""
|
||||
assert STATUS_ORDER["in_progress"] >= STATUS_ORDER["blocked"]
|
||||
"""progress (DB) vs wait (file) — same rank, no regression."""
|
||||
assert STATUS_ORDER["progress"] >= STATUS_ORDER["wait"]
|
||||
|
||||
def test_todo_to_done_is_regression(self):
|
||||
"""Applying file=todo to DB=done would be a regression."""
|
||||
@@ -718,7 +718,7 @@ class TestPatchTaskStatusInFile:
|
||||
content = (
|
||||
"---\nid: WP-001\n---\n"
|
||||
"```task\nid: T01\nstatus: todo\n```\n"
|
||||
"```task\nid: T02\nstatus: in_progress\n```\n"
|
||||
"```task\nid: T02\nstatus: progress\n```\n"
|
||||
)
|
||||
f = self._make_workplan(tmp_path, content)
|
||||
_patch_task_status_in_file(f, "T02", "done")
|
||||
@@ -783,7 +783,7 @@ class TestLifecycleRenormalization:
|
||||
"## Implement Demo\n\n"
|
||||
"```task\n"
|
||||
"id: STATE-WP-0001-T01\n"
|
||||
"status: in_progress\n"
|
||||
"status: progress\n"
|
||||
"priority: high\n"
|
||||
"state_hub_task_id: \"task-1\"\n"
|
||||
"```\n",
|
||||
@@ -805,7 +805,7 @@ class TestLifecycleRenormalization:
|
||||
task = {
|
||||
"id": "task-1",
|
||||
"title": "Implement Demo",
|
||||
"status": "in_progress",
|
||||
"status": "progress",
|
||||
"description": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from api.services.lifecycle import (
|
||||
def test_task_start_activates_planning_parent(parent_status):
|
||||
assert should_activate_parent_for_task_start(
|
||||
previous_task_status="todo",
|
||||
new_task_status="in_progress",
|
||||
new_task_status="progress",
|
||||
parent_workstream_status=parent_status,
|
||||
)
|
||||
|
||||
@@ -25,15 +25,15 @@ def test_task_start_activates_planning_parent(parent_status):
|
||||
def test_task_start_does_not_rewrite_non_planning_parent(parent_status):
|
||||
assert not should_activate_parent_for_task_start(
|
||||
previous_task_status="todo",
|
||||
new_task_status="in_progress",
|
||||
new_task_status="progress",
|
||||
parent_workstream_status=parent_status,
|
||||
)
|
||||
|
||||
|
||||
def test_task_start_requires_todo_to_in_progress_transition():
|
||||
def test_task_start_requires_todo_to_progress_transition():
|
||||
assert not should_activate_parent_for_task_start(
|
||||
previous_task_status="in_progress",
|
||||
new_task_status="in_progress",
|
||||
previous_task_status="progress",
|
||||
new_task_status="progress",
|
||||
parent_workstream_status="ready",
|
||||
)
|
||||
assert not should_activate_parent_for_task_start(
|
||||
@@ -44,22 +44,22 @@ def test_task_start_requires_todo_to_in_progress_transition():
|
||||
|
||||
|
||||
def test_has_active_task_status_ignores_terminal_and_todo_statuses():
|
||||
assert has_active_task_status(["todo", "done", "cancelled"]) is False
|
||||
assert has_active_task_status(["todo", "blocked"]) is True
|
||||
assert has_active_task_status(["in_progress"]) is True
|
||||
assert has_active_task_status(["todo", "done", "cancel"]) is False
|
||||
assert has_active_task_status(["todo", "wait"]) is True
|
||||
assert has_active_task_status(["progress"]) is True
|
||||
|
||||
|
||||
def test_active_task_state_activates_planning_parent_for_renormalization():
|
||||
assert should_activate_parent_for_active_tasks(
|
||||
parent_workstream_status="proposed",
|
||||
task_statuses=["todo", "in_progress"],
|
||||
task_statuses=["todo", "progress"],
|
||||
)
|
||||
|
||||
|
||||
def test_active_task_state_does_not_unblock_blocked_parent():
|
||||
assert not should_activate_parent_for_active_tasks(
|
||||
parent_workstream_status="blocked",
|
||||
task_statuses=["in_progress"],
|
||||
task_statuses=["progress"],
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_status_value_unwraps_enum_like_values():
|
||||
class Status:
|
||||
value = "In_Progress"
|
||||
|
||||
assert status_value(Status()) == "in_progress"
|
||||
assert status_value(Status()) == "progress"
|
||||
|
||||
|
||||
def test_transition_workstream_status_normalizes_aliases():
|
||||
@@ -92,10 +92,10 @@ def test_transition_task_status_activates_parent_once():
|
||||
|
||||
task = Task()
|
||||
ws = Workstream()
|
||||
result = transition_task_status(task, "in_progress", parent_workstream=ws)
|
||||
result = transition_task_status(task, "progress", parent_workstream=ws)
|
||||
|
||||
assert task.status == "in_progress"
|
||||
assert task.status == "progress"
|
||||
assert ws.status == "active"
|
||||
assert result.parent_activated is True
|
||||
assert result.previous_status == "todo"
|
||||
assert result.target_status == "in_progress"
|
||||
assert result.target_status == "progress"
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_archived_workstream_file_requires_confirmation():
|
||||
def test_task_status_update_writes_through_to_task_block():
|
||||
result = classify_task_status_change(
|
||||
current_status="todo",
|
||||
target_status="in_progress",
|
||||
target_status="progress",
|
||||
file_backed=True,
|
||||
task_linked=True,
|
||||
)
|
||||
@@ -80,7 +80,7 @@ def test_task_status_update_writes_through_to_task_block():
|
||||
def test_task_without_file_is_deferred():
|
||||
result = classify_task_status_change(
|
||||
current_status="todo",
|
||||
target_status="in_progress",
|
||||
target_status="progress",
|
||||
file_backed=False,
|
||||
task_linked=True,
|
||||
)
|
||||
@@ -92,7 +92,7 @@ def test_task_without_file_is_deferred():
|
||||
def test_unlinked_task_is_deferred_until_link_repaired():
|
||||
result = classify_task_status_change(
|
||||
current_status="todo",
|
||||
target_status="in_progress",
|
||||
target_status="progress",
|
||||
file_backed=True,
|
||||
task_linked=False,
|
||||
)
|
||||
@@ -101,22 +101,22 @@ def test_unlinked_task_is_deferred_until_link_repaired():
|
||||
assert "not linked" in result.reason
|
||||
|
||||
|
||||
def test_blocked_task_requires_blocking_reason():
|
||||
def test_wait_task_requires_blocking_reason():
|
||||
result = classify_task_status_change(
|
||||
current_status="todo",
|
||||
target_status="blocked",
|
||||
target_status="wait",
|
||||
file_backed=True,
|
||||
task_linked=True,
|
||||
)
|
||||
|
||||
assert result.reconciliation_class == ReconciliationClass.HUMAN_CONFIRMATION
|
||||
assert "blocking reason" in result.reason
|
||||
assert "wait condition" in result.reason
|
||||
|
||||
|
||||
def test_blocked_task_with_reason_writes_through():
|
||||
def test_wait_task_with_reason_writes_through():
|
||||
result = classify_task_status_change(
|
||||
current_status="todo",
|
||||
target_status="blocked",
|
||||
target_status="wait",
|
||||
file_backed=True,
|
||||
task_linked=True,
|
||||
blocking_reason="Waiting on dependency",
|
||||
|
||||
@@ -207,7 +207,7 @@ class TestTasks:
|
||||
|
||||
r = await client.delete(f"/tasks/{task['id']}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "cancelled"
|
||||
assert r.json()["status"] == "cancel"
|
||||
|
||||
async def test_filter_by_priority(self, client):
|
||||
await _create_domain(client)
|
||||
@@ -238,7 +238,7 @@ class TestTasks:
|
||||
)
|
||||
task = await _create_task(client, ws["id"])
|
||||
|
||||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"})
|
||||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
@@ -251,7 +251,7 @@ class TestTasks:
|
||||
ws = await _create_workstream(client, topic["id"], slug="blocked-ws", status="blocked")
|
||||
task = await _create_task(client, ws["id"])
|
||||
|
||||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"})
|
||||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
@@ -326,13 +326,13 @@ class TestStateSummary:
|
||||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
task = await _create_task(client, ws["id"])
|
||||
# Mark as blocked so it shows in blocked_tasks
|
||||
# Mark as wait so it shows in waiting_tasks
|
||||
await client.patch(f"/tasks/{task['id']}",
|
||||
json={"status": "blocked", "blocking_reason": "waiting on dep"})
|
||||
json={"status": "wait", "blocking_reason": "waiting on dep"})
|
||||
|
||||
r = await client.get("/state/summary")
|
||||
body = r.json()
|
||||
assert len(body["blocked_tasks"]) >= 1
|
||||
assert len(body["waiting_tasks"]) >= 1
|
||||
|
||||
async def test_summary_derives_blocked_workstream_from_flow_engine(self, client):
|
||||
await _create_domain(client)
|
||||
@@ -449,7 +449,7 @@ class TestReconciliationEndpoints:
|
||||
assert body["reconciliation_class"] == "human_confirmation"
|
||||
assert "open work" in body["reason"]
|
||||
|
||||
async def test_classify_task_blocked_without_reason_needs_confirmation(self, client):
|
||||
async def test_classify_task_wait_without_reason_needs_confirmation(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
@@ -458,7 +458,7 @@ class TestReconciliationEndpoints:
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "task",
|
||||
"target_id": task["id"],
|
||||
"target_status": "blocked",
|
||||
"target_status": "wait",
|
||||
"file_backed": True,
|
||||
"task_linked": True,
|
||||
})
|
||||
@@ -467,7 +467,7 @@ class TestReconciliationEndpoints:
|
||||
body = r.json()
|
||||
assert body["current_status"] == "todo"
|
||||
assert body["reconciliation_class"] == "human_confirmation"
|
||||
assert "blocking reason" in body["reason"]
|
||||
assert "wait condition" in body["reason"]
|
||||
|
||||
async def test_classify_unknown_workstream_returns_404(self, client):
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
@@ -582,7 +582,7 @@ class TestReconciliationEndpoints:
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "task",
|
||||
"target_id": task["id"],
|
||||
"target_status": "in_progress",
|
||||
"target_status": "progress",
|
||||
"actor": "dashboard",
|
||||
"apply": True,
|
||||
})
|
||||
@@ -592,10 +592,10 @@ class TestReconciliationEndpoints:
|
||||
assert body["reconciliation_class"] == "write_through"
|
||||
assert body["write_through_result"] == "applied"
|
||||
assert body["workplan_path"] == "workplans/STATE-WP-9999-demo.md"
|
||||
assert "status: in_progress" in wp.read_text(encoding="utf-8")
|
||||
assert "status: progress" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/tasks/{task['id']}")
|
||||
assert r.json()["status"] == "in_progress"
|
||||
assert r.json()["status"] == "progress"
|
||||
|
||||
async def test_apply_task_start_write_through_activates_parent_file_and_db(self, client, tmp_path):
|
||||
await _create_domain(client)
|
||||
@@ -630,7 +630,7 @@ class TestReconciliationEndpoints:
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "task",
|
||||
"target_id": task["id"],
|
||||
"target_status": "in_progress",
|
||||
"target_status": "progress",
|
||||
"actor": "dashboard",
|
||||
"expected_current_status": "todo",
|
||||
"apply": True,
|
||||
@@ -641,13 +641,13 @@ class TestReconciliationEndpoints:
|
||||
assert body["write_through_result"] == "applied"
|
||||
text = wp.read_text(encoding="utf-8")
|
||||
assert "status: active" in text
|
||||
assert "status: in_progress" in text
|
||||
assert "status: progress" in text
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
r = await client.get(f"/tasks/{task['id']}")
|
||||
assert r.json()["status"] == "in_progress"
|
||||
assert r.json()["status"] == "progress"
|
||||
|
||||
async def test_apply_task_confirmation_case_creates_reconciliation_message(self, client, tmp_path):
|
||||
await _create_domain(client)
|
||||
@@ -682,7 +682,7 @@ class TestReconciliationEndpoints:
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "task",
|
||||
"target_id": task["id"],
|
||||
"target_status": "blocked",
|
||||
"target_status": "wait",
|
||||
"apply": True,
|
||||
})
|
||||
|
||||
@@ -698,7 +698,7 @@ class TestReconciliationEndpoints:
|
||||
r = await client.get("/messages/?to_agent=state-hub&unread_only=true")
|
||||
messages = r.json()
|
||||
assert len(messages) == 1
|
||||
assert "blocking reason" in messages[0]["body"]
|
||||
assert "wait reason" in messages[0]["body"] or "wait condition" in messages[0]["body"]
|
||||
|
||||
async def test_apply_workstream_stale_expected_status_creates_conflict_message(self, client, tmp_path):
|
||||
await _create_domain(client)
|
||||
@@ -792,7 +792,7 @@ class TestReconciliationEndpoints:
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "task",
|
||||
"target_id": task["id"],
|
||||
"target_status": "in_progress",
|
||||
"target_status": "progress",
|
||||
"expected_current_status": "todo",
|
||||
"apply": True,
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_all_assertions_satisfied_reports_reachable_workstations():
|
||||
id="tasks.all_done",
|
||||
target="tasks.*.status",
|
||||
op="all_eq",
|
||||
value=["done", "cancelled"],
|
||||
value=["done", "cancel"],
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -26,7 +26,7 @@ def test_all_assertions_satisfied_reports_reachable_workstations():
|
||||
)
|
||||
|
||||
result = FlowEngine().evaluate(
|
||||
{"status": "active", "tasks": [{"status": "done"}, {"status": "cancelled"}]},
|
||||
{"status": "active", "tasks": [{"status": "done"}, {"status": "cancel"}]},
|
||||
flow,
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ def test_failing_exit_assertion_identifies_blocking_assertion():
|
||||
id="tasks.all_done",
|
||||
target="tasks.*.status",
|
||||
op="all_eq",
|
||||
value=["done", "cancelled"],
|
||||
value=["done", "cancel"],
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -87,7 +87,7 @@ def test_can_reach_checks_current_exit_assertions_before_target_entry():
|
||||
id="tasks.all_done",
|
||||
target="tasks.*.status",
|
||||
op="all_eq",
|
||||
value=["done", "cancelled"],
|
||||
value=["done", "cancel"],
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -144,7 +144,7 @@ def test_empty_assertions_make_all_workstations_reachable():
|
||||
entity_type="task",
|
||||
workstations=[
|
||||
WorkstationDef(name="todo"),
|
||||
WorkstationDef(name="in_progress"),
|
||||
WorkstationDef(name="progress"),
|
||||
WorkstationDef(name="done"),
|
||||
],
|
||||
)
|
||||
@@ -152,7 +152,7 @@ def test_empty_assertions_make_all_workstations_reachable():
|
||||
result = FlowEngine().evaluate({"status": "todo"}, flow)
|
||||
|
||||
assert result.exit_blocked is False
|
||||
assert result.reachable == ["todo", "in_progress", "done"]
|
||||
assert result.reachable == ["todo", "progress", "done"]
|
||||
|
||||
|
||||
def test_circular_reference_in_target_path_does_not_loop_forever():
|
||||
@@ -194,10 +194,10 @@ def test_yaml_flow_definitions_load_and_evaluate_representative_entities():
|
||||
assert "blocked" in [item.workstation for item in workstream_result.unreachable]
|
||||
|
||||
task_result = FlowEngine().evaluate(
|
||||
{"status": "blocked", "needs_human": False},
|
||||
{"status": "wait", "needs_human": False},
|
||||
flows["task"],
|
||||
)
|
||||
assert "in_progress" in task_result.reachable
|
||||
assert "progress" in task_result.reachable
|
||||
|
||||
contribution_result = FlowEngine().evaluate(
|
||||
{"status": "acknowledged", "previous_workstation": "acknowledged"},
|
||||
|
||||
@@ -173,7 +173,7 @@ class TestTokenPassthrough:
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
task = await _create_task(client, ws["id"])
|
||||
|
||||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"})
|
||||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
events = (await client.get("/token-events/", params={"task_id": task["id"]})).json()
|
||||
|
||||
Reference in New Issue
Block a user