Spaces:
Paused
Paused
| """Round 8 — OAuth Personal Workspace 显式选择单测。 | |
| 覆盖 spec `prompts/0426/spec/shared/oauth-workspace-selection.md` 关键路径: | |
| W-I1 三层兜底次序(decode → primary HTTP → UI fallback) | |
| W-I2 OAUTH_WS_NO_PERSONAL fail-fast 不重试 | |
| W-I4 personal 三条件 OR 识别(structure / plan_type=free / is_personal) | |
| W-I6 evidence 不含 token / cookie 原值 | |
| W-I7 不存在 time.sleep(8) 残留 | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import json | |
| class _FakeContext: | |
| def __init__(self, cookies): | |
| self._cookies = cookies | |
| def cookies(self, _url=None): | |
| return self._cookies | |
| class _FakePage: | |
| """模拟 Playwright Page — 支持 cookies、evaluate、locator、goto。""" | |
| def __init__(self, cookies=None, evaluate_result=None, evaluate_exc=None, | |
| url="https://auth.openai.com/sign-in-with-chatgpt/codex/consent", | |
| locator_visible_count=0, goto_exc=None, | |
| inner_text_value="Personal workspace", | |
| page_title_value="OpenAI"): | |
| self.context = _FakeContext(cookies or []) | |
| self._evaluate_result = evaluate_result | |
| self._evaluate_exc = evaluate_exc | |
| self.url = url | |
| self._locator_visible_count = locator_visible_count | |
| self._goto_exc = goto_exc | |
| self._inner_text = inner_text_value | |
| self._title = page_title_value | |
| self.evaluate_calls = [] | |
| self.goto_calls = [] | |
| def evaluate(self, js, args=None): | |
| self.evaluate_calls.append((js[:50], args)) | |
| if self._evaluate_exc: | |
| raise self._evaluate_exc | |
| return self._evaluate_result | |
| def goto(self, url, **kw): | |
| self.goto_calls.append(url) | |
| if self._goto_exc: | |
| raise self._goto_exc | |
| def inner_text(self, _sel, timeout=None): | |
| return self._inner_text | |
| def content(self): | |
| return self._inner_text | |
| def title(self): | |
| return self._title | |
| def locator(self, _sel): | |
| # 返回一个 mock locator,模拟 personal 按钮可见 + 点击成功 | |
| page = self | |
| class _Loc: | |
| def __init__(self): | |
| self._first_called = False | |
| def first(self): | |
| if self._first_called: | |
| return self | |
| self._first_called = True | |
| return self | |
| def is_visible(self, timeout=None): | |
| # 通过递减计数模拟前 N 个 selector 不可见、第 N+1 个可见 | |
| page._locator_visible_count -= 1 | |
| return page._locator_visible_count <= 0 | |
| def inner_text(self, timeout=None): | |
| return "Personal account" | |
| def click(self, **kw): | |
| pass | |
| return _Loc() | |
| def _make_jwt_session(workspaces): | |
| """构造 oai-oauth-session cookie value(JWT-like 三段格式,首段为 base64url payload)。 | |
| 实施按 spec §2.2.1 规则:含 "." → 取首段 base64url decode,所以将 payload 放首段。 | |
| """ | |
| payload = {"workspaces": workspaces, "user": {"email": "x@y.com"}} | |
| body = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=") | |
| return f"{body}.sig.tail" | |
| def _make_plain_session(workspaces): | |
| """构造非 JWT 的 base64url 整段。""" | |
| payload = {"workspaces": workspaces} | |
| return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=") | |
| def test_decode_oauth_session_cookie_jwt_format(): | |
| """spec §2.2.1 — JWT 三段格式取首段 base64url decode。""" | |
| from autoteam.oauth_workspace import decode_oauth_session_cookie | |
| workspaces = [{"id": "ws-1", "structure": "personal", "plan_type": "free"}] | |
| val = _make_jwt_session(workspaces) | |
| page = _FakePage(cookies=[{"name": "oai-oauth-session", "value": val}]) | |
| result = decode_oauth_session_cookie(page) | |
| assert result is not None | |
| assert result["workspaces"][0]["id"] == "ws-1" | |
| def test_decode_oauth_session_cookie_plain_b64(): | |
| """非 JWT 格式整段 b64url decode 也应成功。""" | |
| from autoteam.oauth_workspace import decode_oauth_session_cookie | |
| workspaces = [{"id": "ws-2", "structure": "personal"}] | |
| val = _make_plain_session(workspaces) | |
| page = _FakePage(cookies=[{"name": "oai-oauth-session", "value": val}]) | |
| result = decode_oauth_session_cookie(page) | |
| assert result is not None | |
| assert result["workspaces"][0]["id"] == "ws-2" | |
| def test_decode_oauth_session_cookie_alt_name(): | |
| """W-I1 alt — 兼容 oai-client-auth-session 这个 alt cookie name。""" | |
| from autoteam.oauth_workspace import decode_oauth_session_cookie | |
| workspaces = [{"id": "ws-3", "structure": "personal"}] | |
| val = _make_plain_session(workspaces) | |
| page = _FakePage(cookies=[{"name": "oai-client-auth-session", "value": val}]) | |
| result = decode_oauth_session_cookie(page) | |
| assert result is not None and result["workspaces"][0]["id"] == "ws-3" | |
| def test_decode_oauth_session_cookie_missing_returns_none(): | |
| """没有目标 cookie 时应返回 None,不抛异常。""" | |
| from autoteam.oauth_workspace import decode_oauth_session_cookie | |
| page = _FakePage(cookies=[{"name": "other-cookie", "value": "x"}]) | |
| assert decode_oauth_session_cookie(page) is None | |
| def test_decode_oauth_session_cookie_corrupt_value_returns_none(): | |
| """value 不可解码时应返回 None,不抛异常。""" | |
| from autoteam.oauth_workspace import decode_oauth_session_cookie | |
| page = _FakePage(cookies=[{"name": "oai-oauth-session", "value": "!@#$%^&*"}]) | |
| assert decode_oauth_session_cookie(page) is None | |
| def test_is_personal_workspace_three_or_conditions(): | |
| """W-I4 — structure / plan_type=free / is_personal 三条件 OR 识别。""" | |
| from autoteam.oauth_workspace import _is_personal_workspace | |
| assert _is_personal_workspace({"structure": "personal"}) | |
| assert _is_personal_workspace({"structure": "personal_v2"}) | |
| assert _is_personal_workspace({"plan_type": "free"}) | |
| assert _is_personal_workspace({"is_personal": True}) | |
| # 三条件都不命中 → False | |
| assert not _is_personal_workspace( | |
| {"structure": "workspace", "plan_type": "team", "is_personal": False} | |
| ) | |
| def test_select_oauth_workspace_success_2xx(): | |
| """spec §2.2.2 — 200 + body 含 continue_url → success+redirect。""" | |
| from autoteam.oauth_workspace import select_oauth_workspace | |
| page = _FakePage(evaluate_result={ | |
| "ok": True, "status": 200, | |
| "body": json.dumps({"continue_url": "https://auth.openai.com/callback?code=X"}), | |
| "location": "", | |
| }) | |
| ok, redirect, evidence = select_oauth_workspace( | |
| page, "ws-1", consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is True | |
| assert "callback" in redirect | |
| assert evidence["http_status"] == 200 | |
| def test_select_oauth_workspace_success_302_with_location(): | |
| """302 redirect + Location 头 → success+location。""" | |
| from autoteam.oauth_workspace import select_oauth_workspace | |
| page = _FakePage(evaluate_result={ | |
| "ok": False, "status": 302, | |
| "body": "", | |
| "location": "https://auth.openai.com/authorize/callback?code=ABC", | |
| }) | |
| ok, redirect, _ = select_oauth_workspace( | |
| page, "ws-1", consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is True | |
| assert "callback" in redirect | |
| def test_select_oauth_workspace_404_endpoint_error(): | |
| """404 → ok=False evidence 含 http_status。""" | |
| from autoteam.oauth_workspace import select_oauth_workspace | |
| page = _FakePage(evaluate_result={ | |
| "ok": False, "status": 404, "body": "Not Found", "location": "", | |
| }) | |
| ok, redirect, evidence = select_oauth_workspace( | |
| page, "ws-1", consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is False | |
| assert redirect is None | |
| assert evidence["http_status"] == 404 | |
| def test_select_oauth_workspace_401_marked_auth_required(): | |
| """401/403 evidence detail 标 auth_or_sentinel_required。""" | |
| from autoteam.oauth_workspace import select_oauth_workspace | |
| page = _FakePage(evaluate_result={ | |
| "ok": False, "status": 401, "body": "", "location": "", | |
| }) | |
| ok, _, evidence = select_oauth_workspace( | |
| page, "ws-1", consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is False | |
| assert evidence.get("detail") == "auth_or_sentinel_required" | |
| def test_select_oauth_workspace_evaluate_exception_returns_false(): | |
| """page.evaluate 抛异常 → ok=False evidence 含 exception 类型,不向上传。""" | |
| from autoteam.oauth_workspace import select_oauth_workspace | |
| page = _FakePage(evaluate_exc=RuntimeError("boom")) | |
| ok, _, evidence = select_oauth_workspace( | |
| page, "ws-1", consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is False | |
| assert "RuntimeError" in (evidence.get("exception") or "") | |
| def test_ensure_personal_no_personal_workspace_fail_fast(): | |
| """W-I2 — workspaces[] 不含 personal → OAUTH_WS_NO_PERSONAL,不走 fallback。""" | |
| from autoteam.oauth_workspace import ( | |
| OAUTH_WS_NO_PERSONAL, | |
| ensure_personal_workspace_selected, | |
| ) | |
| val = _make_plain_session([ | |
| {"id": "ws-team", "structure": "workspace", "plan_type": "team"} | |
| ]) | |
| page = _FakePage(cookies=[{"name": "oai-oauth-session", "value": val}]) | |
| ok, fail_cat, evidence = ensure_personal_workspace_selected( | |
| page, consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is False | |
| assert fail_cat == OAUTH_WS_NO_PERSONAL | |
| # 不应走 fallback(W-I2) | |
| assert "fallback" not in evidence | |
| def test_ensure_personal_primary_path_success(): | |
| """spec §2.2.4 — workspaces[] 含 personal + primary 200 → ok=True。""" | |
| from autoteam.oauth_workspace import ensure_personal_workspace_selected | |
| val = _make_plain_session([ | |
| {"id": "ws-personal", "structure": "personal", "plan_type": "free"} | |
| ]) | |
| page = _FakePage( | |
| cookies=[{"name": "oai-oauth-session", "value": val}], | |
| evaluate_result={ | |
| "ok": True, "status": 200, | |
| "body": json.dumps({"continue_url": "https://x"}), | |
| "location": "", | |
| }, | |
| ) | |
| ok, fail_cat, evidence = ensure_personal_workspace_selected( | |
| page, consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is True | |
| assert fail_cat == "" | |
| assert evidence["primary"]["http_status"] == 200 | |
| def test_ensure_personal_primary_fail_fallback_success(): | |
| """主路径失败 → UI fallback 成功 → ok=True(三层兜底)。""" | |
| from autoteam.oauth_workspace import ensure_personal_workspace_selected | |
| val = _make_plain_session([ | |
| {"id": "ws-personal", "structure": "personal"} | |
| ]) | |
| page = _FakePage( | |
| cookies=[{"name": "oai-oauth-session", "value": val}], | |
| evaluate_result={"ok": False, "status": 404, "body": "", "location": ""}, | |
| url="https://auth.openai.com/workspace", # _is_workspace_selection_page 触发 | |
| inner_text_value="select a workspace", | |
| locator_visible_count=1, # 第 1 个 selector 不可见,第 2 个可见 | |
| ) | |
| ok, fail_cat, evidence = ensure_personal_workspace_selected( | |
| page, consent_url="https://auth.openai.com/consent", | |
| ) | |
| # primary 失败 → fallback 应被调用 | |
| assert evidence.get("primary_failed") is True | |
| assert "fallback" in evidence | |
| assert ok is True | |
| assert fail_cat == "" | |
| def test_ensure_personal_primary_and_fallback_both_fail(): | |
| """主路径 + fallback 都失败 → OAUTH_WS_ENDPOINT_ERROR。""" | |
| from autoteam.oauth_workspace import ( | |
| OAUTH_WS_ENDPOINT_ERROR, | |
| ensure_personal_workspace_selected, | |
| ) | |
| val = _make_plain_session([ | |
| {"id": "ws-personal", "structure": "personal"} | |
| ]) | |
| # primary 404 + fallback URL 不是 workspace 页 | |
| page = _FakePage( | |
| cookies=[{"name": "oai-oauth-session", "value": val}], | |
| evaluate_result={"ok": False, "status": 404, "body": "", "location": ""}, | |
| url="https://example.com", # 非 workspace 页 | |
| inner_text_value="other content", | |
| ) | |
| ok, fail_cat, _ = ensure_personal_workspace_selected( | |
| page, consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is False | |
| assert fail_cat == OAUTH_WS_ENDPOINT_ERROR | |
| def test_ensure_personal_decode_failed_falls_back_to_ui(): | |
| """W-I3 — cookie 解码失败应 fallback 到 UI(不直接 fail)。""" | |
| from autoteam.oauth_workspace import ensure_personal_workspace_selected | |
| page = _FakePage( | |
| cookies=[], # 没有 cookie → decode 返回 None | |
| url="https://example.com", | |
| inner_text_value="other", | |
| ) | |
| # primary 不会执行(没 personal id),只走 fallback,fallback 失败 | |
| ok, fail_cat, evidence = ensure_personal_workspace_selected( | |
| page, consent_url="https://auth.openai.com/consent", | |
| ) | |
| assert ok is False | |
| # primary skip,只看 fallback | |
| assert "fallback" in evidence | |
| def test_redact_workspaces_strips_sensitive_fields(): | |
| """W-I6 — _redact_workspaces 应只保留白名单字段,不暴露 token。""" | |
| from autoteam.oauth_workspace import _redact_workspaces | |
| raw = [{ | |
| "id": "ws-1", | |
| "name": "My Personal", | |
| "structure": "personal", | |
| "role": "owner", | |
| "plan_type": "free", | |
| "access_token": "DO_NOT_PERSIST", | |
| "session_token": "DO_NOT_PERSIST", | |
| }] | |
| out = _redact_workspaces(raw) | |
| assert len(out) == 1 | |
| assert "access_token" not in out[0] | |
| assert "session_token" not in out[0] | |
| assert out[0]["id"] == "ws-1" | |
| def test_no_time_sleep_8_in_manager_personal_branch(): | |
| """W-I7 — manager.py _run_post_register_oauth personal 分支不应残留可执行的 time.sleep(8)。 | |
| 注释里出现 "time.sleep(8)" 字面串(描述 Round 8 删除决策)是允许的, | |
| 所以筛选去注释行后再校验。 | |
| """ | |
| import inspect | |
| import re | |
| from autoteam import manager | |
| src = inspect.getsource(manager._run_post_register_oauth) | |
| # 移除注释行(以 # 开头的整行)再校验,只检查可执行代码 | |
| lines = [] | |
| for line in src.splitlines(): | |
| stripped = line.lstrip() | |
| if stripped.startswith("#"): | |
| continue | |
| lines.append(line) | |
| code_only = "\n".join(lines) | |
| # 不应有可执行的 time.sleep(8) — Round 8 删了这个旧的"等 default unset"魔法等待 | |
| assert not re.search(r"^\s*time\.sleep\(8\)", code_only, re.MULTILINE) | |
| assert "Round 8" in src # 说明已应用 Round 8 改造 | |
| def test_ensure_personal_skip_ui_fallback_on_empty_workspaces(): | |
| """Round 11 三轮 — skip_ui_fallback_on_empty=True 时,空 workspaces[] 不调用 UI fallback。 | |
| 场景:刚踢出 Team 的新号,oai-oauth-session.workspaces=[](server-side 状态), | |
| /workspace UI 显示 "Workspaces not found in client auth session" 错误页。 | |
| 如果调用方传 skip_ui_fallback_on_empty=True,本函数必须直接 fail-fast 不导航, | |
| 避免浏览器停在错误页阻塞外层 consent loop。 | |
| 断言: | |
| - 返回 (False, OAUTH_WS_ENDPOINT_ERROR, evidence) | |
| - evidence.primary.phase == "skipped_empty_workspaces" | |
| - evidence.fallback.phase == "skipped_by_caller_request" | |
| - page.goto_calls 不含 /workspace(没导航) | |
| """ | |
| from autoteam.oauth_workspace import ( | |
| OAUTH_WS_ENDPOINT_ERROR, | |
| ensure_personal_workspace_selected, | |
| ) | |
| # 构造 cookie:可解码但 workspaces=[] | |
| session_payload = {"workspaces": []} | |
| cookie_val = base64.b64encode(json.dumps(session_payload).encode()).decode() | |
| page = _FakePage( | |
| cookies=[{"name": "oai-oauth-session", "value": cookie_val}], | |
| url="https://auth.openai.com/sign-in-with-chatgpt/codex/consent", | |
| ) | |
| ok, fail_cat, evidence = ensure_personal_workspace_selected( | |
| page, | |
| consent_url="https://auth.openai.com/oauth/authorize?client_id=codex", | |
| skip_ui_fallback_on_empty=True, | |
| ) | |
| assert ok is False, "空 workspaces[] + skip_ui_fallback_on_empty=True 必须 fail" | |
| assert fail_cat == OAUTH_WS_ENDPOINT_ERROR | |
| assert evidence["primary"]["phase"] == "skipped_empty_workspaces" | |
| assert evidence["fallback"]["phase"] == "skipped_by_caller_request", ( | |
| "fallback.phase 必须明示是 caller 请求跳过(skipped_by_caller_request)," | |
| "不应是 force_select_personal_via_ui 的实际 phase" | |
| ) | |
| # 关键:不应该导航到 /workspace UI | |
| assert not any("workspace" in url for url in page.goto_calls), ( | |
| f"skip_ui_fallback_on_empty=True 时不应 goto /workspace," | |
| f"但实际 goto_calls={page.goto_calls}" | |
| ) | |
| def test_ensure_personal_default_still_falls_back_on_empty_workspaces(): | |
| """Round 11 三轮 — skip_ui_fallback_on_empty 默认 False,保留旧行为(走 UI fallback)。 | |
| 本测试守住默认参数语义,防止未来误改默认值导致非 codex_auth 调用方行为偏移。 | |
| """ | |
| from autoteam.oauth_workspace import ensure_personal_workspace_selected | |
| session_payload = {"workspaces": []} | |
| cookie_val = base64.b64encode(json.dumps(session_payload).encode()).decode() | |
| page = _FakePage( | |
| cookies=[{"name": "oai-oauth-session", "value": cookie_val}], | |
| url="https://auth.openai.com/sign-in-with-chatgpt/codex/consent", | |
| ) | |
| # 默认参数 — 不传 skip_ui_fallback_on_empty | |
| ok, _fail_cat, evidence = ensure_personal_workspace_selected( | |
| page, | |
| consent_url="https://auth.openai.com/oauth/authorize?client_id=codex", | |
| ) | |
| # 默认应该走 UI fallback(_FakePage 不会真的找到 personal button → 失败) | |
| assert evidence["primary"]["phase"] == "skipped_empty_workspaces" | |
| # fallback 应该被尝试(phase 不是 skipped_by_caller_request) | |
| fb_phase = evidence.get("fallback", {}).get("phase", "") | |
| assert fb_phase != "skipped_by_caller_request", ( | |
| "默认参数不应该跳过 fallback,fallback.phase 应该是实际尝试的状态" | |
| ) | |
| # ok 是 False 因为 _FakePage 没有真正的 Personal button | |
| assert ok is False | |