Spaces:
Paused
Paused
| """Round 8 — Master 母号订阅健康度探针单测。 | |
| 覆盖 spec `prompts/0426/spec/shared/master-subscription-health.md` 7 类返回: | |
| active / subscription_cancelled / network_error / auth_invalid / workspace_missing / | |
| role_not_owner | |
| 不变量验证: | |
| M-I1 函数永不抛异常 | |
| M-I3 healthy ⇔ reason == "active" | |
| M-I7 eligible_for_auto_reactivation 严格 `is True` 比对 | |
| cache TTL + schema_version 行为 | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import time | |
| import pytest | |
| class _FakeChatGPTAPI: | |
| """模拟 ChatGPTTeamAPI._api_fetch — 按路径预设响应,记录调用次数。""" | |
| def __init__(self, responses=None): | |
| self.responses = responses or {} | |
| self.calls = [] | |
| def _api_fetch(self, method, path): | |
| self.calls.append((method, path)) | |
| if path in self.responses: | |
| r = self.responses[path] | |
| if isinstance(r, Exception): | |
| raise r | |
| return r | |
| return {"status": 404, "body": ""} | |
| def _patch_account_id(monkeypatch, tmp_path): | |
| """所有测试默认 admin account_id=test-master,cache 写到 tmp 目录。""" | |
| monkeypatch.setattr( | |
| "autoteam.admin_state.get_chatgpt_account_id", | |
| lambda: "test-master", | |
| ) | |
| # cache 写到 tmp,避免污染真实 accounts/.master_health_cache.json | |
| fake_cache = tmp_path / "master_health_cache.json" | |
| monkeypatch.setattr("autoteam.master_health.CACHE_FILE", fake_cache) | |
| yield | |
| if fake_cache.exists(): | |
| fake_cache.unlink() | |
| def _make_accounts_response(items): | |
| return {"status": 200, "body": json.dumps({"items": items})} | |
| def test_master_subscription_active_when_eligible_false(): | |
| """spec §3.2 — eligible_for_auto_reactivation=False(或不存在)且 role 是 owner → active。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| { | |
| "id": "test-master", | |
| "structure": "workspace", | |
| "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": False, | |
| "plan_type": "team", | |
| } | |
| ]), | |
| }) | |
| healthy, reason, evidence = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is True | |
| assert reason == "active" | |
| assert evidence["account_id"] == "test-master" | |
| assert evidence.get("current_user_role") == "account-owner" | |
| def test_master_subscription_cancelled_when_eligible_true(): | |
| """spec §3.2 — eligible_for_auto_reactivation is True → subscription_cancelled。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| { | |
| "id": "test-master", | |
| "structure": "workspace", | |
| "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": True, | |
| "plan_type": "team", | |
| } | |
| ]), | |
| }) | |
| healthy, reason, evidence = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is False | |
| assert reason == "subscription_cancelled" | |
| def test_master_subscription_eligible_truthy_string_does_not_trigger_cancel(): | |
| """M-I7 — 严格 `is True`,字符串 "true" 等不应触发 cancelled(防误判)。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| { | |
| "id": "test-master", | |
| "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": "true", # 字符串而非 bool | |
| } | |
| ]), | |
| }) | |
| healthy, reason, _ = is_master_subscription_healthy(api, cache_ttl=0) | |
| # "true" 字符串不是 True bool → 走 active 路径(防御性) | |
| assert reason == "active" | |
| assert healthy is True | |
| def test_master_subscription_workspace_missing(): | |
| """spec §3.2 — items[] 中找不到目标 account_id → workspace_missing。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| {"id": "other-account", "current_user_role": "account-owner"} | |
| ]), | |
| }) | |
| healthy, reason, evidence = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is False | |
| assert reason == "workspace_missing" | |
| assert evidence.get("items_count") == 1 | |
| def test_master_subscription_role_not_owner(): | |
| """spec §3.2 — role 不在 owner 白名单 → role_not_owner。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| {"id": "test-master", "current_user_role": "member"} | |
| ]), | |
| }) | |
| healthy, reason, _ = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is False | |
| assert reason == "role_not_owner" | |
| def test_master_subscription_auth_invalid_on_401(): | |
| """spec §3.2 — _api_fetch status=401 → auth_invalid(不是 network_error)。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": {"status": 401, "body": "Unauthorized"}, | |
| }) | |
| healthy, reason, evidence = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is False | |
| assert reason == "auth_invalid" | |
| assert evidence.get("http_status") == 401 | |
| def test_master_subscription_network_error_on_500(): | |
| """spec §3.2 — 5xx → network_error(不反向判 cancel)。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": {"status": 500, "body": "ISE"}, | |
| }) | |
| healthy, reason, _ = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is False | |
| assert reason == "network_error" | |
| def test_master_subscription_no_throw_on_underlying_exception(): | |
| """M-I1 — _api_fetch 抛异常时函数也不抛,降级 network_error。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": RuntimeError("connection refused"), | |
| }) | |
| healthy, reason, evidence = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert healthy is False | |
| assert reason == "network_error" | |
| assert "exception" in (evidence.get("detail") or "") | |
| def test_master_subscription_cache_hit_skips_http(): | |
| """spec §4 — cache 命中时不发 HTTP。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| items = [{ | |
| "id": "test-master", | |
| "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": False, | |
| }] | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response(items), | |
| }) | |
| # 第一次:写 cache | |
| h1, r1, _ = is_master_subscription_healthy(api, cache_ttl=300) | |
| assert h1 is True and r1 == "active" | |
| n_calls_after_first = len(api.calls) | |
| # 第二次:cache 命中,不再调 _api_fetch | |
| h2, r2, ev2 = is_master_subscription_healthy(api, cache_ttl=300) | |
| assert h2 is True and r2 == "active" | |
| assert ev2.get("cache_hit") is True | |
| assert len(api.calls) == n_calls_after_first # 没新调用 | |
| def test_master_subscription_force_refresh_bypasses_cache(): | |
| """force_refresh=True 应跳过 cache 强制重测。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| items = [{ | |
| "id": "test-master", | |
| "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": False, | |
| }] | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response(items), | |
| }) | |
| is_master_subscription_healthy(api, cache_ttl=300) | |
| n_first = len(api.calls) | |
| is_master_subscription_healthy(api, cache_ttl=300, force_refresh=True) | |
| assert len(api.calls) > n_first # force 强制走 HTTP | |
| def test_master_subscription_cache_schema_version_mismatch_invalidates(tmp_path, monkeypatch): | |
| """spec §4.2 — schema_version 不匹配的 cache 整体丢弃。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| fake_cache = tmp_path / "broken_cache.json" | |
| fake_cache.write_text(json.dumps({ | |
| "schema_version": 999, # 错误版本 | |
| "cache": { | |
| "test-master": { | |
| "healthy": True, | |
| "reason": "active", | |
| "probed_at": time.time(), | |
| } | |
| } | |
| }), encoding="utf-8") | |
| monkeypatch.setattr("autoteam.master_health.CACHE_FILE", fake_cache) | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| {"id": "test-master", "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": False} | |
| ]), | |
| }) | |
| is_master_subscription_healthy(api, cache_ttl=300) | |
| # 旧 schema 应被丢弃 → 走 HTTP 重测 | |
| assert len(api.calls) == 1 | |
| def test_master_subscription_evidence_redacts_sensitive_fields(): | |
| """M-I6 — evidence 落盘不应含 token/cookie 等敏感字段。""" | |
| from autoteam.master_health import _redact_raw_item | |
| sensitive = { | |
| "id": "test", | |
| "structure": "workspace", | |
| "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": False, | |
| "plan_type": "team", | |
| "access_token": "DO_NOT_PERSIST", | |
| "session_token": "DO_NOT_PERSIST", | |
| "cookies": {"oai_token": "secret"}, | |
| } | |
| redacted = _redact_raw_item(sensitive) | |
| assert "access_token" not in redacted | |
| assert "session_token" not in redacted | |
| assert "cookies" not in redacted | |
| assert redacted.get("id") == "test" | |
| def test_master_subscription_invariant_m_i3_healthy_iff_active(): | |
| """M-I3 — 不变量验证:healthy=True ⇔ reason=='active'。""" | |
| from autoteam.master_health import is_master_subscription_healthy | |
| # active 必 healthy | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response([ | |
| {"id": "test-master", "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": False} | |
| ]), | |
| }) | |
| h, r, _ = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert h is True and r == "active" | |
| # 各种 not_active reason 必 unhealthy | |
| for cancel_items in [ | |
| [{"id": "test-master", "current_user_role": "account-owner", | |
| "eligible_for_auto_reactivation": True}], | |
| [], # workspace_missing | |
| [{"id": "test-master", "current_user_role": "member"}], # role_not_owner | |
| ]: | |
| api = _FakeChatGPTAPI({ | |
| "/backend-api/accounts": _make_accounts_response(cancel_items), | |
| }) | |
| h, r, _ = is_master_subscription_healthy(api, cache_ttl=0) | |
| assert h is False | |
| assert r != "active" | |