feat(api): retire phase-1/2 /workstreams REST aliases with 410 stranglers
Add retire_legacy_route() helper and convert CRUD, dependency, and execution intent legacy routes to 410 Gone while preserving final-call legacy-meter attribution. Update tests, retirement backlog, and close STATE-WP-0075 with handoff to STATE-WP-0070 T02 and STATE-WP-0073 T04.
This commit is contained in:
parent
c4ac853e35
commit
7036e2aa6b
11 changed files with 320 additions and 139 deletions
|
|
@ -1280,7 +1280,7 @@ class TestC06WorkstreamCreation:
|
|||
report = fix_repo("http://unused", "demo-repo")
|
||||
|
||||
assert created_workstreams[0]["slug"] == "demo-repo-state-wp-0001"
|
||||
assert created_tasks[0]["workstream_id"] == "new-ws"
|
||||
assert created_tasks[0]["workplan_id"] == "new-ws"
|
||||
patched = wp.read_text(encoding="utf-8")
|
||||
assert 'state_hub_workstream_id: "new-ws"' in patched
|
||||
assert 'state_hub_task_id: "new-task"' in patched
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert r.status_code == 200
|
||||
assert r.json()["interfaces"] == []
|
||||
|
||||
async def test_legacy_workstream_route_is_metered_with_identity_buckets(self, client):
|
||||
async def test_legacy_get_workstreams_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
await _create_workplan(client, topic["id"])
|
||||
|
|
@ -80,7 +80,8 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
"X-StateHub-Component": "old-client",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["Deprecation"] == "true"
|
||||
assert r.headers.get("Sunset")
|
||||
assert r.headers["X-StateHub-Replacement"] == "/workplans/"
|
||||
|
|
@ -91,7 +92,6 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert item["window"]["tenants"] == {"tenant-a": 1}
|
||||
assert item["window"]["users"] == {"alice": 1}
|
||||
assert item["window"]["components"] == {"old-client": 1}
|
||||
assert item["retirement_candidate"] is False
|
||||
|
||||
async def test_weekly_review_accepts_hours_window(self, client):
|
||||
r = await client.post("/legacy-meter/interfaces", json={
|
||||
|
|
@ -164,7 +164,7 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert item["retirement_candidate"] is False
|
||||
assert item["retirement_reason"] == "1 call(s) in review window"
|
||||
|
||||
async def test_workplan_finish_does_not_meter_legacy_completion_event(self, client):
|
||||
async def test_legacy_patch_workstream_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
wp = await _create_workplan(client, topic["id"])
|
||||
|
|
@ -174,15 +174,14 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
json={"status": "finished"},
|
||||
headers={"X-StateHub-Component": "old-client"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "finished"
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
items = _summary_by_key(summary)
|
||||
assert items["rest_api:PATCH /workstreams/{workstream_id}"]["window"]["components"] == {
|
||||
"old-client": 1
|
||||
}
|
||||
assert items.get("event_subject:org.statehub.workstream.completed") is None
|
||||
|
||||
async def test_legacy_workplan_index_returns_410(self, client):
|
||||
r = await client.get(
|
||||
|
|
@ -390,9 +389,100 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
json={"execution_state": "manual", "launch_mode": "manual"},
|
||||
headers={"X-StateHub-Component": "old-executor"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["X-StateHub-Replacement"] == "/execution/workplans/{workplan_id}/intent"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:PATCH /execution/workstreams/{workstream_id}/intent"]
|
||||
assert item["window"]["components"] == {"old-executor": 1}
|
||||
|
||||
async def test_legacy_post_workstream_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
repo = await _create_repo(client, slug="legacy-create-repo")
|
||||
|
||||
r = await client.post(
|
||||
"/workstreams/",
|
||||
json={
|
||||
"repo_id": str(repo["id"]),
|
||||
"topic_id": str(topic["id"]),
|
||||
"slug": "legacy-create",
|
||||
"title": "Legacy Create",
|
||||
"status": "active",
|
||||
},
|
||||
headers={"X-StateHub-Component": "old-creator"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["X-StateHub-Replacement"] == "/workplans/"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:POST /workstreams/"]
|
||||
assert item["window"]["calls"] == 1
|
||||
assert item["window"]["components"] == {"old-creator": 1}
|
||||
|
||||
async def test_legacy_get_workstream_detail_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
wp = await _create_workplan(client, topic["id"])
|
||||
|
||||
r = await client.get(
|
||||
f"/workstreams/{wp['id']}",
|
||||
headers={"X-StateHub-Component": "old-reader"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["X-StateHub-Replacement"] == "/workplans/{workplan_id}"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:GET /workstreams/{workstream_id}"]
|
||||
assert item["window"]["calls"] == 1
|
||||
assert item["window"]["components"] == {"old-reader": 1}
|
||||
|
||||
async def test_legacy_workstream_dependency_routes_return_410_and_meter(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
first = await _create_workplan(client, topic["id"], slug="dep-first", title="First")
|
||||
second = await _create_workplan(client, topic["id"], slug="dep-second", title="Second")
|
||||
|
||||
dep = await client.post(
|
||||
f"/workplans/{first['id']}/dependencies/",
|
||||
json={"to_workplan_id": second["id"]},
|
||||
)
|
||||
assert dep.status_code == 201, dep.text
|
||||
dep_id = dep.json()["id"]
|
||||
|
||||
r = await client.post(
|
||||
f"/workstreams/{first['id']}/dependencies/",
|
||||
json={"to_workstream_id": second["id"]},
|
||||
headers={"X-StateHub-Component": "old-dep-writer"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
r = await client.get(
|
||||
f"/workstreams/{first['id']}/dependencies/",
|
||||
headers={"X-StateHub-Component": "old-dep-reader"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
r = await client.delete(
|
||||
f"/workstreams/{first['id']}/dependencies/{dep_id}",
|
||||
headers={"X-StateHub-Component": "old-dep-archiver"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
items = _summary_by_key(summary)
|
||||
assert items["rest_api:POST /workstreams/{workstream_id}/dependencies/"]["window"]["components"] == {
|
||||
"old-dep-writer": 1
|
||||
}
|
||||
assert items["rest_api:GET /workstreams/{workstream_id}/dependencies/"]["window"]["components"] == {
|
||||
"old-dep-reader": 1
|
||||
}
|
||||
assert items["rest_api:DELETE /workstreams/{workstream_id}/dependencies/{dep_id}"]["window"]["components"] == {
|
||||
"old-dep-archiver": 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
|
||||
r = await client.get(f"/workstreams/?topic_id={topic['id']}")
|
||||
r = await client.get(f"/workplans/?topic_id={topic['id']}")
|
||||
assert r.status_code == 200
|
||||
ids = [w["id"] for w in r.json()]
|
||||
assert ws["id"] in ids
|
||||
|
|
@ -154,7 +154,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
|
||||
r = await client.patch(f"/workstreams/{ws['id']}", json={"status": "finished"})
|
||||
r = await client.patch(f"/workplans/{ws['id']}", json={"status": "finished"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "finished"
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
|
||||
r = await client.patch(f"/workstreams/{ws['id']}", json={"status": "completed"})
|
||||
r = await client.patch(f"/workplans/{ws['id']}", json={"status": "completed"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "finished"
|
||||
|
||||
|
|
@ -171,9 +171,9 @@ class TestWorkstreams:
|
|||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
await client.patch(f"/workstreams/{ws['id']}", json={"owner": "alice"})
|
||||
await client.patch(f"/workplans/{ws['id']}", json={"owner": "alice"})
|
||||
|
||||
r = await client.get("/workstreams/?owner=alice")
|
||||
r = await client.get("/workplans/?owner=alice")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
assert r.json()[0]["id"] == ws["id"]
|
||||
|
|
@ -183,7 +183,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"], slug="my-special-ws")
|
||||
|
||||
r = await client.get("/workstreams/?slug=my-special-ws")
|
||||
r = await client.get("/workplans/?slug=my-special-ws")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
|
||||
|
|
@ -358,7 +358,7 @@ class TestTasks:
|
|||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ class TestTasks:
|
|||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "blocked"
|
||||
|
||||
|
|
@ -460,7 +460,7 @@ class TestStateSummary:
|
|||
dependency_ws = await _create_workstream(client, topic["id"], slug="dependency-ws")
|
||||
|
||||
r = await client.post(
|
||||
f"/workstreams/{blocked_ws['id']}/dependencies/",
|
||||
f"/workplans/{blocked_ws['id']}/dependencies/",
|
||||
json={
|
||||
"to_workstream_id": dependency_ws["id"],
|
||||
"description": "Blocked until dependency completes",
|
||||
|
|
@ -543,7 +543,7 @@ class TestFlowEndpoints:
|
|||
assert r.status_code == 200
|
||||
assert r.json()["current_workstation"] == "finished"
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "finished"
|
||||
|
||||
async def test_advance_workstream_respects_current_exit_assertions(self, client):
|
||||
|
|
@ -554,7 +554,7 @@ class TestFlowEndpoints:
|
|||
task = await _create_task(client, ws["id"])
|
||||
await client.patch(f"/tasks/{task['id']}", json={"status": "done"})
|
||||
await client.post(
|
||||
f"/workstreams/{ws['id']}/dependencies/",
|
||||
f"/workplans/{ws['id']}/dependencies/",
|
||||
json={
|
||||
"to_workstream_id": dependency_ws["id"],
|
||||
"description": "Dependency must finish first",
|
||||
|
|
@ -565,7 +565,7 @@ class TestFlowEndpoints:
|
|||
assert r.status_code == 409
|
||||
assert r.json()["detail"]["blocking_assertions"][0]["id"] == "dependencies.all_complete"
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
|
||||
|
|
@ -677,7 +677,7 @@ class TestReconciliationEndpoints:
|
|||
assert body["workplan_path"] == "workplans/STATE-WP-9999-demo.md"
|
||||
assert "status: backlog" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "backlog"
|
||||
|
||||
async def test_apply_workstream_without_file_does_not_mutate_db(self, client):
|
||||
|
|
@ -700,7 +700,7 @@ class TestReconciliationEndpoints:
|
|||
assert body["write_through_result"] == "not_applicable"
|
||||
assert body["reconciliation_record_id"]
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
r = await client.get("/messages/?to_agent=state-hub&unread_only=true")
|
||||
|
|
@ -805,7 +805,7 @@ class TestReconciliationEndpoints:
|
|||
assert "status: active" in text
|
||||
assert "status: progress" in text
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
r = await client.get(f"/tasks/{task['id']}")
|
||||
|
|
@ -883,7 +883,7 @@ class TestReconciliationEndpoints:
|
|||
"---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
await client.patch(f"/workstreams/{ws['id']}", json={"status": "ready"})
|
||||
await client.patch(f"/workplans/{ws['id']}", json={"status": "ready"})
|
||||
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "workstream",
|
||||
|
|
@ -901,7 +901,7 @@ class TestReconciliationEndpoints:
|
|||
assert "expected" in body["reason"]
|
||||
assert "status: active" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "ready"
|
||||
|
||||
async def test_apply_workstream_file_status_drift_creates_conflict_message(self, client, tmp_path):
|
||||
|
|
@ -941,7 +941,7 @@ class TestReconciliationEndpoints:
|
|||
assert "differs from cached DB status" in body["reason"]
|
||||
assert "status: ready" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
async def test_apply_task_unavailable_host_path_creates_conflict_message(self, client, tmp_path):
|
||||
|
|
@ -987,7 +987,7 @@ class TestExecutionQueueEndpoints:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"], status="ready")
|
||||
|
||||
r = await client.patch(f"/execution/workstreams/{ws['id']}/intent", json={
|
||||
r = await client.patch(f"/execution/workplans/{ws['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"concurrency_mode": "parallel",
|
||||
|
|
@ -1002,7 +1002,7 @@ class TestExecutionQueueEndpoints:
|
|||
assert body["concurrency_mode"] == "parallel"
|
||||
assert body["queue_rank"] == 7
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "ready"
|
||||
assert r.json()["execution_state"] == "queued"
|
||||
|
||||
|
|
@ -1041,23 +1041,23 @@ class TestExecutionQueueEndpoints:
|
|||
planning_priority="low",
|
||||
planning_order=3,
|
||||
)
|
||||
await client.patch(f"/execution/workstreams/{queued['id']}/intent", json={
|
||||
await client.patch(f"/execution/workplans/{queued['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"queue_rank": 2,
|
||||
})
|
||||
await client.patch(f"/execution/workstreams/{blocked['id']}/intent", json={
|
||||
await client.patch(f"/execution/workplans/{blocked['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"queue_rank": 1,
|
||||
})
|
||||
await client.patch(f"/execution/workstreams/{lifecycle_blocked['id']}/intent", json={
|
||||
await client.patch(f"/execution/workplans/{lifecycle_blocked['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"queue_rank": 0,
|
||||
})
|
||||
await client.post(
|
||||
f"/workstreams/{blocked['id']}/dependencies/",
|
||||
f"/workplans/{blocked['id']}/dependencies/",
|
||||
json={"to_workstream_id": dependency["id"], "description": "wait"},
|
||||
)
|
||||
|
||||
|
|
@ -1096,7 +1096,7 @@ class TestExecutionQueueEndpoints:
|
|||
assert body["immediate_pickup"] is True
|
||||
assert body["status"] == "requested"
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
updated = r.json()
|
||||
assert updated["status"] == "ready"
|
||||
assert updated["execution_state"] == "launching"
|
||||
|
|
@ -1508,7 +1508,7 @@ class TestFabricGraphReadModel:
|
|||
after_progress = await client.get("/progress/")
|
||||
assert len(after_progress.json()) == before_progress_count
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "ready"
|
||||
r = await client.get(f"/tasks/{task['id']}")
|
||||
assert r.json()["status"] == "todo"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue