Files
codex-app-server/cloud/tests/test_sub2api.py
host f39ed3654e ci3: lint/mypy 第二阶段清理 + 覆盖率 76% -> 78%(门槛不动)
基底:#251 头 174b631(堆叠于 #242 feat-ci-expand 之上),
基线 origin/main aebc6238;本轨不改 #251 已定语义、不动
workflow 门槛、不删检查、不加 skip。

lint(ruff E,F@120):109 -> 10
- F401 43 -> 0:删未用导入;try/except 双绑定的名字出现
  “打地鼠”(删 except 分支后 try 分支又被标出),两分支
  均已核对正文无引用后清掉;MCP 三模块的 Error 名不删,
  改走 importlib 属性绑定(见下),两路径绑定都保留。
- E702 40 -> 0:全在 host.py main() 的 argparse 段,纯分行,
  tokenize 按分号 token 拆(含 2377 行多行语句只断首个分号);
  一次 5 空格缩进失误已回滚重做并以 ast + 全量 host 测试验证。
- E501 13 -> 0:host.py 5 项随 E702 分行连带清掉;mcp_server
  3 项把描述拆成相邻字面量(AST 比对 24 条工具描述与原值
  完全一致,注意 ruff 按东亚宽度计长,CJK 算 2);test_host
  5 项断开 git commit 参数表。
- E402 8 -> 10:生产侧 2 项清掉(db.py _re、github_app
  _base64 移到顶部,之前无引用、无重名);剩余 10 项全是
  tests/ 的 sys.path 前置 import(6 项旧 + 4 个新测试文件
  沿用同一既定模式),移上去会破坏单文件直跑,有意保留。
- F841 4 -> 0:只去绑定、不删调用(run_task、DB 入队、
  tarfile 上下文、git 提交),各有注释与原测试/新测试覆盖;
  host dispatch_from_branch 新增真实 git 仓库回归测试。
- F811 1 -> 0:server 本地 _row_get 与 peer_messages.row_get
  逐行相同,删本地定义,全部调用点解析到同一 import;
  新增 test_server_row_get(同一对象 + 三路行为)。

mypy(生产范围):24 -> 0,零新增 type: ignore
- no-redef 9 -> 0:MCP 三模块 try/except 两次 from-import
  改 importlib.import_module + 属性绑定(cloud 优先、flat
  copy 回落、内层 ImportError 同样回落,语义与原来一致;
  flat-copy 子进程测试证实回落后 __module__ 为顶层名);
  server _row_get 随 F811 消除。
- arg-type 10 -> 0:harness2_tasks shared 改 TypedDict
  逐键精确类型;CodexProbeAdapter 补 no-op validate_tree
  (_adapter_validate 本就跳过缺失方法,行为一致)。
- var-annotated 3 -> 0:sub2api _SHARED_ADMIN/_login_slots、
  verify-multiuser RESULTS 加精确注解。
- assignment/misc 2 -> 0:marker_tools ZoneInfo 先声明
  Any 再 try import(草稿先验 mypy 通过)。

bandit:总数 286 不变,High 仍 4 项,全在 host.py,本轨不改
(属第 3 项白名单轨范围),只做 triage:
- 1319 run_verify:无插值,整串验测命令,管道/&& 须 shell
  语义(新增 test_host_run_verify 断言管道与 && 真实生效)。
- 1497 notify:模板来自算子配置,三处插值全部 shlex.quote,
  证据为既有 test_notify_failure_quotes_shell_metacharacters
  (; touch 与 $(touch ) 不执行)。
- 2099/2103 goal verify:无插值,任务配置验测串,同 run_verify。
- 无新增 High;pip-audit 0 漏洞。

覆盖率(同口径三套件 coverage 计量):75.96% -> 77.87%
(22392 行/缺 5385 -> 22437 行/缺 4963,显示 76% -> 78%)
- client.py 0% -> 90%(pipe 假子进程 + main 分支)
- run_parallel.py 0% -> 97%、migrate 包装 0% -> 95%
- playwright_mcp 37% -> 62%、imagegen_mcp 47% -> 79%、
  websearch_mcp 51% -> 86%、deploy.py 56% -> 77%
