"""Console-side realized-savings detector (savings_detector). A flagged (idle/oversized) server that is destroyed / stopped / downsized directly in the cloud console — invisible to the Helix audit hook — must still land in the 已优化成本 ledger with ``source='console'``. The detector runs before the daily recompute, comparing freshly-synced server state against yesterday's verdict + spec snapshot. """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from decimal import Decimal import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import ( SAVINGS_METHOD_DESTROY, SAVINGS_METHOD_DOWNSIZE, SAVINGS_METHOD_STOP, SAVINGS_SOURCE_CONSOLE, SAVINGS_SOURCE_HELIX, CmdbCostFact, CmdbEfficiencySavingsEvent, CmdbServer, CmdbServerEfficiency, CmdbServerEfficiencyTriage, ) from app.domains.cmdb.savings_detector import ( detect_console_savings_for_integration, ) from app.integrations.models import Integration async def _integration(db: AsyncSession) -> Integration: integ = Integration( id=uuid.uuid4(), kind="cloud", provider="aws", name="aws-det", 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 _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", "idle"), "recommendation": kw.pop("recommendation", "stop"), "monthly_cost_usd": kw.pop("monthly_cost_usd", Decimal("120")), "computed_at": datetime.now(UTC) - timedelta(days=1), } d.update(kw) e = CmdbServerEfficiency(**d) db.add(e) await db.flush() return e async def _event_for(db, server_id: str) -> CmdbEfficiencySavingsEvent | None: return ( await db.execute( select(CmdbEfficiencySavingsEvent).where( CmdbEfficiencySavingsEvent.server_id == str(server_id), ) ) ).scalar_one_or_none() @pytest.mark.asyncio async def test_detects_console_destroy(db_session: AsyncSession): integ = await _integration(db_session) srv = await _server(db_session, integ) await _efficiency(db_session, integ, srv, verdict="idle") srv.deleted_at = datetime.now(UTC) - timedelta(hours=3) await db_session.flush() res = await detect_console_savings_for_integration( db_session, str(integ.id), ) assert res["detected"] == 1 ev = await _event_for(db_session, str(srv.id)) assert ev is not None assert ev.method == SAVINGS_METHOD_DESTROY assert ev.source == SAVINGS_SOURCE_CONSOLE assert ev.verdict_at_action == "idle" # Baseline prefers the flagged-time cost snapshot on the eff row. assert ev.monthly_cost_baseline_usd == Decimal("120.00") @pytest.mark.asyncio async def test_detects_console_stop_and_downsize(db_session: AsyncSession): integ = await _integration(db_session) stopped = await _server(db_session, integ, status="stopped") await _efficiency(db_session, integ, stopped, verdict="idle") shrunk = await _server( db_session, integ, status="running", instance_type="m5.xlarge", vcpu=4, memory_mb=16384, ) await _efficiency( db_session, integ, shrunk, verdict="oversized", recommendation="downsize", instance_type="m5.2xlarge", vcpu=8, memory_mb=32768, monthly_cost_usd=Decimal("300"), ) await db_session.flush() res = await detect_console_savings_for_integration( db_session, str(integ.id), ) assert res["detected"] == 2 assert res["by_method"] == { SAVINGS_METHOD_STOP: 1, SAVINGS_METHOD_DOWNSIZE: 1, } ev_stop = await _event_for(db_session, str(stopped.id)) assert ev_stop is not None and ev_stop.method == SAVINGS_METHOD_STOP ev_down = await _event_for(db_session, str(shrunk.id)) assert ev_down is not None assert ev_down.method == SAVINGS_METHOD_DOWNSIZE assert ev_down.source == SAVINGS_SOURCE_CONSOLE assert ev_down.evidence["from_vcpu"] == 8 assert ev_down.evidence["to_vcpu"] == 4 assert ev_down.monthly_cost_baseline_usd == Decimal("300.00") @pytest.mark.asyncio async def test_unflagged_and_unchanged_servers_ignored(db_session: AsyncSession): integ = await _integration(db_session) # Healthy box that shrank — not flagged, must not be credited. healthy = await _server(db_session, integ, vcpu=4) await _efficiency( db_session, integ, healthy, verdict="healthy", recommendation="keep", vcpu=8, memory_mb=32768, ) # Flagged box with nothing observed — running, same spec. flagged = await _server(db_session, integ, vcpu=8, memory_mb=32768) await _efficiency( db_session, integ, flagged, verdict="oversized", vcpu=8, memory_mb=32768, ) await db_session.flush() res = await detect_console_savings_for_integration( db_session, str(integ.id), ) assert res["detected"] == 0 assert await _event_for(db_session, str(healthy.id)) is None assert await _event_for(db_session, str(flagged.id)) is None @pytest.mark.asyncio async def test_existing_helix_event_not_double_counted(db_session: AsyncSession): integ = await _integration(db_session) srv = await _server(db_session, integ, status="stopped") await _efficiency(db_session, integ, srv, verdict="idle") db_session.add(CmdbEfficiencySavingsEvent( id=uuid.uuid4(), server_id=str(srv.id), instance_id=srv.instance_id, integration_id=str(integ.id), method=SAVINGS_METHOD_STOP, source=SAVINGS_SOURCE_HELIX, monthly_cost_baseline_usd=Decimal("50"), acted_at=datetime.now(UTC) - timedelta(days=1), )) await db_session.flush() res = await detect_console_savings_for_integration( db_session, str(integ.id), ) assert res["detected"] == 0 ev = await _event_for(db_session, str(srv.id)) assert ev.source == SAVINGS_SOURCE_HELIX # untouched @pytest.mark.asyncio async def test_triage_flag_survives_verdict_drift(db_session: AsyncSession): """Verdict drifted back to healthy after the owner acknowledged the idle finding — the triage's verdict_at_decision keeps it creditable.""" integ = await _integration(db_session) srv = await _server(db_session, integ, status="stopped") await _efficiency(db_session, integ, srv, verdict="healthy", recommendation="keep") db_session.add(CmdbServerEfficiencyTriage( server_id=str(srv.id), status="acknowledged", verdict_at_decision="idle", decided_at=datetime.now(UTC), )) await db_session.flush() res = await detect_console_savings_for_integration( db_session, str(integ.id), ) assert res["detected"] == 1 ev = await _event_for(db_session, str(srv.id)) assert ev.method == SAVINGS_METHOD_STOP assert ev.verdict_at_action == "idle" @pytest.mark.asyncio async def test_savings_outcomes_includes_downsize_and_source_split( db_session: AsyncSession, ): from app.domains.cmdb.savings_service import savings_outcomes integ = await _integration(db_session) srv = await _server(db_session, integ, vcpu=4) now = datetime.now(UTC) db_session.add(CmdbEfficiencySavingsEvent( id=uuid.uuid4(), server_id=str(srv.id), instance_id=srv.instance_id, integration_id=str(integ.id), method=SAVINGS_METHOD_DOWNSIZE, source=SAVINGS_SOURCE_CONSOLE, verdict_at_action="oversized", monthly_cost_baseline_usd=Decimal("300"), acted_at=now - timedelta(days=10), )) # Post-action cost lines: $5/day over the 10 elapsed days → post # monthly = 150 → realized = 300 − 150 = 150. for d in range(1, 11): db_session.add(CmdbCostFact( fact_key=uuid.uuid4().hex, date=(now - timedelta(days=10 - d)).date(), integration_id=str(integ.id), resource_id=srv.instance_id, resource_type="Instance", service="AmazonEC2", amount_usd=Decimal("5"), currency="USD", )) await db_session.flush() data = await savings_outcomes(db_session) methods = {r["key"]: r["monthly_usd"] for r in data["by_method"]} sources = {r["key"]: r["monthly_usd"] for r in data["by_source"]} assert SAVINGS_METHOD_DOWNSIZE in methods assert methods[SAVINGS_METHOD_DOWNSIZE] == pytest.approx(150.0, abs=1.0) assert sources.get(SAVINGS_SOURCE_CONSOLE) == pytest.approx(150.0, abs=1.0) assert data["realized"]["events"] == 1