"""Subscription-scoped efficiency rollup + owner triage participation. Covers the "云订阅里能看到 + 让大家都参与" feature: attributing per-server efficiency verdicts to a cloud subscription via the cost-facts path, the scope summary/instance endpoints and their owner scoping, and the non-destructive triage loop (keep/acknowledge/snooze) with freshness (a drifted verdict collapses a stale triage back to open). """ from __future__ import annotations import uuid from datetime import UTC, date, datetime, timedelta import pytest from httpx import AsyncClient from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import ( CmdbCloudScope, CmdbCostFact, CmdbServer, CmdbServerEfficiency, CmdbServerEfficiencyTriage, ) from app.integrations.models import Integration async def _integration(db: AsyncSession) -> Integration: integ = Integration( id=uuid.uuid4(), kind="cloud", provider="aws", name="aws-eff", slug=f"aws-{uuid.uuid4().hex[:6]}", enabled=True, is_primary=False, capabilities=["servers", "metrics"], ) db.add(integ) await db.flush() return integ async def _scope(db: AsyncSession, integ: Integration, **kw) -> CmdbCloudScope: d = { "id": uuid.uuid4(), "integration_id": str(integ.id), "scope_type": "aws_account", "external_id": kw.pop("external_id", f"acct-{uuid.uuid4().hex[:8]}"), "display_name": kw.pop("display_name", "sub-a"), } d.update(kw) s = CmdbCloudScope(**d) db.add(s) await db.flush() return s async def _server(db: AsyncSession, integ: Integration, **kw) -> CmdbServer: now = datetime.now(UTC) d = { "id": uuid.uuid4(), "instance_id": kw.pop("instance_id", f"i-{uuid.uuid4().hex[:10]}"), "name": kw.pop("name", "srv"), "status": kw.pop("status", "running"), "region": "us-east-1", "integration_id": str(integ.id), "first_seen_at": now - timedelta(days=100), "last_synced_at": now, } d.update(kw) s = CmdbServer(**d) db.add(s) await db.flush() return s async def _efficiency(db, integ, server, **kw) -> CmdbServerEfficiency: d = { "id": uuid.uuid4(), "server_id": str(server.id), "integration_id": str(integ.id), "verdict": kw.pop("verdict", "healthy"), "recommendation": kw.pop("recommendation", "keep"), "computed_at": datetime.now(UTC), } d.update(kw) e = CmdbServerEfficiency(**d) db.add(e) await db.flush() return e async def _cost_fact(db, integ, scope, server, amount=10): """Attribute a server to a scope the same way cost ingest does: a cost fact whose resource_id == the server's instance_id carries the scope_id.""" f = CmdbCostFact( fact_key=uuid.uuid4().hex, date=date.today(), integration_id=str(integ.id), scope_id=str(scope.id), resource_id=server.instance_id, resource_type="Instance", service="AmazonEC2", amount_usd=amount, currency="USD", ) db.add(f) await db.flush() return f # ── scope rollup ───────────────────────────────────────────────────── @pytest.mark.asyncio async def test_scope_summary_rolls_up_attributed_servers( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): integ = await _integration(db_session) scope_a = await _scope(db_session, integ, display_name="A") scope_b = await _scope(db_session, integ, display_name="B") in_a_idle = await _server(db_session, integ, name="a-idle") in_a_ok = await _server(db_session, integ, name="a-ok") in_b = await _server(db_session, integ, name="b-idle") await _efficiency(db_session, integ, in_a_idle, verdict="idle", recommendation="stop", estimated_monthly_savings_usd=120) await _efficiency(db_session, integ, in_a_ok, verdict="healthy") await _efficiency(db_session, integ, in_b, verdict="idle", recommendation="stop", estimated_monthly_savings_usd=999) await _cost_fact(db_session, integ, scope_a, in_a_idle) await _cost_fact(db_session, integ, scope_a, in_a_ok) await _cost_fact(db_session, integ, scope_b, in_b) await db_session.commit() r = await client.get( f"/api/v1/cmdb/efficiency/scopes/{scope_a.id}/summary", headers=cloud_admin_headers, ) assert r.status_code == 200 totals = r.json()["data"]["totals"] # Only scope A's two servers roll up here — scope B's 999 savings must # NOT leak in. assert totals["servers"] == 2 assert totals["with_recommendation"] == 1 assert totals["pending"] == 1 assert float(totals["estimated_monthly_savings_usd"]) == 120.0 @pytest.mark.asyncio async def test_scope_instances_actionable_only( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): integ = await _integration(db_session) scope = await _scope(db_session, integ) idle = await _server(db_session, integ, name="idle") ok = await _server(db_session, integ, name="ok") await _efficiency(db_session, integ, idle, verdict="idle", recommendation="stop", estimated_monthly_savings_usd=50) await _efficiency(db_session, integ, ok, verdict="healthy") await _cost_fact(db_session, integ, scope, idle) await _cost_fact(db_session, integ, scope, ok) await db_session.commit() r = await client.get( f"/api/v1/cmdb/efficiency/scopes/{scope.id}/instances?actionable_only=true", headers=cloud_admin_headers, ) assert r.status_code == 200 items = r.json()["data"] assert len(items) == 1 assert items[0]["verdict"] == "idle" assert items[0]["is_pending"] is True @pytest.mark.asyncio async def test_scope_summary_bad_id_404( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): r = await client.get( f"/api/v1/cmdb/efficiency/scopes/{uuid.uuid4()}/summary", headers=cloud_admin_headers, ) assert r.status_code == 404 # ── triage ─────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_triage_keep_requires_reason( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): integ = await _integration(db_session) s = await _server(db_session, integ) await _efficiency(db_session, integ, s, verdict="idle", recommendation="stop") await db_session.commit() r = await client.post( f"/api/v1/cmdb/efficiency/instances/{s.id}/triage", json={"action": "keep"}, headers=cloud_admin_headers, ) assert r.status_code == 422 @pytest.mark.asyncio async def test_triage_keep_then_clears_pending( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): integ = await _integration(db_session) scope = await _scope(db_session, integ) s = await _server(db_session, integ) await _efficiency(db_session, integ, s, verdict="idle", recommendation="stop", estimated_monthly_savings_usd=40) await _cost_fact(db_session, integ, scope, s) await db_session.commit() r = await client.post( f"/api/v1/cmdb/efficiency/instances/{s.id}/triage", json={"action": "keep", "reason": "DR standby"}, headers=cloud_admin_headers, ) assert r.status_code == 200 body = r.json()["data"] assert body["triage_status"] == "kept" assert body["is_pending"] is False # The row persisted + the scope pending count dropped to zero. row = ( await db_session.execute( select(CmdbServerEfficiencyTriage).where( CmdbServerEfficiencyTriage.server_id == str(s.id), ) ) ).scalar_one() assert row.status == "kept" and row.reason == "DR standby" r2 = await client.get( f"/api/v1/cmdb/efficiency/scopes/{scope.id}/summary", headers=cloud_admin_headers, ) assert r2.json()["data"]["totals"]["pending"] == 0 @pytest.mark.asyncio async def test_triage_goes_stale_when_verdict_drifts( client: AsyncClient, db_session: AsyncSession, cloud_admin_headers, ): integ = await _integration(db_session) s = await _server(db_session, integ) eff = await _efficiency(db_session, integ, s, verdict="idle", recommendation="stop", estimated_monthly_savings_usd=40) await db_session.commit() r = await client.post( f"/api/v1/cmdb/efficiency/instances/{s.id}/triage", json={"action": "acknowledge"}, headers=cloud_admin_headers, ) assert r.status_code == 200 assert r.json()["data"]["triage_status"] == "acknowledged" # Daily recompute drifts the verdict busy → the acknowledge no longer # applies; the read side must collapse it back to open. eff.verdict = "busy" eff.recommendation = "keep" await db_session.commit() r2 = await client.get( f"/api/v1/cmdb/efficiency/instances/{s.id}", headers=cloud_admin_headers, ) assert r2.status_code == 200 assert r2.json()["data"]["triage_status"] == "open" @pytest.mark.asyncio async def test_triage_non_owner_404( client: AsyncClient, db_session: AsyncSession, employee_user, employee_headers, ): integ = await _integration(db_session) other = await _server(db_session, integ, owner_person_id=None) await _efficiency(db_session, integ, other, verdict="idle", recommendation="stop") await db_session.commit() r = await client.post( f"/api/v1/cmdb/efficiency/instances/{other.id}/triage", json={"action": "acknowledge"}, headers=employee_headers, ) assert r.status_code == 404 @pytest.mark.asyncio async def test_triage_readonly_auditor_cannot_triage_foreign_server( client: AsyncClient, db_session: AsyncSession, auditor_headers, ): """Round-3 P1 fix: read-only admin tiers (auditor / finance / management) are org-wide *readers* of cloud efficiency, but must NOT be able to WRITE a triage on a server they don't own. Before the fix, ``_owner_scope`` returned org-wide (None) for these roles on the triage write path, skipping the ownership check → an auditor could acknowledge/ snooze/keep any server, mutating operational state. The write path now uses ``_owner_scope_for_write`` (org-wide only for can_manage_cloud), so a foreign server 404s for a read-only role.""" integ = await _integration(db_session) other = await _server(db_session, integ, owner_person_id=None) await _efficiency(db_session, integ, other, verdict="idle", recommendation="stop") await db_session.commit() r = await client.post( f"/api/v1/cmdb/efficiency/instances/{other.id}/triage", json={"action": "acknowledge"}, headers=auditor_headers, ) assert r.status_code == 404, r.text