Spaces:
Paused
Paused
File size: 12,226 Bytes
9e11583 d6082ad 9e11583 d6082ad 9e11583 d6082ad 9e11583 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | """覆盖 cmd_check 新增 include_standby 开关 + _probe_standby_quota。
原 task #2:
- include_standby=False(默认) 不探测 standby 池,保持向后兼容
- include_standby=True 调用 _probe_standby_quota,遍历 standby + 限速 + 24h 去重
- 401/403 类 auth_error → STATUS_AUTH_INVALID
task #3 修复回归:
- network_error 不写 last_quota_check_at(允许下一轮重试),不改 status,只 log
- 未知 status 防御分支不写时间戳,避免去重逻辑卡住未来探测
- check_codex_quota 仅 401/403 才返回 auth_error;5xx/429/超时/SSL 错误归 network_error
"""
from __future__ import annotations
from autoteam import manager
from autoteam.accounts import STATUS_ACTIVE, STATUS_AUTH_INVALID, STATUS_STANDBY
def _stub_cmd_check_deps(monkeypatch, accounts_list):
"""把 cmd_check 走通但所有外部副作用短路,仅观察 _probe_standby_quota 是否被调用。
配合 accounts_list 至少包含一个 auth_file 存在的 active 账号,避免 "没有可检查的 active"
提前 return。
"""
monkeypatch.setattr(manager, "load_accounts", lambda: accounts_list)
monkeypatch.setattr(manager, "_reconcile_team_members", lambda *_a, **_kw: {})
monkeypatch.setattr(manager, "_check_and_refresh", lambda acc: ("ok", {"primary_pct": 10, "weekly_pct": 10}))
monkeypatch.setattr(manager, "update_account", lambda *_a, **_kw: None)
monkeypatch.setattr(manager, "sync_to_cpa", lambda: None)
# 屏蔽 personal 分支中 load_accounts 再调(上面已 monkeypatch 生效)
# CLOUDMAIL_DOMAIN 走 config import,无需额外 mock
def _fake_auth_file(tmp_path, email):
f = tmp_path / f"codex-{email}.json"
f.write_text("{}", encoding="utf-8")
return str(f)
def test_check_skips_standby_by_default(tmp_path, monkeypatch):
"""cmd_check() 不传 include_standby → 默认 False → 不应调用 _probe_standby_quota。"""
probe_called = {"n": 0}
monkeypatch.setattr(manager, "_probe_standby_quota", lambda: probe_called.__setitem__("n", probe_called["n"] + 1))
active = {
"email": "a@example.com",
"status": STATUS_ACTIVE,
"auth_file": _fake_auth_file(tmp_path, "a@example.com"),
}
_stub_cmd_check_deps(monkeypatch, [active])
manager.cmd_check() # 默认 include_standby=False
assert probe_called["n"] == 0
def test_check_include_standby_probes_all(tmp_path, monkeypatch):
"""cmd_check(include_standby=True) 必须调用 _probe_standby_quota。"""
probe_called = {"n": 0}
monkeypatch.setattr(manager, "_probe_standby_quota", lambda: probe_called.__setitem__("n", probe_called["n"] + 1))
active = {
"email": "a@example.com",
"status": STATUS_ACTIVE,
"auth_file": _fake_auth_file(tmp_path, "a@example.com"),
}
_stub_cmd_check_deps(monkeypatch, [active])
manager.cmd_check(include_standby=True)
assert probe_called["n"] == 1
def test_check_rate_limited_between_accounts(tmp_path, monkeypatch):
"""_probe_standby_quota 相邻账号必须 sleep STANDBY_PROBE_INTERVAL_SEC,避免群访风控。"""
stby_a = {
"email": "s1@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "s1"),
"last_quota_check_at": None,
}
stby_b = {
"email": "s2@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "s2"),
"last_quota_check_at": None,
}
monkeypatch.setattr(manager, "get_standby_accounts", lambda: [stby_a, stby_b])
monkeypatch.setattr(manager, "_check_and_refresh", lambda acc: ("ok", {"primary_pct": 20, "weekly_pct": 20}))
monkeypatch.setattr(manager, "update_account", lambda *_a, **_kw: None)
sleeps = []
monkeypatch.setattr(manager.time, "sleep", lambda s: sleeps.append(s))
manager._probe_standby_quota()
# 2 账号之间应该 sleep 恰好 1 次(第一个前不 sleep),间隔 = STANDBY_PROBE_INTERVAL_SEC
assert sleeps == [manager.STANDBY_PROBE_INTERVAL_SEC]
def test_check_skips_recently_probed(tmp_path, monkeypatch):
"""last_quota_check_at 在 24h 内的 standby 必须被跳过,不再消耗 wham 配额。"""
now = 1_700_000_000.0
monkeypatch.setattr(manager.time, "time", lambda: now)
recent = {
"email": "recent@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "recent"),
"last_quota_check_at": now - 3600, # 1h 前探测过
}
stale = {
"email": "stale@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "stale"),
"last_quota_check_at": now - (manager.STANDBY_PROBE_DEDUP_SEC + 60), # 超过 24h
}
monkeypatch.setattr(manager, "get_standby_accounts", lambda: [recent, stale])
monkeypatch.setattr(manager.time, "sleep", lambda *_a: None)
probed = []
def fake_check_and_refresh(acc):
probed.append(acc["email"])
return ("ok", {"primary_pct": 50, "weekly_pct": 50})
monkeypatch.setattr(manager, "_check_and_refresh", fake_check_and_refresh)
monkeypatch.setattr(manager, "update_account", lambda *_a, **_kw: None)
manager._probe_standby_quota()
# recent 被 24h 去重跳过,只有 stale 被实际探测
assert probed == ["stale@example.com"]
def test_check_marks_auth_invalid_on_401(tmp_path, monkeypatch):
"""_check_and_refresh 返回 auth_error(401/403/token 刷新失败) → 标 STATUS_AUTH_INVALID。"""
stby = {
"email": "dead@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "dead"),
"last_quota_check_at": None,
}
monkeypatch.setattr(manager, "get_standby_accounts", lambda: [stby])
monkeypatch.setattr(manager, "_check_and_refresh", lambda acc: ("auth_error", None))
monkeypatch.setattr(manager.time, "sleep", lambda *_a: None)
updates = []
monkeypatch.setattr(manager, "update_account", lambda email, **kw: updates.append((email, kw)))
manager._probe_standby_quota()
assert len(updates) == 1
email, fields = updates[0]
assert email == "dead@example.com"
assert fields["status"] == STATUS_AUTH_INVALID
assert "last_quota_check_at" in fields
# ---------------------------------------------------------------------------
# task #3 回归保护:network_error 必须不动 status / 不写时间戳
# ---------------------------------------------------------------------------
def test_probe_network_error_keeps_status_unchanged(tmp_path, monkeypatch):
"""5xx/timeout/SSL 异常 → status_str="network_error" → **不写** last_quota_check_at,
**不改** status。避免一次网络抖动让整批 standby 在 24h 内不再被探测。"""
stby = {
"email": "flaky@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "flaky"),
"last_quota_check_at": None,
}
monkeypatch.setattr(manager, "get_standby_accounts", lambda: [stby])
monkeypatch.setattr(manager, "_check_and_refresh", lambda acc: ("network_error", None))
monkeypatch.setattr(manager.time, "sleep", lambda *_a: None)
updates = []
monkeypatch.setattr(manager, "update_account", lambda email, **kw: updates.append((email, kw)))
manager._probe_standby_quota()
# network_error 分支必须既不调 update_account(不改 status),也不写时间戳
assert updates == [], f"network_error 不应触发 update_account,但收到: {updates}"
def test_probe_unknown_status_does_not_write_timestamp(tmp_path, monkeypatch):
"""未知 status_str → 防御分支不写 last_quota_check_at,避免 24h 去重屏蔽未来真实探测。"""
stby = {
"email": "weird@example.com",
"status": STATUS_STANDBY,
"auth_file": _fake_auth_file(tmp_path, "weird"),
"last_quota_check_at": None,
}
monkeypatch.setattr(manager, "get_standby_accounts", lambda: [stby])
monkeypatch.setattr(manager, "_check_and_refresh", lambda acc: ("future_unknown_status", None))
monkeypatch.setattr(manager.time, "sleep", lambda *_a: None)
updates = []
monkeypatch.setattr(manager, "update_account", lambda email, **kw: updates.append((email, kw)))
manager._probe_standby_quota()
assert updates == [], f"未知 status 不应触发 update_account,但收到: {updates}"
def test_probe_auth_error_only_for_401_403(monkeypatch):
"""check_codex_quota 必须严格区分:
- 401/403 → auth_error
- 5xx / 429 / 4xx其他 → network_error
- timeout / SSL / Connection 异常 → network_error
- 200 但 JSON 解析失败 → network_error
- 200 + 正常 payload → ok
"""
import requests
from autoteam import codex_auth
# 关闭外部依赖:account_id 探测
monkeypatch.setattr(codex_auth, "get_chatgpt_account_id", lambda: None)
class FakeResp:
def __init__(self, status_code, payload=None, raise_json=False, text=""):
self.status_code = status_code
self._payload = payload
self._raise_json = raise_json
self.text = text
def json(self):
if self._raise_json:
raise ValueError("not json")
return self._payload
def make_get(resp_or_exc):
def fake_get(*_a, **_kw):
if isinstance(resp_or_exc, Exception):
raise resp_or_exc
return resp_or_exc
return fake_get
# check_codex_quota 内部用 `import requests` 后调 requests.get,
# 函数内的 import 会查 sys.modules['requests'],因此在 requests 模块上 monkeypatch.get
# 即可拦截调用。
# --- 401 → auth_error
monkeypatch.setattr(requests, "get", make_get(FakeResp(401)))
assert codex_auth.check_codex_quota("tok")[0] == "auth_error"
# --- 403 → auth_error
monkeypatch.setattr(requests, "get", make_get(FakeResp(403)))
assert codex_auth.check_codex_quota("tok")[0] == "auth_error"
# --- 500 → network_error(关键回归:以前会被误判 auth_error)
monkeypatch.setattr(requests, "get", make_get(FakeResp(500, text="upstream down")))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- 502 / 503 / 504 → network_error
for code in (502, 503, 504):
monkeypatch.setattr(requests, "get", make_get(FakeResp(code)))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- 429 → network_error(限流是临时性的)
monkeypatch.setattr(requests, "get", make_get(FakeResp(429)))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- 418 (其他 4xx,非 401/403/429) → network_error(保守归类)
monkeypatch.setattr(requests, "get", make_get(FakeResp(418)))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- ConnectionError → network_error
monkeypatch.setattr(requests, "get", make_get(requests.exceptions.ConnectionError("dns boom")))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- Timeout → network_error
monkeypatch.setattr(requests, "get", make_get(requests.exceptions.Timeout("slow")))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- SSLError → network_error
monkeypatch.setattr(requests, "get", make_get(requests.exceptions.SSLError("ssl fail")))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- 200 但 JSON 解析失败 → network_error(以前会被误判 auth_error)
monkeypatch.setattr(requests, "get", make_get(FakeResp(200, raise_json=True)))
assert codex_auth.check_codex_quota("tok")[0] == "network_error"
# --- 200 + 健康 payload → ok
healthy = {
"rate_limit": {
"primary_window": {"used_percent": 10, "reset_at": 0},
"secondary_window": {"used_percent": 5, "reset_at": 0},
"limit_reached": False,
}
}
monkeypatch.setattr(requests, "get", make_get(FakeResp(200, payload=healthy)))
status, info = codex_auth.check_codex_quota("tok")
assert status == "ok"
assert isinstance(info, dict)
assert info["primary_pct"] == 10
|