AutoTeam-F / tests /unit /test_manager_reinvite.py
ZRainbow's picture
fix: harden Playwright and HTTP transport cleanup
a011d67
Raw
History Blame Contribute Delete
5.64 kB
import types
from autoteam import accounts, manager
def test_reinvite_account_uses_unified_oauth_login_and_marks_active(monkeypatch):
"""OAuth + plan=team + 实测配额 ok → STATUS_ACTIVE。
SPEC-2 §3.3 给 reinvite_account 加了 quota probe(check_codex_quota),
必须 mock 它返回 ok 才能走到 active 分支。
"""
updates = []
monkeypatch.setattr(
manager,
"login_codex_via_browser",
lambda email, password, mail_client=None: {
"email": email,
"access_token": "token-1",
"refresh_token": "refresh-1",
"plan_type": "team",
"plan_type_raw": "team",
},
)
monkeypatch.setattr(manager, "save_auth_file", lambda bundle: f"/tmp/{bundle['email']}.json")
monkeypatch.setattr(
manager,
"update_account",
lambda email, **kwargs: updates.append((email, kwargs)),
)
# SPEC-2 quota probe — 返回 ok + primary_pct=0(剩余 100%)
monkeypatch.setattr(
manager,
"check_codex_quota",
lambda token: ("ok", {"primary_pct": 0, "weekly_pct": 0, "primary_total": 1000}),
)
monkeypatch.setattr(manager, "get_chatgpt_account_id", lambda: "wsk-1")
monkeypatch.setattr(manager.time, "time", lambda: 1234567890)
monkeypatch.setattr(
manager,
"_is_email_in_team",
lambda email: (_ for _ in ()).throw(AssertionError("should not check team membership separately")),
)
result = manager.reinvite_account(
types.SimpleNamespace(browser=False),
None,
{"email": "tmp-user@example.com", "password": "secret"},
)
assert result is True
# 第一条 update 是 quota probe 写 last_quota,第二条是 active 终态,
# Round 12 wire-up C1 之后还会追一条 _auth_repair_reset(清 auth_retry_* 字段).
assert len(updates) >= 1
# 找到最后一条带 status 的 update,跳过 _auth_repair_reset 那条(无 status).
status_updates = [u for u in updates if "status" in u[1]]
assert status_updates, f"no status update recorded: {updates}"
final_email, final_kwargs = status_updates[-1]
assert final_email == "tmp-user@example.com"
assert final_kwargs["status"] == accounts.STATUS_ACTIVE
assert final_kwargs["last_active_at"] == 1234567890
assert final_kwargs["auth_file"] == "/tmp/tmp-user@example.com.json"
def test_reinvite_account_stops_http_transport_session_before_oauth(monkeypatch):
"""HTTP-only Team API sessions must be stopped before browser OAuth starts."""
updates = []
class FakeHttpOnlyApi:
browser = None
def __init__(self):
self.stopped = False
def is_started(self):
return True
def stop(self):
self.stopped = True
api = FakeHttpOnlyApi()
monkeypatch.setattr(
manager,
"login_codex_via_browser",
lambda email, password, mail_client=None: {
"email": email,
"access_token": "token-1",
"refresh_token": "refresh-1",
"plan_type": "team",
"plan_type_raw": "team",
},
)
monkeypatch.setattr(manager, "save_auth_file", lambda bundle: f"/tmp/{bundle['email']}.json")
monkeypatch.setattr(manager, "update_account", lambda email, **kwargs: updates.append((email, kwargs)))
monkeypatch.setattr(manager, "check_codex_quota", lambda token: ("ok", {"primary_pct": 0}))
monkeypatch.setattr(manager, "get_chatgpt_account_id", lambda: "wsk-1")
monkeypatch.setattr(manager.time, "time", lambda: 1234567890)
result = manager.reinvite_account(
api,
None,
{"email": "tmp-user@example.com", "password": "secret"},
)
assert result is True
assert api.stopped is True
assert any(kwargs.get("status") == accounts.STATUS_ACTIVE for _email, kwargs in updates)
def test_reinvite_account_marks_auth_invalid_when_oauth_login_returns_non_team(monkeypatch):
"""SPEC-2 §3.3 plan_drift — 白名单内但 plan!=team → STATUS_AUTH_INVALID(不是 STANDBY)。
旧行为 STANDBY 死循环踩同一个号 reinvite,SPEC-2 改为 AUTH_INVALID 让下游清账。
"""
updates = []
monkeypatch.setattr(
manager,
"login_codex_via_browser",
lambda email, password, mail_client=None: {
"email": email,
"access_token": "token-1",
"refresh_token": "refresh-1",
"plan_type": "free",
"plan_type_raw": "free",
},
)
monkeypatch.setattr(
manager,
"update_account",
lambda email, **kwargs: updates.append((email, kwargs)),
)
monkeypatch.setattr(
manager,
"_is_email_in_team",
lambda email: (_ for _ in ()).throw(AssertionError("should not check team membership separately")),
)
result = manager.reinvite_account(
types.SimpleNamespace(browser=False),
None,
{"email": "tmp-user@example.com", "password": ""},
)
assert result is False
# Round 12 wire-up C1 — 失败路径除了 status=AUTH_INVALID, 还会调
# _record_auth_repair_failure → 写 auth_retry_* + final status.
# 关注 status=AUTH_INVALID 落盘即可,其余字段允许.
auth_invalid_updates = [
u for u in updates
if u[1].get("status") == accounts.STATUS_AUTH_INVALID
]
assert auth_invalid_updates, f"expected at least one AUTH_INVALID update, got {updates}"
assert auth_invalid_updates[0] == (
"tmp-user@example.com", {"status": accounts.STATUS_AUTH_INVALID, "auth_file": None}
)