"""总览报表 —— 「做成了多少」,以及它必须和治理待办说同一件事。 这个文件最重要的不是某个数字对不对,是**两处口径不能分叉**: 报表里的「还没认出是谁」和治理面的 `count_unlinked` 必须永远相等。 它们分叉的表现是界面上两个 tab 显示两个数,而谁都说不清该信哪个 —— 而这正是本域已经踩过一次的形状(「认不出是谁会自己收敛」那句话在四处 文档里各写各的,直到实测把它证伪)。 """ from __future__ import annotations import uuid import pytest from app.domains.github_org import constants as ghc from app.domains.github_org import report from app.domains.github_org import service as gh from app.identity.person_models import IdentityPerson pytestmark = pytest.mark.asyncio async def _org(db, slug="pingcap", **kw): o = await gh.create_org(db, slug=slug, display_name=slug, **kw) await db.commit() return o async def _person(db, *, status="active"): p = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", work_email=f"u{uuid.uuid4().hex[:8]}@pingcap.com", display_name="Some One", employment_status=status, ) db.add(p) await db.flush() return p async def _m(db, org, login, *, person=None, source="import", state=ghc.MEMBERSHIP_STATE_ACTIVE): row = await gh.upsert_membership( db, org=org, github_login=login, person_id=str(person.id) if person else None, state=state, source=source, actor_kind="integration", trace_id="t", ) await db.flush() return row # ── 口径一致性:报表不能和治理面分叉 ──────────────────────────── async def test_not_yet_identified_matches_the_governance_count(db_session): """🔴 同一个数,两个页面,必须相等。 报表的分母口径若和 ``count_unlinked`` 不同,用户会在「待处理」看到一个 数、在「总览」看到另一个数 —— 然后不再信任两者中的任何一个。 """ org = await _org(db_session) p = await _person(db_session) await _m(db_session, org, "known", person=p) a = await _m(db_session, org, "stranger-a") await _m(db_session, org, "stranger-b") await db_session.commit() # 把其中一条确认成外部人员 —— 它必须同时从两处的缺口里消失 await gh.confirm_external(db_session, a, actor_person_id=None) await db_session.commit() rep = await report.build_report(db_session) assert rep["totals"]["not_yet_identified"] == await gh.count_unlinked(db_session) assert rep["totals"]["confirmed_external"] == await gh.count_confirmed_external(db_session) assert rep["totals"]["not_yet_identified"] == 1 async def test_leavers_still_in_orgs_matches_the_governance_number(db_session): """这个数是这套东西的成败本身,两处必须一致。""" org = await _org(db_session, "pingcap-inc") leaver = await _person(db_session, status="left") await _m(db_session, org, "goneperson", person=leaver) await db_session.commit() rep = await report.build_report(db_session) assert rep["offboarding"]["leavers_still_in_orgs"] == 1 assert rep["offboarding"]["leavers_still_in_orgs"] == ( await gh.count_leavers_still_in_orgs(db_session) ) async def test_exempt_orgs_are_excluded_from_the_leaver_number(db_session): """豁免组织不算 —— 与治理面同一条规则。把它们算进来只会训练人忽略这个数。""" org = await _org( db_session, "tikv", offboarding_policy=ghc.OFFBOARDING_POLICY_EXEMPT, ) leaver = await _person(db_session, status="left") await _m(db_session, org, "maintainer", person=leaver) await db_session.commit() rep = await report.build_report(db_session) assert rep["offboarding"]["leavers_still_in_orgs"] == 0 # ── 来源分布:这一页真正要说的事 ────────────────────────────── async def test_source_split_separates_automation_from_discovery(db_session): """🔴 报表的核心断言。 `import` / `reconcile` 证明的恰恰是「在此之前没有任何流程管过」; 只有 `approval` / `auto_join` 才是这套自动化真的交付了的部分。 把它们混在一个「成员总数」里,这个区别就消失了。 """ org = await _org(db_session) await _m(db_session, org, "a", source=ghc.SOURCE_IMPORT) await _m(db_session, org, "b", source=ghc.SOURCE_IMPORT) await _m(db_session, org, "c", source=ghc.SOURCE_RECONCILE) await _m(db_session, org, "d", source=ghc.SOURCE_APPROVAL) await _m(db_session, org, "e", source=ghc.SOURCE_MANUAL) await db_session.commit() src = (await report.build_report(db_session))["by_source"] assert src["total"] == 5 assert src["discovered"] == 3 # import ×2 + reconcile ×1 assert src["automated"] == 1 # approval ×1 assert src["manual"] == 1 assert src["automated_pct"] == 20.0 async def test_an_empty_ledger_reports_none_not_a_fake_zero_percent(db_session): """没有任何成员时,「自动化覆盖率」是**未知**,不是 0%。 0% 读起来像「跑了但一条都没覆盖」,而真相是「还没有东西可覆盖」。 """ await _org(db_session) rep = await report.build_report(db_session) assert rep["by_source"]["total"] == 0 assert rep["by_source"]["automated_pct"] is None assert rep["totals"]["identified_pct"] is None # ── 趋势:读事件流,不拿当前状态倒推 ────────────────────────── async def test_trend_counts_events_not_current_state(db_session): """🔴 一条「加入后又被移除」的成员资格,在趋势里应该同时出现在两条线上。 拿当前状态倒推历史会把它算成从没发生过 —— 而那恰恰是最该被看见的一种 事件(有人进来过又走了)。 """ org = await _org(db_session) m = await _m(db_session, org, "inandout", state=ghc.MEMBERSHIP_STATE_ACTIVE) await db_session.commit() await gh.mark_removed( db_session, m, reason="offboarding", removal_source=ghc.REMOVAL_SOURCE_OFFBOARDING, actor_person_id=None, actor_kind="integration", trace_id="t", ) await db_session.commit() trend = (await report.build_report(db_session))["trend"] total_removed = sum(p["removed"] for p in trend["points"]) assert total_removed == 1, trend # 这条记录当前是 removed,但它「来过」这件事仍然记在事件流里 assert any(p["joined"] > 0 for p in trend["points"]), trend async def test_trend_window_is_bounded(db_session): """窗口要真的收窄扫描范围,不是查全表再丢掉。""" await _org(db_session) rep = await report.build_report(db_session, days=7) assert rep["trend"]["days"] == 7 # ── 坐席:报表里也不能编数字 ───────────────────────────────── async def test_uncapped_plans_report_no_remaining_in_the_report_too(db_session): """🔴 生产实测:PingCAP-QE 是 free 计划,报 filled=203 / total=92。 按字面算余量是 **-111**。报表和治理面必须用同一个 `seat_remaining`, 否则会出现「待办说没事、报表说欠 111 个坐席」这种谁也解释不了的分叉。 """ org = await _org(db_session, "pingcap-qe") org.seat_plan = "free" org.seat_filled = 203 org.seat_total = 92 await db_session.commit() row = next( o for o in (await report.build_report(db_session))["by_org"] if o["org_slug"] == "pingcap-qe" ) assert row["seat_remaining"] is None assert row["seat_filled"] == 203 # 原始数照实给出,只是不算余量 async def test_capped_plan_reports_a_real_remaining(db_session): org = await _org(db_session, "pingcap-inc") org.seat_plan = "business" org.seat_filled = 260 org.seat_total = 263 await db_session.commit() row = next( o for o in (await report.build_report(db_session))["by_org"] if o["org_slug"] == "pingcap-inc" ) assert row["seat_remaining"] == 3 # ── 每组织构成 ─────────────────────────────────────────────── async def test_per_org_identified_pct_excludes_confirmed_external_from_the_gap( db_session, ): """已确认是外部人员的行是**已处理**,不该继续压低覆盖率。""" org = await _org(db_session) p = await _person(db_session) await _m(db_session, org, "known", person=p) ext = await _m(db_session, org, "outsider") await db_session.commit() await gh.confirm_external(db_session, ext, actor_person_id=None) await db_session.commit() row = (await report.build_report(db_session))["by_org"][0] assert row["live_total"] == 2 assert row["identified"] == 1 assert row["confirmed_external"] == 1 assert row["not_yet_identified"] == 0 async def test_an_org_with_no_members_still_appears(db_session): """空组织必须出现在报表里 —— 「一个人都没有」是结论,不是缺数据。""" await _org(db_session, "emptyorg") rep = await report.build_report(db_session) row = next(o for o in rep["by_org"] if o["org_slug"] == "emptyorg") assert row["live_total"] == 0 assert row["identified_pct"] is None