# ── #2132 the two attribution axes must union, not overlap ────────── def test_canonical_owner_total_deducts_sibling_per_key_spend() -> None: """Structural pin for the mixed-axis double count. One person, two Bedrock keys. Canonical key A is idle this month, so A falls to the Owner-tag path and its headline becomes the WHOLE owner total ≈ B's spend; B is on the per-key axis and reports the same money again. The fleet rollup adds both, and A's budget is judged against B's usage. ``_caveat_bedrock_per_key_tag`` says in words that "Summing the two would double-count" — but only to a human reader; ``analytics.total_mtd`` and ``budget_alerts`` do exactly that sum. Deducting the sibling's per-key total from the Owner-tag headline fixes both consumers at once: A's number becomes A's own spend (budget correct) and canonical + siblings adds back up to the owner total exactly once (rollup correct). """ import inspect from app.domains.apikey import spend src = inspect.getsource(spend) assert "async def _bedrock_sibling_per_key_totals" in src assert src.count("_apply_per_key_deduction(payload, flt)") == 2, ( "the deduction must run on BOTH owner-tag reductions — the " "basic panel/cron path and the detail page — or the two " "surfaces disagree" ) canonical_branch = inspect.getsource(spend._resolve_bucket_filter) assert "deduct_per_key=(" in canonical_branch assert "deduct_per_key_monthly=deduct_monthly" in canonical_branch, ( "the 6-month bar series needs the same deduction as the headline " "and the daily bars, or the detail page shows three answers " "(#2225)" ) def test_deduction_never_produces_a_negative_headline() -> None: """The axes are independent AWS reductions; ingest lag must not push a headline below zero.""" from decimal import Decimal from app.domains.apikey.spend import _apply_per_key_deduction class _Flt: deduct_per_key = (Decimal("999"), {"2026-06-01": 999.0}, ["AK-B"]) # PRODUCTION key name (#2225): every builder in the module emits # ``daily_series``; the original version of this test invented a # ``daily`` key that matched the bug instead of the code, so the # dead per-day loop passed its own mutation check. payload = { "mtd_usd": Decimal("10"), "daily_series": [{"date": "2026-06-01", "usd": 10.0}], } _apply_per_key_deduction(payload, _Flt()) assert payload["mtd_usd"] == Decimal(0) assert payload["daily_series"][0]["usd"] == 0.0 assert payload["per_key_deducted_refs"] == ["AK-B"] # …and the reported deduction is what was actually taken off, not # the full sibling total the floor prevented us from taking. assert payload["per_key_deducted_usd"] == "10" # ── self-review 2026-07-31: the deduction's two ordering invariants ── def test_deduction_runs_after_the_unallocated_post_check() -> None: """Order matters, and the first cut had it backwards. The Bedrock post-check asks "did this AK match NO cost facts?" and, if so, emits the ``OWNER_UNALLOCATED`` caveat — "your Owner cost-allocation tag is not activated in AWS Billing". That question is about the RAW owner-tag total. Deducting first makes ``mtd_usd`` zero whenever the canonical key is idle but a sibling is not — a perfectly ordinary state, and exactly the state ``_bedrock_owner_canonical`` exists to serve. The caveat then tells the reader to go fix an AWS setting that is working. """ import inspect from app.domains.apikey import spend for fn in ( spend.compute_assignment_spend, spend.compute_assignment_spend_detail, ): src = inspect.getsource(fn) deduct_at = src.index("_apply_per_key_deduction(payload, flt)") check_at = src.index("_CAVEAT_BEDROCK_OWNER_UNALLOCATED") assert deduct_at > check_at, ( f"{fn.__name__}: the per-key deduction must run AFTER the " "unallocated post-check, or an idle canonical key with a " "spending sibling gets a false 'tag not activated' caveat" ) def test_breakdowns_divide_the_owner_total_not_the_deducted_headline() -> None: """``by_model`` / ``by_profile`` are built from the OWNER-TAG rows. Their ``percent_of_month`` (and ``by_profile_total_usd``) therefore have to divide the owner-tag total. Handing them the deducted headline makes the shares sum past 100% exactly when a sibling is on the per-key axis — the case this whole fix is about. """ import inspect from app.domains.apikey import spend src = inspect.getsource(spend.compute_assignment_spend_detail) assert 'owner_total_usd = payload["mtd_usd"]' in src assert src.count("month_total_usd=owner_total_usd") == 2, ( "both by_model and by_profile must divide the owner-tag total" ) assert 'float(owner_total_usd) if by_profile else None' in src # Only the owner-tag tail is affected: the Bailian and per-key # branches return EARLY, before any deduction, so their # ``month_total_usd=payload["mtd_usd"]`` is correct and must stay. # The rule is "nothing after the deduction divides the headline". tail = src[src.index("_apply_per_key_deduction(payload, flt)"):] assert 'month_total_usd=payload["mtd_usd"]' not in tail, ( "a breakdown built from owner-tag rows is still dividing the " "post-deduction headline — its shares will sum past 100%" )