"""handle 标签体系。 这个文件钉住的核心是**标签挂在 handle 上、不挂在成员资格上**带来的那个 性质:标一次,跨该 handle 所在的所有组织一致生效,不会漂移。 585 把分类挂在成员资格上,而实测 `sre-bot` / `ti-chi-bot` / `crazycs520` 各在 5 个组织里 —— 那意味着标一次要写 5 行,有人只改了 2 行,剩下 3 行 还是空的,而界面上看不出来。这里的第一组用例就是那个修正的证据。 """ from __future__ import annotations import uuid import pytest from app.domains.github_org import constants as ghc from app.domains.github_org import labels as lb 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 _seed_vocab(db): """迁移播种的那几个;测试库由 metadata 建表,所以要自己种。""" for key, cat, zh, en in ( ("account:bot", "account", "机器账号", "Machine account"), ("account:outside_human", "account", "外部人员", "External person"), ("employer:bytedance", "employer", "ByteDance", "ByteDance"), ("community:maintainer", "community", "社区 maintainer", "Maintainer"), ("employer:other", "employer", "其他公司", "Other company"), ): await lb.upsert_label( db, key=key, category=cat, display_zh=zh, display_en=en, ) await db.commit() async def _org(db, slug): o = await gh.create_org(db, slug=slug, display_name=slug) await db.commit() return o async def _m(db, org, login, *, person=None, profile=None): m = await gh.upsert_membership( db, org=org, github_login=login, person_id=str(person.id) if person else None, state=ghc.MEMBERSHIP_STATE_ACTIVE, source="reconcile", actor_kind="integration", trace_id="t", ) if profile is not None: m.github_profile = profile await db.flush() return m # ── 标签挂 handle:一次生效跨所有组织 ────────────────────────── async def test_one_label_covers_every_org_the_handle_is_in(db_session): """🔴 这就是把标签从成员资格移到 handle 换来的东西。 `sre-bot` 在 5 个组织里。打一次 `account:bot`,五条成员资格全部被确认 为外部 —— 而不是让人点五次、或者只点了两次留下三条不一致。 """ await _seed_vocab(db_session) orgs = [await _org(db_session, s) for s in ("pingcap", "tikv", "pingcap-inc", "pingcap-qe", "tidbcloud")] for o in orgs: await _m(db_session, o, "sre-bot") await db_session.commit() out = await lb.attach( db_session, github_login="sre-bot", label_keys=["account:bot"], ) await db_session.commit() assert out["memberships_synced"] == 5 for o in orgs: m = await gh.find_membership( db_session, org_id=o.id, github_login="sre-bot", ) assert m.external_confirmed_at is not None, o.slug assert m.external_kind == ghc.EXTERNAL_KIND_BOT, o.slug async def test_detaching_the_account_label_clears_every_org_too(db_session): """反方向也要一致 —— 否则撤销之后会留下一半确认、一半没确认。""" await _seed_vocab(db_session) a, b = await _org(db_session, "pingcap"), await _org(db_session, "tikv") await _m(db_session, a, "sre-bot") await _m(db_session, b, "sre-bot") await db_session.commit() await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() await lb.detach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() for o in (a, b): m = await gh.find_membership(db_session, org_id=o.id, github_login="sre-bot") assert m.external_confirmed_at is None, o.slug assert m.external_kind is None, o.slug async def test_labels_are_case_insensitive_on_the_handle(db_session): """GitHub handle 大小写不敏感。标 `TestInfra-Bot` 之后,按 `testinfra-bot` 也要查得到 —— 否则同一个账号会被标两次。""" await _seed_vocab(db_session) org = await _org(db_session, "pingcap") await _m(db_session, org, "TestInfra-Bot") await db_session.commit() await lb.attach(db_session, github_login="TestInfra-Bot", label_keys=["account:bot"]) await db_session.commit() got = await lb.labels_for_handles(db_session, ["testinfra-bot"]) assert [x["key"] for x in got["testinfra-bot"]] == ["account:bot"] async def test_attaching_twice_is_idempotent(db_session): await _seed_vocab(db_session) org = await _org(db_session, "pingcap") await _m(db_session, org, "sre-bot") await db_session.commit() first = await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() second = await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() assert first["added"] == ["account:bot"] assert second["added"] == [] # 第二次无事可做 got = await lb.labels_for_handles(db_session, ["sre-bot"]) assert len(got["sre-bot"]) == 1 # 没攒出两行 # ── 多对多:这是枚举做不到的事 ──────────────────────────────── async def test_a_handle_can_carry_labels_from_several_categories(db_session): """🔴 585 的枚举是互斥的,而现实里一个 handle 可以同时是 「ByteDance 的人」和「社区 maintainer」。""" await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "crazycs520") await db_session.commit() await lb.attach( db_session, github_login="crazycs520", label_keys=["employer:bytedance", "community:maintainer"], ) await db_session.commit() keys = {x["key"] for x in (await lb.labels_for_handles(db_session, ["crazycs520"]))["crazycs520"]} assert keys == {"employer:bytedance", "community:maintainer"} async def test_a_non_account_label_does_not_confirm_external(db_session): """只有 `account:*` 派生外部确认。标一个 employer 不等于「已处理」—— 「他是 ByteDance 的」和「这条不再是待办」是两件事,后者要人明确表态。""" await _seed_vocab(db_session) org = await _org(db_session, "tikv") m = await _m(db_session, org, "crazycs520") await db_session.commit() await lb.attach(db_session, github_login="crazycs520", label_keys=["employer:bytedance"]) await db_session.commit() assert m.external_confirmed_at is None # ── 词表是受控的 ───────────────────────────────────────────── async def test_an_unknown_label_key_is_refused(db_session): """🔴 拒绝而不是静默创建。 打错一个 key 会静默造出一个只有一个成员的「标签」,而词表存在的全部 价值就是它受控 —— 否则和自由文本一样烂。 """ await _seed_vocab(db_session) org = await _org(db_session, "pingcap") await _m(db_session, org, "someone") await db_session.commit() with pytest.raises(ValueError, match="unknown label"): await lb.attach(db_session, github_login="someone", label_keys=["employer:bytedanec"]) # 拼错 async def test_a_retired_label_keeps_its_history_visible(db_session): """🔴 停用不是删除。 「这个人当年被标成什么」是访问复核要回看的东西;把标签删掉会让历史 链接变成悬空引用然后从界面上消失。 """ await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "crazycs520") await db_session.commit() await lb.attach(db_session, github_login="crazycs520", label_keys=["employer:bytedance"]) await db_session.commit() assert await lb.deactivate_label(db_session, "employer:bytedance") await db_session.commit() got = (await lb.labels_for_handles(db_session, ["crazycs520"]))["crazycs520"] assert len(got) == 1 assert got[0]["retired"] is True # 标成已停用 assert got[0]["display_zh"] == "ByteDance" # 但显示名还在,不是裸 key async def test_a_retired_label_is_hidden_from_the_picker(db_session): await _seed_vocab(db_session) await lb.deactivate_label(db_session, "employer:bytedance") await db_session.commit() keys = {x.key for x in await lb.list_labels(db_session)} assert "employer:bytedance" not in keys keys_all = {x.key for x in await lb.list_labels(db_session, include_inactive=True)} assert "employer:bytedance" in keys_all # ── 规则建议 ──────────────────────────────────────────────── async def test_rules_can_suggest_any_label_not_just_bot(db_session): """585 的策略只会建议 bot。规则表要能表达「company 含 ByteDance ⇒ 建议 employer:bytedance」。""" await _seed_vocab(db_session) await lb.set_rules(db_session, [ {"match": "handle_glob", "value": "*-bot", "label": "account:bot"}, {"match": "profile_company_contains", "value": "bytedance", "label": "employer:bytedance"}, ]) org = await _org(db_session, "tikv") await _m(db_session, org, "sre-bot") await _m(db_session, org, "crazycs520", profile={"name": "Cheng Cheng", "company": "ByteDance Inc"}) await db_session.commit() out = await lb.suggest(db_session) by = {i["handle"]: i["suggestions"] for i in out["items"]} assert [s["label"] for s in by["sre-bot"]] == ["account:bot"] assert [s["label"] for s in by["crazycs520"]] == ["employer:bytedance"] # 理由里要带上实际看到的 company —— 人靠理由判断 assert "ByteDance Inc" in by["crazycs520"][0]["reason"] async def test_a_rule_pointing_at_an_unknown_label_is_refused(db_session): """一条永远建议不出东西的规则,看起来和「没有匹配」一模一样。""" await _seed_vocab(db_session) with pytest.raises(ValueError, match="unknown label"): await lb.set_rules(db_session, [ {"match": "handle_glob", "value": "*-bot", "label": "nope:xxx"}, ]) async def test_an_unknown_match_type_is_refused(db_session): await _seed_vocab(db_session) with pytest.raises(ValueError, match="unknown match type"): await lb.set_rules(db_session, [ {"match": "vibes", "value": "x", "label": "account:bot"}, ]) async def test_already_labelled_handles_drop_out_of_suggestions(db_session): """人做过的判断不能被重新捞出来问一遍。""" await _seed_vocab(db_session) await lb.set_rules(db_session, [ {"match": "handle_glob", "value": "*-bot", "label": "account:bot"}, ]) org = await _org(db_session, "pingcap") await _m(db_session, org, "sre-bot") await db_session.commit() assert (await lb.suggest(db_session))["total_candidates"] == 1 await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() assert (await lb.suggest(db_session))["total_candidates"] == 0 async def test_a_human_named_robot_still_gets_suggested(db_session): """🔴 与 585 同一条:规则会误伤真人,所以永不自动落库。 这条断言的不是「它不匹配」,而是「它确实匹配,因此必须有人看一眼」。 """ await _seed_vocab(db_session) await lb.set_rules(db_session, [ {"match": "handle_glob", "value": "*robot*", "label": "account:bot"}, ]) org = await _org(db_session, "pingcap") await _m(db_session, org, "robot-liu") await db_session.commit() out = await lb.suggest(db_session) assert [i["handle"] for i in out["items"]] == ["robot-liu"] async def test_no_rules_means_no_suggestions_not_a_crash(db_session): await _seed_vocab(db_session) org = await _org(db_session, "pingcap") await _m(db_session, org, "sre-bot") await db_session.commit() out = await lb.suggest(db_session) assert out["rules"] == [] assert out["items"] == [] # ── 与已认出的人互斥 ──────────────────────────────────────── async def test_a_membership_linked_to_a_person_is_not_touched(db_session): """🔴 「是某个员工」优先于标签规则。 如果标签能把一条已经认出是谁的成员资格改成「外部」,那这个人离职时 就不会被回收 —— 而那正是这整套东西存在要防的事。 """ await _seed_vocab(db_session) org = await _org(db_session, "pingcap") 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="Robot Liu", employment_status="active", ) db_session.add(p) await db_session.flush() m = await _m(db_session, org, "robot-liu", person=p) await db_session.commit() out = await lb.attach(db_session, github_login="robot-liu", label_keys=["account:bot"]) await db_session.commit() assert out["memberships_synced"] == 0 assert m.external_confirmed_at is None assert m.person_id == str(p.id) async def test_report_still_agrees_after_labelling(db_session): """报表的「已确认外部 / 其中机器账号」必须跟着标签动 —— 两处分叉的 表现是界面上两个数,而谁都说不清该信哪个。""" await _seed_vocab(db_session) a, b = await _org(db_session, "pingcap"), await _org(db_session, "tikv") await _m(db_session, a, "sre-bot") await _m(db_session, b, "sre-bot") await db_session.commit() await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() t = (await report.build_report(db_session))["totals"] assert t["confirmed_external"] == 2 assert t["bots"] == 2 assert t["not_yet_identified"] == 0 assert t["not_yet_identified"] == await gh.count_unlinked(db_session) # ── 台账筛选:标签要能被检索,否则打了也没用 ──────────────────── async def test_filtering_the_ledger_by_label(client, github_admin_headers, db_session): """「把 ByteDance 的人筛出来」—— 这是标签存在的最主要用途。""" await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "crazycs520") await _m(db_session, org, "someone-else") await db_session.commit() await lb.attach(db_session, github_login="crazycs520", label_keys=["employer:bytedance"]) await db_session.commit() r = await client.get( "/api/v1/github/memberships?label=employer:bytedance", headers=github_admin_headers, ) assert r.status_code == 200, r.text body = r.json() assert body["meta"]["total"] == 1 assert body["data"][0]["github_login"] == "crazycs520" assert [x["key"] for x in body["data"][0]["labels"]] == ["employer:bytedance"] async def test_a_label_filter_matches_the_handle_in_every_org( client, github_admin_headers, db_session, ): """标签挂 handle,所以筛一个标签会把它在所有组织里的行都带出来。""" await _seed_vocab(db_session) a, b = await _org(db_session, "pingcap"), await _org(db_session, "tikv") await _m(db_session, a, "sre-bot") await _m(db_session, b, "sre-bot") await db_session.commit() await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() r = await client.get( "/api/v1/github/memberships?label=account:bot", headers=github_admin_headers, ) assert r.json()["meta"]["total"] == 2 async def test_several_labels_are_ORed(client, github_admin_headers, db_session): await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "crazycs520") await _m(db_session, org, "sre-bot") await _m(db_session, org, "nobody") await db_session.commit() await lb.attach(db_session, github_login="crazycs520", label_keys=["employer:bytedance"]) await lb.attach(db_session, github_login="sre-bot", label_keys=["account:bot"]) await db_session.commit() r = await client.get( "/api/v1/github/memberships" "?label=employer:bytedance&label=account:bot", headers=github_admin_headers, ) logins = {x["github_login"] for x in r.json()["data"]} assert logins == {"crazycs520", "sre-bot"} async def test_a_handle_with_two_labels_is_not_duplicated( client, github_admin_headers, db_session, ): """🔴 用 EXISTS 而不是 join 的原因。 join 会让同一条成员资格按标签数重复出现,然后 total 和分页一起错 —— 而那种错法在「一个 handle 只有一个标签」的小数据上看不出来。 """ await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "crazycs520") await db_session.commit() await lb.attach( db_session, github_login="crazycs520", label_keys=["employer:bytedance", "community:maintainer"], ) await db_session.commit() r = await client.get( "/api/v1/github/memberships" "?label=employer:bytedance&label=community:maintainer", headers=github_admin_headers, ) body = r.json() assert body["meta"]["total"] == 1, body["meta"] assert len(body["data"]) == 1 async def test_unlabelled_only_is_the_remaining_work_list( client, github_admin_headers, db_session, ): """「还剩多少要人判断」—— 规则命中的能批量处理,剩下的才是真要看的。""" await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "labelled-one") await _m(db_session, org, "Ehco1996") await _m(db_session, org, "zz-jason") await db_session.commit() await lb.attach(db_session, github_login="labelled-one", label_keys=["employer:other"]) await db_session.commit() r = await client.get( "/api/v1/github/memberships?unlabelled_only=true", headers=github_admin_headers, ) logins = {x["github_login"] for x in r.json()["data"]} assert logins == {"Ehco1996", "zz-jason"} async def test_label_filter_composes_with_the_org_filter( client, github_admin_headers, db_session, ): """两个筛选要能叠加 —— 「tikv 里的 ByteDance 的人」是一次真实的查询。""" await _seed_vocab(db_session) a, b = await _org(db_session, "pingcap"), await _org(db_session, "tikv") await _m(db_session, a, "crazycs520") await _m(db_session, b, "crazycs520") await db_session.commit() await lb.attach(db_session, github_login="crazycs520", label_keys=["employer:bytedance"]) await db_session.commit() r = await client.get( f"/api/v1/github/memberships?label=employer:bytedance&org_id={b.id}", headers=github_admin_headers, ) body = r.json() assert body["meta"]["total"] == 1 assert body["data"][0]["org_slug"] == "tikv" # ── 规则的「启用」开关 ──────────────────────────────────────────── async def test_an_account_label_can_never_be_set_to_auto_apply(db_session): """🔴 ``account:*`` 不能自动落 —— 它是终态。 打上 ``account:bot`` 会把该 handle 所有活成员资格一起确认成「外部」, 记录从治理待办里消失,离职回收再也够不着它。已知反例不是假想: ``robot-liu`` 命中 ``*robot*``,而他是个人。 """ await _seed_vocab(db_session) with pytest.raises(ValueError, match="account:"): await lb.set_rules(db_session, [{ "match": lb.MATCH_HANDLE_GLOB, "value": "*robot*", "label": "account:bot", "enabled": True, }]) # 同一条规则**不启用**是允许的 —— 建议一直都可以。 out = await lb.set_rules(db_session, [{ "match": lb.MATCH_HANDLE_GLOB, "value": "*robot*", "label": "account:bot", }]) assert out[0]["enabled"] is False async def test_rules_without_the_enabled_key_read_as_disabled(db_session): """586 播种的 9 条规则没有 ``enabled``,升级后必须读成**关**。 反过来会让那 9 条启发式规则在这次升级之后立刻开始自动落库,而没有 任何人点过同意。这条默认值的方向只有一个是安全的。 """ await _seed_vocab(db_session) from app.integrations.models import PlatformSetting db_session.add(PlatformSetting( key=lb.RULES_KEY, value=[{ # 老形状:三个键,没有 enabled "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", }], )) await db_session.commit() rules = await lb.get_rules(db_session) assert len(rules) == 1 assert rules[0]["enabled"] is False assert rules[0]["source"] == lb.SOURCE_MANUAL async def test_only_enabled_rules_actually_label_anything(db_session): await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "someone", profile={"login": "someone", "company": "ByteDance"}) await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", "enabled": False, }]) await db_session.commit() out = await lb.apply_enabled_rules(db_session) assert out["labelled"] == 0, "关着的规则不该落任何标签" await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", "enabled": True, }]) await db_session.commit() out = await lb.apply_enabled_rules(db_session) await db_session.commit() assert out["labelled"] == 1 got = await lb.labels_for_handles(db_session, ["someone"]) assert [x["key"] for x in got["someone"]] == ["employer:bytedance"] async def test_auto_apply_never_touches_a_handle_a_human_already_labelled( db_session, ): """人的判断优先于启发式。 「规则又把我改回去了」会让人不再相信这套东西,而不相信的结果是他们 退回去在备注里写自由文本 —— 也就是这整套东西要消灭的那个东西。 """ await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "someone", profile={"login": "someone", "company": "ByteDance"}) await lb.attach( db_session, github_login="someone", label_keys=["employer:other"], ) await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", "enabled": True, }]) await db_session.commit() out = await lb.apply_enabled_rules(db_session) await db_session.commit() assert out["labelled"] == 0 got = await lb.labels_for_handles(db_session, ["someone"]) assert [x["key"] for x in got["someone"]] == ["employer:other"] async def test_auto_apply_skips_handles_already_linked_to_a_person(db_session): """认出是谁的行不进规则的池子 —— 「是员工」压过任何启发式。""" await _seed_vocab(db_session) org = await _org(db_session, "tikv") p = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", display_name="有名有姓", work_email=f"u{uuid.uuid4().hex[:8]}@pingcap.com", employment_status="active", ) db_session.add(p) await db_session.flush() await _m(db_session, org, "employee1", person=p, profile={"login": "employee1", "company": "ByteDance"}) await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", "enabled": True, }]) await db_session.commit() out = await lb.apply_enabled_rules(db_session) assert out["labelled"] == 0 async def test_rule_impact_counts_before_anyone_flips_the_switch(db_session): """「启用」旁边那个数字。没有它,启用就是个盲开关。""" await _seed_vocab(db_session) org = await _org(db_session, "tikv") for i in range(3): await _m(db_session, org, f"bd{i}", profile={"login": f"bd{i}", "company": "ByteDance Inc"}) await _m(db_session, org, "other1", profile={"login": "other1", "company": "ClickHouse"}) await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", "enabled": False, }]) await db_session.commit() impact = await lb.rule_impact(db_session) assert len(impact) == 1 assert impact[0]["would_label"] == 3 assert len(impact[0]["sample"]) == 3 # 关着的规则也要算 —— 这个数字存在的意义正是「点之前先看一眼」。 assert impact[0]["enabled"] is False async def test_a_dry_run_apply_writes_nothing(db_session): await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "someone", profile={"login": "someone", "company": "ByteDance"}) await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", "enabled": True, }]) await db_session.commit() out = await lb.apply_enabled_rules(db_session, dry_run=True) assert out["labelled"] == 1 and out["dry_run"] is True got = await lb.labels_for_handles(db_session, ["someone"]) assert not got.get("someone"), "dry_run 不该写任何东西" class _FakeLLM: """替身要**强制 wire 约束**:断言的是喂给模型的那段文本,不是我们自己 重新推一遍过滤逻辑。 只断言 helper 返回什么,变异「提议改回读未打标签的池」照样全绿 —— 实测过:那个变异下 32 条测试一条不红。真正载荷在于 `propose_rules_with_ai` 有没有用对那个池,而唯一能看见它的地方是 prompt。 """ def __init__(self) -> None: self.seen_user: str | None = None async def chat_json(self, *, system, user, max_tokens=0, temperature=0.0): self.seen_user = user return {"rules": []} async def test_rule_proposal_prompt_covers_labelled_handles_and_drops_junk( db_session, monkeypatch, ): """🔴 提议的输入是**全部**认不出的 handle,而占位文本要剔掉。 2026-09-01 生产实测:首批默认标签打完之后,未打标签的池子里填了 company 的只剩 6 个,而且 6 个全是占位文本(``N/A`` / ``none`` / ``Home`` / ``Garden Leave`` ……),于是模型只能提出一条只命中 1 个 handle 的规则。 规则是给**未来**用的 —— 下一批加入组织的人。把输入圈在「还没打标签」上, 等于让每一次成功的批量打标签把提议功能抽干一次。 """ await _seed_vocab(db_session) org = await _org(db_session, "tikv") # 已经打过标签的:仍然要出现在喂给模型的分布里 await _m(db_session, org, "labelled1", profile={"login": "labelled1", "company": "ClickHouse"}) await lb.attach( db_session, github_login="labelled1", label_keys=["employer:other"], ) # 没打标签、但 company 是占位文本:必须被剔掉 await _m(db_session, org, "junk1", profile={"login": "junk1", "company": "N/A"}) await _m(db_session, org, "junk2", profile={"login": "junk2", "company": "Home"}) await db_session.commit() fake = _FakeLLM() from app.domains.github_org import handle_matching as _hm async def _fake_client(_db): return fake monkeypatch.setattr(_hm, "_llm_client", _fake_client) out = await lb.propose_rules_with_ai(db_session) assert out["reason"] == "nothing_new" assert fake.seen_user is not None # 打过标签的那个的 company 必须在输入里 —— 否则批量打标签会把提议抽干 assert "ClickHouse" in fake.seen_user # 占位文本必须不在 assert "N/A" not in fake.seen_user assert "Home" not in fake.seen_user async def test_rule_proposal_says_no_signal_when_every_company_is_junk( db_session, monkeypatch, ): """全是占位文本时要如实说「没有信号」,而不是拿它去问模型。 拿 ``Home`` / ``N/A`` 去问,换来的是一条只命中一个 handle 的规则 —— 那不是规则,是一个应该直接打在那个 handle 上的标签。 """ await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "j1", profile={"login": "j1", "company": "none"}) await _m(db_session, org, "j2", profile={"login": "j2", "company": "无"}) await db_session.commit() fake = _FakeLLM() from app.domains.github_org import handle_matching as _hm async def _fake_client(_db): return fake monkeypatch.setattr(_hm, "_llm_client", _fake_client) out = await lb.propose_rules_with_ai(db_session) assert out["reason"] == "no_company_signal" assert fake.seen_user is None, "没有信号时不该去打扰模型" async def test_impact_still_counts_only_handles_with_no_label(db_session): """反向锚:命中数不能跟着提议的输入一起放宽。 「会打给 N 个 handle」是自动应用真实会碰到的那批 —— 把已经有人标过的 行算进来,那个数字就成了一个虚高的承诺,而它正是管理员点开关的唯一依据。 """ await _seed_vocab(db_session) org = await _org(db_session, "tikv") await _m(db_session, org, "bd1", profile={"login": "bd1", "company": "ByteDance"}) await _m(db_session, org, "bd2", profile={"login": "bd2", "company": "ByteDance"}) await lb.attach( db_session, github_login="bd1", label_keys=["employer:other"], ) await lb.set_rules(db_session, [{ "match": lb.MATCH_COMPANY_CONTAINS, "value": "bytedance", "label": "employer:bytedance", }]) await db_session.commit() impact = await lb.rule_impact(db_session) assert impact[0]["would_label"] == 1 assert impact[0]["sample"] == ["bd2"]