state-hub/tests/test_legacy_meter.py
tegwick 6e5e150803
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
STATE-WP-0069 T04: Sunset headers and POST /progress body metering
Add Sunset to legacy compat responses (Jun 2027 planning horizon).
Meter POST /progress/ bodies that use workstream_id; wire hub-core body
hook. Consolidate workstreams deprecation headers via legacy_compat.
2026-07-08 23:06:45 +02:00

282 lines
12 KiB
Python

from __future__ import annotations
async def _create_domain(client, slug="legacy-domain", name="Legacy Domain"):
r = await client.post("/domains/", json={"slug": slug, "name": name})
assert r.status_code == 201, r.text
return r.json()
async def _create_topic(client, domain_slug="legacy-domain", slug="legacy-topic", title="Legacy Topic"):
r = await client.post("/topics/", json={
"slug": slug,
"title": title,
"domain": domain_slug,
})
assert r.status_code == 201, r.text
return r.json()
async def _create_repo(client, domain_slug="legacy-domain", slug="legacy-repo"):
r = await client.post("/repos/", json={
"domain_slug": domain_slug,
"slug": slug,
"name": "Legacy Repo",
})
assert r.status_code == 201, r.text
return r.json()
async def _create_workplan(client, topic_id, domain_slug="legacy-domain", slug="legacy-wp", title="Legacy WP"):
repo = await _create_repo(client, domain_slug=domain_slug, slug=f"{slug}-repo")
r = await client.post("/workplans/", json={
"repo_id": repo["id"],
"topic_id": topic_id,
"slug": slug,
"title": title,
"status": "ready",
})
assert r.status_code == 201, r.text
return r.json()
def _summary_by_key(summary):
return {item["interface"]["interface_key"]: item for item in summary["interfaces"]}
class TestWorkplanAliasesAndLegacyMeter:
async def test_preferred_workplan_routes_do_not_meter_legacy_usage(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
r = await client.get(f"/workplans/?topic_id={topic['id']}")
assert r.status_code == 200
assert [row["id"] for row in r.json()] == [wp["id"]]
r = await client.patch(f"/workplans/{wp['id']}", json={"status": "active"})
assert r.status_code == 200
assert r.json()["status"] == "active"
r = await client.get("/legacy-meter/summary")
assert r.status_code == 200
assert r.json()["interfaces"] == []
async def test_legacy_workstream_route_is_metered_with_identity_buckets(self, client):
await _create_domain(client)
topic = await _create_topic(client)
await _create_workplan(client, topic["id"])
r = await client.get(
f"/workstreams/?topic_id={topic['id']}",
headers={
"X-StateHub-Tenant": "tenant-a",
"X-StateHub-User": "alice",
"X-StateHub-Component": "old-client",
},
)
assert r.status_code == 200
assert r.headers["Deprecation"] == "true"
assert r.headers.get("Sunset")
assert r.headers["X-StateHub-Replacement"] == "/workplans/"
summary = (await client.get("/legacy-meter/summary")).json()
item = _summary_by_key(summary)["rest_api:GET /workstreams/"]
assert item["window"]["calls"] == 1
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_reports_unused_verified_legacy_interface(self, client):
r = await client.post("/legacy-meter/interfaces", json={
"interface_key": "rest_api:GET /obsolete",
"interface_kind": "rest_api",
"replacement_ref": "/workplans/",
"replacement_verified": True,
})
assert r.status_code == 201, r.text
review = (await client.get("/legacy-meter/weekly-review")).json()
assert review["activity_core_handoff"]["scheduler_owner"] == "activity-core"
candidates = _summary_by_key({"interfaces": review["retirement_candidates"]})
assert "rest_api:GET /obsolete" in candidates
assert candidates["rest_api:GET /obsolete"]["retirement_reason"] == "no measured usage in review window"
async def test_recent_usage_blocks_weekly_retirement_candidate(self, client):
r = await client.post("/legacy-meter/usage", json={
"interface_key": "rest_api:GET /old-but-used",
"interface_kind": "rest_api",
"replacement_ref": "/workplans/",
"replacement_verified": True,
"tenant_key": "tenant-a",
"user_key": "alice",
"component_key": "old-client",
})
assert r.status_code == 201, r.text
review = (await client.get("/legacy-meter/weekly-review")).json()
items = _summary_by_key(review)
item = items["rest_api:GET /old-but-used"]
assert item["window"]["calls"] == 1
assert item["retirement_candidate"] is False
assert item["retirement_reason"] == "1 call(s) in review window"
async def test_legacy_completion_event_is_metered_and_workplan_event_is_preferred(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
r = await client.patch(
f"/workstreams/{wp['id']}",
json={"status": "finished"},
headers={"X-StateHub-Component": "old-client"},
)
assert r.status_code == 200
assert r.json()["status"] == "finished"
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
}
event = items["event_subject:org.statehub.workstream.completed"]
assert event["interface"]["replacement_ref"] == "org.statehub.workplan.completed"
assert event["window"]["components"] == {"state-hub.events": 1}
async def test_legacy_workstream_id_query_param_on_tasks_is_metered(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
task_r = await client.post("/tasks/", json={
"workplan_id": wp["id"],
"title": "Meter me",
"status": "todo",
})
assert task_r.status_code == 201, task_r.text
r = await client.get(
f"/tasks/?workstream_id={wp['id']}",
headers={
"X-StateHub-Tenant": "tenant-a",
"X-StateHub-User": "alice",
"X-StateHub-Component": "old-task-client",
},
)
assert r.status_code == 200
assert r.headers["Deprecation"] == "true"
assert r.headers["X-StateHub-Replacement"] == "/tasks/?workplan_id=<workplan_id>"
summary = (await client.get("/legacy-meter/summary")).json()
item = _summary_by_key(summary)["rest_api:GET /tasks/?workstream_id"]
assert item["window"]["calls"] == 1
assert item["window"]["components"] == {"old-task-client": 1}
async def test_legacy_workstream_id_query_param_on_progress_is_metered(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
r = await client.get(
f"/progress/?workstream_id={wp['id']}",
headers={"X-StateHub-Component": "old-progress-client"},
)
assert r.status_code == 200
assert r.headers["Deprecation"] == "true"
assert r.headers["X-StateHub-Replacement"] == "/progress/?workplan_id=<workplan_id>"
summary = (await client.get("/legacy-meter/summary")).json()
item = _summary_by_key(summary)["rest_api:GET /progress/?workstream_id"]
assert item["window"]["calls"] == 1
assert item["window"]["components"] == {"old-progress-client": 1}
async def test_legacy_workstream_id_body_on_progress_post_is_metered(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
r = await client.post(
"/progress/",
json={
"workstream_id": str(wp["id"]),
"event_type": "note",
"summary": "legacy body field",
"author": "test",
},
headers={"X-StateHub-Component": "old-progress-writer"},
)
assert r.status_code == 201
assert r.headers["Deprecation"] == "true"
assert r.headers["X-StateHub-Replacement"] == "POST /progress/ with workplan_id"
summary = (await client.get("/legacy-meter/summary")).json()
item = _summary_by_key(summary)["rest_api:POST /progress/ workstream_id"]
assert item["window"]["calls"] == 1
assert item["window"]["components"] == {"old-progress-writer": 1}
async def test_workplan_id_body_on_progress_post_is_not_metered(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
r = await client.post(
"/progress/",
json={
"workplan_id": str(wp["id"]),
"event_type": "note",
"summary": "preferred body field",
"author": "test",
},
)
assert r.status_code == 201
assert r.headers.get("Deprecation") != "true"
summary = (await client.get("/legacy-meter/summary")).json()
assert _summary_by_key(summary).get("rest_api:POST /progress/ workstream_id") is None
async def test_workplan_id_query_param_on_tasks_is_not_metered(self, client):
await _create_domain(client)
topic = await _create_topic(client)
wp = await _create_workplan(client, topic["id"])
r = await client.get(f"/tasks/?workplan_id={wp['id']}")
assert r.status_code == 200
assert r.headers.get("Deprecation") != "true"
summary = (await client.get("/legacy-meter/summary")).json()
assert _summary_by_key(summary).get("rest_api:GET /tasks/?workstream_id") is None
async def test_workplan_dependency_and_execution_aliases(self, client):
await _create_domain(client)
topic = await _create_topic(client)
first = await _create_workplan(client, topic["id"], slug="first", title="First")
second = await _create_workplan(client, topic["id"], slug="second", title="Second")
r = await client.post(
f"/workplans/{first['id']}/dependencies/",
json={"to_workstream_id": second["id"]},
)
assert r.status_code == 201, r.text
r = await client.get(f"/workplans/{first['id']}/dependencies/")
assert r.status_code == 200
assert len(r.json()) == 1
r = await client.patch(
f"/execution/workplans/{first['id']}/intent",
json={"execution_state": "queued", "launch_mode": "queued"},
)
assert r.status_code == 200
assert r.json()["execution_state"] == "queued"
r = await client.patch(
f"/execution/workstreams/{first['id']}/intent",
json={"execution_state": "manual", "launch_mode": "manual"},
headers={"X-StateHub-Component": "old-executor"},
)
assert r.status_code == 200
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}