from __future__ import annotations import json import os from datetime import datetime, timezone from pathlib import Path 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_get_workstreams_returns_410_and_meters(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 == 410 assert "retired" in r.json()["detail"] 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} async def test_weekly_review_accepts_hours_window(self, client): r = await client.post("/legacy-meter/interfaces", json={ "interface_key": "rest_api:GET /hour-window", "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?hours=8")).json() window_seconds = ( datetime.fromisoformat(review["window_end"].replace("Z", "+00:00")) - datetime.fromisoformat(review["window_start"].replace("Z", "+00:00")) ).total_seconds() assert 8 * 3600 - 5 <= window_seconds <= 8 * 3600 + 5 bad = await client.get("/legacy-meter/weekly-review?days=7&hours=8") assert bad.status_code == 422 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" assert review["activity_core_handoff"]["source_endpoint"] == "/legacy-meter/weekly-review" assert "window_start" in review assert "window_end" in review assert isinstance(review["interfaces"], list) assert isinstance(review["retirement_candidates"], list) 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" if os.environ.get("CAPTURE_LEGACY_METER_EVIDENCE"): evidence_dir = Path(__file__).resolve().parents[1] / "docs" / "evidence" evidence_dir.mkdir(parents=True, exist_ok=True) today = datetime.now(tz=timezone.utc).strftime("%Y%m%d") out = evidence_dir / f"legacy-meter-weekly-review-{today}.json" payload = { "captured_at": datetime.now(tz=timezone.utc).isoformat(), "source": "pytest", "workplan": "STATE-WP-0069", "weekly_review": review, } out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") 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_patch_workstream_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.patch( f"/workstreams/{wp['id']}", json={"status": "finished"}, headers={"X-StateHub-Component": "old-client"}, ) 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 } async def test_legacy_workplan_index_returns_410(self, client): r = await client.get( "/workstreams/workplan-index", headers={"X-StateHub-Component": "old-index-client"}, ) assert r.status_code == 410 assert "retired" in r.json()["detail"] assert r.headers["X-StateHub-Replacement"] == "/workplans/index" summary = (await client.get("/legacy-meter/summary")).json() item = _summary_by_key(summary)["rest_api:GET /workstreams/workplan-index"] assert item["window"]["calls"] == 1 assert item["window"]["components"] == {"old-index-client": 1} async def test_legacy_delete_workstream_returns_410(self, client): await _create_domain(client) topic = await _create_topic(client) wp = await _create_workplan(client, topic["id"]) r = await client.delete( f"/workstreams/{wp['id']}", headers={"X-StateHub-Component": "old-archiver"}, ) assert r.status_code == 410 assert "retired" in r.json()["detail"] assert r.headers["Deprecation"] == "true" assert r.headers["X-StateHub-Replacement"] == "DELETE /workplans/{workplan_id}" summary = (await client.get("/legacy-meter/summary")).json() item = _summary_by_key(summary)["rest_api:DELETE /workstreams/{workstream_id}"] assert item["window"]["calls"] == 1 assert item["window"]["components"] == {"old-archiver": 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=" 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=" 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_legacy_workstream_id_body_on_tasks_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( "/tasks/", json={ "workstream_id": str(wp["id"]), "title": "legacy task body", "status": "todo", }, headers={"X-StateHub-Component": "old-task-writer"}, ) assert r.status_code == 201 assert r.headers["Deprecation"] == "true" summary = (await client.get("/legacy-meter/summary")).json() item = _summary_by_key(summary)["rest_api:POST /tasks/ workstream_id"] assert item["window"]["calls"] == 1 assert item["window"]["components"] == {"old-task-writer": 1} async def test_legacy_workstream_id_body_on_decisions_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( "/decisions/", json={ "workstream_id": str(wp["id"]), "title": "legacy decision body", "decision_type": "pending", }, headers={"X-StateHub-Component": "old-decision-writer"}, ) assert r.status_code == 201 assert r.headers["Deprecation"] == "true" summary = (await client.get("/legacy-meter/summary")).json() item = _summary_by_key(summary)["rest_api:POST /decisions/ workstream_id"] assert item["window"]["calls"] == 1 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 == 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 }