feat(api): retire phase-1/2 /workstreams REST aliases with 410 stranglers
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
Build and Publish Multi-Context Image / build-and-push (push) Successful in 2m18s

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:
tegwick 2026-07-10 13:31:57 +02:00
parent c4ac853e35
commit 7036e2aa6b
11 changed files with 320 additions and 139 deletions

View file

@ -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
}