Files
codex-app-server/cloud/tests/test_sub2api.py
T
host 4936db3c47 fix: docker 默认 runner 支持 env + 并发开通竞态复用
翻转实弹暴露的两个 #88 缺口:
- _docker_default_runner 不接受 env,_call_runner 遇 env 即 loud fail,
  导致 sub2api 路径下所有 docker 任务失败(xcmapi 路径不传 env,
  此前未暴露)。默认 runner 现接受 env 并与进程环境合并后传给
  docker CLI 子进程(-e <名> 由此解析,值绝不进 argv)。
- 同一用户两任务并发开通时建号撞 email 已存在:现轮询复用
  胜出者入库的行,等不到再原错抛出。
测试:sub2api 16、docker 沙箱 29;全量 cloud 789(4 跳过)、
host 65、deploy 19 全绿。
2026-10-27 17:59:04 +08:00

332 lines
13 KiB
Python
Raw 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 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.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
def __call__(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":
return 200, {"data": {"access_token": self.admin_token,
"refresh_token": "adm-ref",
"expires_in": 3600}}
if path == "/api/v1/auth/login":
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)
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):
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)
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):
from cloud.sandbox import DockerExecutor
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):
from cloud.sandbox import DockerExecutor
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()