- verify-multiuser 0% -> 27%(仅 check/req helper;main()
  是主机侧真实多用户实弹验证,按门禁不建生产任务、
  不以 mock 冒充,仍为缺口)
- host.py 55% -> 57%(文件 2020 -> 2060 行系 E702 分行
  的计量口径变化,非代码膨胀)
- 未到 85%:门槛保持 75% 不动,不删低覆盖模块出统计口径;
  缺口主力仍是 server.py / worker.py / host.py 大文件,
  下一阶段须针对性深测,非本轨快赢可及。

验证:三套件 1576 -> 1629 项全过(tests 127、cloud/tests
1449 skipped=4、deploy/tests 53);ruff E9 通过;mypy 0;
bandit High 4(同上);pip-audit 0 漏洞;试跑产物已删,
提交后工作树干净。
2026-11-03 02:48:54 +00:00

530 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""sub2api 网关接入单测:管理 API 客户端 + 幂等开通 + provider 覆盖。
不跑真实模型、不联网:HTTP 经 request 可调用对象注入假实现;
开通走内存 SQLite;host/sandbox 侧只断言命令与 env 构造。
"""
import os
import sqlite3
import sys
import threading
import unittest
from pathlib import Path
from unittest import mock
try:
from cloud import sub2api
from cloud.sub2api import Sub2apiAdmin, ensure_user_gateway_key
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud import sub2api
from cloud.sub2api import Sub2apiAdmin, ensure_user_gateway_key
def _conn():
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
return conn
class FakeApi:
"""按 (method, path) 录调用、按表回包的假 sub2api。"""
def __init__(self):
self.calls = []
self._lock = threading.Lock()
self.admin_token = "adm-tok"
self.user_token = "usr-tok"
self.groups = [{"id": 2, "name": "codex-system"}]
self.next_user_id = 10
self.next_key_id = 20
self.fail_refresh_once = False
self.balance_calls = []
self.fail_balance = False
self.fail_create_user = False
self.create_user_hook = None
self.login_429_times = 0
self.admin_login_429_times = 0
def __call__(self, method, url, body=None, token=None):
with self._lock:
return self._handle(method, url, body, token)
def _handle(self, method, url, body=None, token=None):
path = url.split("28084", 1)[-1] if "28084" in url else url
self.calls.append((method, path, body, token))
if path == "/api/v1/auth/login" and body.get("email") == "adm@x":
if self.admin_login_429_times > 0:
self.admin_login_429_times -= 1
return 429, {"message":
"Too many requests, please try again later"}
return 200, {"data": {"access_token": self.admin_token,
"refresh_token": "adm-ref",
"expires_in": 3600}}
if path == "/api/v1/auth/login":
if self.login_429_times > 0:
self.login_429_times -= 1
return 429, {"message":
"Too many requests, please try again later"}
return 200, {"data": {"access_token": self.user_token,
"refresh_token": "usr-ref",
"expires_in": 3600}}
if path == "/api/v1/auth/refresh":
return 200, {"data": {"access_token": "new-tok",
"refresh_token": "new-ref",
"expires_in": 3600}}
if path.startswith("/api/v1/admin/users/") \
and path.endswith("/balance") and method == "POST":
self.balance_calls.append(body)
if self.fail_balance:
return 400, {"message": "bad balance"}
return 200, {"data": {"balance": body.get("balance")}}
if path == "/api/v1/admin/users" and method == "POST":
self.next_user_id += 1
if self.create_user_hook is not None:
self.create_user_hook(self.next_user_id)
if self.fail_create_user:
return 400, {"message": "email already exists"}
return 200, {"data": {"id": self.next_user_id}}
if path == "/api/v1/admin/groups" and method == "GET":
return 200, {"data": self.groups}
if path == "/api/v1/keys" and method == "POST":
self.next_key_id += 1
return 200, {"data": {"id": self.next_key_id,
"key": "sk-probe-%d" % self.next_key_id}}
if path == "/v1/models" and method == "GET":
return 200, {"data": [{"id": "gpt-5"}, {"id": "claude-x"}]}
return 404, {"message": "not found"}
class TestSub2apiAdmin(unittest.TestCase):
def setUp(self):
self.api = FakeApi()
self.admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw",
request=self.api)
def test_create_user_and_group_lookup(self):
uid = self.admin.create_user("u@x", "pw", "u1")
self.assertEqual(uid, 11)
self.assertEqual(self.admin.resolve_group_id(), 2)
def test_401_refreshes_and_retries(self):
# 第一次管理调用先 401:注入一个先 401 后放行的 request。
calls = {"n": 0}
real = self.api
def flaky(method, url, body=None, token=None):
calls["n"] += 1
if calls["n"] == 2:
return 401, {"message": "expired"}
return real(method, url, body, token)
admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw",
request=flaky)
uid = admin.create_user("u@x", "pw", "u1")
self.assertEqual(uid, 11)
paths = [c[1] for c in self.api.calls]
self.assertIn("/api/v1/auth/refresh", paths)
class TestEnsureGatewayKey(unittest.TestCase):
def setUp(self):
self.api = FakeApi()
self.admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw",
request=self.api)
# 节流名额是进程级共享状态:每个单测从空开始,避免跨测累积。
sub2api._login_slots.clear()
self.env = {"SUB2API_BASE_URL": "http://x:28084",
"SUB2API_GROUP_NAME": "codex-system"}
self.old = {k: os.environ.get(k) for k in self.env}
os.environ.update(self.env)
def tearDown(self):
sub2api._login_slots.clear()
for k, v in self.old.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
def test_provision_then_idempotent(self):
conn = _conn()
key = ensure_user_gateway_key(conn, "user-abc", admin=self.admin,
request=self.api)
self.assertTrue(key.startswith("sk-probe-"))
calls_before = len(self.api.calls)
key2 = ensure_user_gateway_key(conn, "user-abc", admin=self.admin,
request=self.api)
self.assertEqual(key, key2)
# 第二次是纯读库,不再调管理 API。
self.assertEqual(len(self.api.calls), calls_before)
row = conn.execute(
"SELECT sub2api_user_id, refresh_token FROM user_gateway_keys"
" WHERE user_id = ?", ("user-abc",)).fetchone()
self.assertEqual(row[0], 11)
self.assertEqual(row[1], "usr-ref")
def test_list_models(self):
models = sub2api.list_gateway_models("http://x:28084", "sk-1",
request=self.api)
self.assertEqual(models, ["gpt-5", "claude-x"])
def test_provision_grants_opening_balance(self):
conn = _conn()
key = ensure_user_gateway_key(conn, "user-bal", admin=self.admin,
request=self.api)
self.assertTrue(key.startswith("sk-probe-"))
self.assertEqual(len(self.api.balance_calls), 1)
self.assertEqual(self.api.balance_calls[0]["balance"], 10.0)
self.assertEqual(self.api.balance_calls[0]["operation"], "set")
def test_provision_balance_failure_cleans_up(self):
self.api.fail_balance = True
conn = _conn()
with self.assertRaises(sub2api.Sub2apiError):
ensure_user_gateway_key(conn, "user-fail", admin=self.admin,
request=self.api)
paths = [(m, p) for (m, p, _b, _t) in self.api.calls]
self.assertIn(("DELETE", "/api/v1/keys/21"), paths)
self.assertIn(("DELETE", "/api/v1/admin/users/11"), paths)
row = conn.execute(
"SELECT COUNT(*) FROM user_gateway_keys"
" WHERE user_id = ?", ("user-fail",)).fetchone()
self.assertEqual(row[0], 0)
def test_provision_zero_opening_balance_skips_grant(self):
old = os.environ.get("CLOUD_SUB2API_OPENING_BALANCE_USD")
os.environ["CLOUD_SUB2API_OPENING_BALANCE_USD"] = "0"
try:
conn = _conn()
ensure_user_gateway_key(conn, "user-zero", admin=self.admin,
request=self.api)
finally:
if old is None:
os.environ.pop("CLOUD_SUB2API_OPENING_BALANCE_USD", None)
else:
os.environ["CLOUD_SUB2API_OPENING_BALANCE_USD"] = old
self.assertEqual(self.api.balance_calls, [])
def test_provision_race_reuses_winner_row(self):
conn = _conn()
def hook(_new_id):
conn.execute(
"INSERT INTO user_gateway_keys(user_id, sub2api_user_id,"
" api_key, refresh_token, created_at, updated_at)"
" VALUES(?, ?, ?, ?, ?, ?)",
("user-race", 99, "sk-race", "ref-race", 0, 0))
conn.commit()
self.api.create_user_hook = hook
self.api.fail_create_user = True
key = ensure_user_gateway_key(conn, "user-race", admin=self.admin,
request=self.api)
self.assertEqual(key, "sk-race")
def test_provision_race_no_row_reraises(self):
self.api.fail_create_user = True
conn = _conn()
with mock.patch("time.sleep", return_value=None):
with self.assertRaises(sub2api.Sub2apiError):
ensure_user_gateway_key(conn, "user-race2",
admin=self.admin, request=self.api)
row = conn.execute(
"SELECT COUNT(*) FROM user_gateway_keys"
" WHERE user_id = ?", ("user-race2",)).fetchone()
self.assertEqual(row[0], 0)
def _admin_env(self):
old = {k: os.environ.get(k) for k in
("SUB2API_ADMIN_EMAIL", "SUB2API_ADMIN_PASSWORD")}
os.environ["SUB2API_ADMIN_EMAIL"] = "adm@x"
os.environ["SUB2API_ADMIN_PASSWORD"] = "pw"
sub2api._SHARED_ADMIN.clear()
return old
def _restore_admin_env(self, old):
for k, v in old.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
sub2api._SHARED_ADMIN.clear()
def _admin_login_count(self):
return len([
c for c in self.api.calls
if c[0] == "POST" and c[1] == "/api/v1/auth/login"
and (c[2] or {}).get("email") == "adm@x"])
def test_shared_admin_session_reuses_login(self):
old = self._admin_env()
try:
conn1 = _conn()
key1 = ensure_user_gateway_key(conn1, "user-s1",
request=self.api)
self.assertTrue(key1.startswith("sk-probe-"))
conn2 = _conn()
key2 = ensure_user_gateway_key(conn2, "user-s2",
request=self.api)
self.assertTrue(key2.startswith("sk-probe-"))
# 同一进程内第二次开通不再管理员登录(只剩首次那一次)。
self.assertEqual(self._admin_login_count(), 1)
finally:
self._restore_admin_env(old)
def test_shared_admin_session_expired_refetches(self):
old = self._admin_env()
sub2api._SHARED_ADMIN[("http://x:28084", "adm@x")] = (
"old-tok", "old-ref", 0)
try:
conn = _conn()
key = ensure_user_gateway_key(conn, "user-s3",
request=self.api)
self.assertTrue(key.startswith("sk-probe-"))
self.assertEqual(self._admin_login_count(), 1)
finally:
self._restore_admin_env(old)
def test_shared_admin_session_concurrent_burst_logs_in_once(self):
old = self._admin_env()
barrier = threading.Barrier(10)
results = []
errors = []
def run(i):
try:
barrier.wait(timeout=10)
conn = _conn()
key = ensure_user_gateway_key(
conn, "user-c%d" % i, request=self.api)
results.append(key)
except Exception as exc: # noqa: BLE001 - 汇总后断言
errors.append(exc)
try:
threads = [threading.Thread(target=run, args=(i,))
for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=30)
self.assertEqual(errors, [])
self.assertEqual(len(results), 10)
self.assertTrue(all(k.startswith("sk-probe-")
for k in results))
# 突发冷缓存:10 路并发也只登录一次。
self.assertEqual(self._admin_login_count(), 1)
finally:
self._restore_admin_env(old)
def test_shared_admin_session_concurrent_100(self):
# 本测只验并发正确性(各 key 唯一、管理员登录 1 次),不验节流:
# 把预算临时抬高,免得 101 次登录被 15 次/分钟的节流拖到 7 分钟。
orig_budget = sub2api._LOGIN_BUDGET_PER_MINUTE
sub2api._LOGIN_BUDGET_PER_MINUTE = 1000
old = self._admin_env()
barrier = threading.Barrier(100)
results = []
errors = []
def run(i):
try:
barrier.wait(timeout=30)
conn = _conn()
key = ensure_user_gateway_key(
conn, "user-h%d" % i, request=self.api)
results.append(key)
except Exception as exc: # noqa: BLE001 - 汇总后断言
errors.append(exc)
try:
threads = [threading.Thread(target=run, args=(i,))
for i in range(100)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=120)
self.assertEqual(errors, [])
self.assertEqual(len(results), 100)
self.assertTrue(all(k.startswith("sk-probe-")
for k in results))
self.assertEqual(len(set(results)), 100)
# 100 路突发冷缓存也只登录一次。
self.assertEqual(self._admin_login_count(), 1)
finally:
sub2api._LOGIN_BUDGET_PER_MINUTE = orig_budget
self._restore_admin_env(old)
def test_user_login_retries_on_429_then_succeeds(self):
self.api.login_429_times = 2
with mock.patch("time.sleep", return_value=None):
token, refresh = sub2api.user_login(
"http://x:28084", "u@x", "pw", request=self.api)
self.assertEqual(token, "usr-tok")
self.assertEqual(refresh, "usr-ref")
logins = [c for c in self.api.calls
if c[1] == "/api/v1/auth/login"
and (c[2] or {}).get("email") != "adm@x"]
self.assertEqual(len(logins), 3)
def test_user_login_429_exhausted_raises(self):
self.api.login_429_times = 100
with mock.patch("time.sleep", return_value=None):
with self.assertRaises(sub2api.Sub2apiError):
sub2api.user_login("http://x:28084", "u@x", "pw",
request=self.api)
logins = [c for c in self.api.calls
if c[1] == "/api/v1/auth/login"
and (c[2] or {}).get("email") != "adm@x"]
self.assertEqual(len(logins), 6)
def test_admin_login_retries_on_429(self):
self.api.admin_login_429_times = 2
admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw",
request=self.api)
with mock.patch("time.sleep", return_value=None):
uid = admin.create_user("u@x", "pw", "u1")
self.assertEqual(uid, 11)
self.assertEqual(self._admin_login_count(), 3)
def test_login_slot_throttle_waits_for_window(self):
orig_budget = sub2api._LOGIN_BUDGET_PER_MINUTE
orig_window = sub2api._LOGIN_WINDOW_SECONDS
sub2api._LOGIN_BUDGET_PER_MINUTE = 2
sub2api._LOGIN_WINDOW_SECONDS = 60.0
sub2api._login_slots.clear()
now = [1000.0]
sleeps = []
def fake_time():
return now[0]
def fake_sleep(seconds):
sleeps.append(seconds)
now[0] += seconds
try:
with mock.patch("time.time", side_effect=fake_time), \
mock.patch("time.sleep", side_effect=fake_sleep):
sub2api._acquire_login_slot()
sub2api._acquire_login_slot()
sub2api._acquire_login_slot()
self.assertEqual(sleeps, [60.0])
finally:
sub2api._LOGIN_BUDGET_PER_MINUTE = orig_budget
sub2api._LOGIN_WINDOW_SECONDS = orig_window
sub2api._login_slots.clear()
class TestProviderCommand(unittest.TestCase):
def test_server_command_sub2api(self):
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import host
old = os.environ.get("SUB2API_BASE_URL")
os.environ["SUB2API_BASE_URL"] = "http://172.17.0.1:28084"
try:
cmd = host._server_command("/bin/codex", provider="sub2api")
finally:
if old is None:
os.environ.pop("SUB2API_BASE_URL", None)
else:
os.environ["SUB2API_BASE_URL"] = old
self.assertIn("app-server", cmd[-1])
joined = " ".join(cmd)
self.assertIn('model_provider="sub2api"', joined)
self.assertIn("http://172.17.0.1:28084/v1", joined)
self.assertIn('env_key="SUB2API_API_KEY"', joined)
def test_server_command_sub2api_needs_base_url(self):
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import host
old = os.environ.pop("SUB2API_BASE_URL", None)
try:
with self.assertRaises(RuntimeError):
host._server_command("/bin/codex", provider="sub2api")
finally:
if old is not None:
os.environ["SUB2API_BASE_URL"] = old
def test_server_command_unknown_provider(self):
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import host
with self.assertRaises(ValueError):
host._server_command("/bin/codex", provider="nope")
class TestSandboxProviderFlag(unittest.TestCase):
def test_base_command_flag(self):
from cloud.sandbox import LocalExecutor, DockerExecutor
local = LocalExecutor("/tmp/host.py")
cmd = local._base_command(model_provider="sub2api")
self.assertIn("--model-provider", cmd)
self.assertIn("sub2api", cmd)
docker = DockerExecutor("/tmp/host.py")
inner = docker._inner_host_command("dispatch",
model_provider="sub2api")
self.assertIn("--model-provider", inner)
def test_run_local_env_merge_and_loud_fail(self):
from cloud.sandbox import LocalExecutor
seen = {}
def runner(cmd, env=None):
seen["env"] = env
return object()
local = LocalExecutor("/tmp/host.py", runner=runner)
local._run_local(["x"], {"SUB2API_API_KEY": "sk-1"})
self.assertEqual(seen["env"]["SUB2API_API_KEY"], "sk-1")
def cmd_only(cmd):
return object()
local2 = LocalExecutor("/tmp/host.py", runner=cmd_only)
with self.assertRaises(RuntimeError):
local2._run_local(["x"], {"SUB2API_API_KEY": "sk-1"})
class TestDockerSecretEnv(unittest.TestCase):
def _docker(self, runner):
from cloud.sandbox import DockerExecutor
ex = DockerExecutor("/tmp/host.py", runner=runner, env_key=None)
ex._check_docker = lambda: None
return ex
def test_secret_name_only_in_argv(self):
seen = {}
def runner(cmd, timeout=None, env=None):
seen["cmd"] = cmd
seen["env"] = env
return object()
ex = self._docker(runner)
task = {"id": "abc", "prompt": "hi"}
cmd = ex.build_run_command(
task, ["python3", "/app/host.py", "status"],
extra_env={"SUB2API_API_KEY": "sk-secret",
"HTTP_PROXY": "http://x:8888"})
self.assertIn("SUB2API_API_KEY", cmd)
self.assertNotIn("SUB2API_API_KEY=sk-secret", cmd)
self.assertIn("HTTP_PROXY=http://x:8888", cmd)
env = ex._docker_cli_env({"SUB2API_API_KEY": "sk-secret"})
self.assertEqual(env["SUB2API_API_KEY"], "sk-secret")
self.assertIsNone(ex._docker_cli_env({"HTTP_PROXY": "http://x:1"}))
self.assertIsNone(ex._docker_cli_env(None))
def test_call_runner_env_loud_fail(self):
def cmd_only(cmd, timeout=None):
return object()
ex = self._docker(cmd_only)
with self.assertRaises(RuntimeError):
ex._call_runner(["docker"], env={"SUB2API_API_KEY": "sk-1"})
if __name__ == "__main__":
unittest.main()