Files
wehub-resource-sync f3d80b4628
Auto Release / auto-release (push) Failing after 1s
CI / lint (push) Failing after 0s
CI / screenshot-quality (push) Failing after 3s
CI / pr-policy (push) Has been skipped
CI / test (3.11) (push) Failing after 0s
CI / test (3.12) (push) Failing after 0s
CI / test (3.13) (push) Failing after 3s
CI / coverage (push) Failing after 1s
Legibility / legibility (push) Failing after 3s
chore: import upstream snapshot with attribution
2026-07-13 12:31:48 +08:00

646 lines
22 KiB
Python

from __future__ import annotations
import asyncio
from pathlib import Path
import pytest
from claude_tap.cli import _reverse_proxy_trace_options, _toml_dotted_key_segment, parse_args, run_client
class _DummyProc:
def __init__(self) -> None:
self.pid = 12345
self.returncode: int | None = None
async def wait(self) -> int:
self.returncode = 0
return 0
def terminate(self) -> None:
self.returncode = 0
def kill(self) -> None:
self.returncode = -9
_CODEX_PROXY_BASE_URL = "http://127.0.0.1:43123/v1"
def _builtin_codex_http_args(*tail: str) -> tuple[str, ...]:
provider = "model_providers.claude-tap-openai"
return (
"/tmp/codex",
"-c",
'model_provider="claude-tap-openai"',
"-c",
f'{provider}.name="claude-tap"',
"-c",
f'{provider}.base_url="{_CODEX_PROXY_BASE_URL}"',
"-c",
f'{provider}.wire_api="responses"',
"-c",
f"{provider}.requires_openai_auth=true",
"-c",
f"{provider}.supports_websockets=false",
*tail,
)
def _custom_codex_http_args(provider: str, *tail: str) -> tuple[str, ...]:
provider_key = f"model_providers.{provider}"
return (
"/tmp/codex",
"-c",
f'{provider_key}.base_url="{_CODEX_PROXY_BASE_URL}"',
"-c",
f"{provider_key}.supports_websockets=false",
*tail,
)
@pytest.mark.asyncio
async def test_run_client_codex_reverse_forces_builtin_provider_to_http(monkeypatch) -> None:
captured: dict[str, object] = {}
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs["env"]
return _DummyProc()
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("claude_tap.cli_clients._codex_selected_provider_base_url_key", lambda _: None)
code = await run_client(43123, ["exec", "hello"], client="codex", proxy_mode="reverse")
assert code == 0
assert captured["cmd"] == _builtin_codex_http_args("exec", "hello")
assert captured["env"]["OPENAI_BASE_URL"] == "http://127.0.0.1:43123/v1"
@pytest.mark.asyncio
async def test_run_client_codex_reverse_isolates_legacy_openai_base_override(monkeypatch) -> None:
captured: dict[str, object] = {}
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("claude_tap.cli_clients._codex_selected_provider_base_url_key", lambda _: None)
code = await run_client(
43123,
["-c", 'openai_base_url="http://example.invalid/v1"', "exec", "hello"],
client="codex",
proxy_mode="reverse",
)
assert code == 0
assert captured["cmd"] == _builtin_codex_http_args(
"-c",
'openai_base_url="http://example.invalid/v1"',
"exec",
"hello",
)
@pytest.mark.asyncio
async def test_run_client_codex_reverse_replaces_builtin_provider_override(monkeypatch) -> None:
captured: dict[str, object] = {}
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
monkeypatch.setattr("claude_tap.cli_clients._codex_selected_provider_base_url_key", lambda _: None)
code = await run_client(
43123,
["--config", 'model_provider="openai"', "exec", "hello"],
client="codex",
proxy_mode="reverse",
)
assert code == 0
assert captured["cmd"] == _builtin_codex_http_args("exec", "hello")
@pytest.mark.asyncio
async def test_run_client_codex_forward_sets_rust_tls_ca_env(monkeypatch) -> None:
captured: dict[str, object] = {}
ca_path = Path("/tmp/test-ca.pem")
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["env"] = kwargs["env"]
return _DummyProc()
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(43123, ["exec", "hello"], client="codex", proxy_mode="forward", ca_cert_path=ca_path)
assert code == 0
assert captured["env"]["HTTPS_PROXY"] == "http://127.0.0.1:43123"
assert captured["env"]["SSL_CERT_FILE"] == str(ca_path)
assert captured["env"]["CODEX_CA_CERTIFICATE"] == str(ca_path)
def test_toml_dotted_key_segment_quotes_non_ascii_provider_ids() -> None:
assert _toml_dotted_key_segment("newapi") == "newapi"
assert _toml_dotted_key_segment("new-api") == "new-api"
assert _toml_dotted_key_segment("new.api") == '"new.api"'
assert _toml_dotted_key_segment("模型") == '"\\u6a21\\u578b"'
def test_parse_args_codex_auto_detects_chatgpt_target(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "auth.json").write_text('{"auth_mode":"chatgpt"}\n', encoding="utf-8")
monkeypatch.setenv("CODEX_HOME", str(codex_home))
args = parse_args(["--tap-client", "codex"])
assert args.target == "https://chatgpt.com/backend-api/codex"
def test_parse_args_codex_auto_detects_custom_provider_target(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "auth.json").write_text('{"OPENAI_API_KEY":"sk-test"}\n', encoding="utf-8")
(codex_home / "config.toml").write_text(
"\n".join(
[
'model = "gpt-5.4"',
'model_provider = "newapi"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
'name = "Custom"',
"requires_openai_auth = true",
'wire_api = "responses"',
"",
]
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
args = parse_args(["--tap-client", "codex"])
assert args.target == "https://new-api.example.test/v1"
def test_parse_args_codex_custom_provider_precedes_openai_base_url_env(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "auth.json").write_text('{"OPENAI_API_KEY":"sk-test"}\n', encoding="utf-8")
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "newapi"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setenv("OPENAI_BASE_URL", "https://stale-env.example.test/v1")
args = parse_args(["--tap-client", "codex"])
assert args.target == "https://new-api.example.test/v1"
def test_parse_args_codex_auto_detects_custom_provider_from_profile(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "auth.json").write_text('{"OPENAI_API_KEY":"sk-test"}\n', encoding="utf-8")
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "openai"',
"",
"[profiles.staging]",
'model_provider = "newapi"',
"",
"[model_providers.openai]",
'base_url = "https://api.openai.com/v1"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
args = parse_args(["--tap-client", "codex", "--", "--profile", "staging"])
assert args.target == "https://new-api.example.test/v1"
def test_parse_args_codex_auto_detects_custom_provider_from_profile_file(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "auth.json").write_text('{"auth_mode":"chatgpt"}\n', encoding="utf-8")
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "openai"',
"",
"[model_providers.openai]",
'base_url = "https://api.openai.com/v1"',
"",
]
),
encoding="utf-8",
)
(codex_home / "staging.config.toml").write_text(
"\n".join(
[
'model_provider = "newapi"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
args = parse_args(["--tap-client", "codex", "--", "--profile", "staging"])
assert args.target == "https://new-api.example.test/v1"
def test_parse_args_codex_provider_base_url_override_precedes_config(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "newapi"',
"",
"[model_providers.newapi]",
'base_url = "https://production.example.test/v1"',
"",
]
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
args = parse_args(
[
"--tap-client",
"codex",
"--",
"-c",
'model_providers.newapi.base_url="https://staging.example.test/v1"',
]
)
assert args.target == "https://staging.example.test/v1"
def test_parse_args_codex_openai_base_url_override_precedes_default(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
args = parse_args(["--tap-client", "codex", "--", "-c", 'openai_base_url="https://gateway.example.test/v1"'])
assert args.target == "https://gateway.example.test/v1"
def test_parse_args_codex_auto_detects_custom_provider_from_model_provider_override(monkeypatch, tmp_path) -> None:
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "auth.json").write_text('{"OPENAI_API_KEY":"sk-test"}\n', encoding="utf-8")
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "openai"',
"",
"[model_providers.openai]",
'base_url = "https://api.openai.com/v1"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
args = parse_args(["--tap-client", "codex", "--", "-c", 'model_provider="newapi"'])
assert args.target == "https://new-api.example.test/v1"
@pytest.mark.asyncio
async def test_run_client_codex_reverse_injects_selected_provider_base_url(monkeypatch, tmp_path) -> None:
captured: dict[str, object] = {}
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "newapi"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs["env"]
return _DummyProc()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(43123, ["exec", "hello"], client="codex", proxy_mode="reverse")
assert code == 0
assert captured["cmd"] == _custom_codex_http_args("newapi", "exec", "hello")
assert captured["env"]["OPENAI_BASE_URL"] == "http://127.0.0.1:43123/v1"
@pytest.mark.asyncio
async def test_run_client_codex_reverse_injects_profile_provider_base_url(monkeypatch, tmp_path) -> None:
captured: dict[str, object] = {}
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "openai"',
"",
"[profiles.staging]",
'model_provider = "newapi"',
"",
"[model_providers.openai]",
'base_url = "https://api.openai.com/v1"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(43123, ["--profile", "staging", "exec", "hello"], client="codex", proxy_mode="reverse")
assert code == 0
assert captured["cmd"] == _custom_codex_http_args("newapi", "--profile", "staging", "exec", "hello")
@pytest.mark.asyncio
async def test_run_client_codex_reverse_injects_profile_file_provider_base_url(monkeypatch, tmp_path) -> None:
captured: dict[str, object] = {}
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "openai"',
"",
"[model_providers.openai]",
'base_url = "https://api.openai.com/v1"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
(codex_home / "staging.config.toml").write_text('model_provider = "newapi"\n', encoding="utf-8")
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(43123, ["--profile", "staging", "exec", "hello"], client="codex", proxy_mode="reverse")
assert code == 0
assert captured["cmd"] == _custom_codex_http_args("newapi", "--profile", "staging", "exec", "hello")
@pytest.mark.asyncio
async def test_run_client_codex_reverse_injects_provider_from_model_provider_override(monkeypatch, tmp_path) -> None:
captured: dict[str, object] = {}
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "openai"',
"",
"[model_providers.openai]",
'base_url = "https://api.openai.com/v1"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(
43123,
["-c", 'model_provider="newapi"', "exec", "hello"],
client="codex",
proxy_mode="reverse",
)
assert code == 0
assert captured["cmd"] == _custom_codex_http_args(
"newapi",
"-c",
'model_provider="newapi"',
"exec",
"hello",
)
@pytest.mark.asyncio
async def test_run_client_codex_reverse_quotes_non_ascii_provider_base_url_key(monkeypatch, tmp_path) -> None:
captured: dict[str, object] = {}
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "模型"',
"",
'[model_providers."模型"]',
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(43123, ["exec", "hello"], client="codex", proxy_mode="reverse")
assert code == 0
assert captured["cmd"] == _custom_codex_http_args('"\\u6a21\\u578b"', "exec", "hello")
@pytest.mark.asyncio
async def test_run_client_codex_reverse_replaces_conflicting_provider_overrides(monkeypatch, tmp_path) -> None:
captured: dict[str, object] = {}
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
(codex_home / "config.toml").write_text(
"\n".join(
[
'model_provider = "newapi"',
"",
"[model_providers.newapi]",
'base_url = "https://new-api.example.test/v1"',
"",
]
),
encoding="utf-8",
)
async def fake_create_subprocess_exec(*cmd, **kwargs):
captured["cmd"] = cmd
return _DummyProc()
monkeypatch.setenv("CODEX_HOME", str(codex_home))
monkeypatch.setattr("claude_tap.cli.shutil.which", lambda _: "/tmp/codex")
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
code = await run_client(
43123,
[
"-c",
'model_providers.newapi.base_url="http://example.invalid/v1"',
"--config=model_providers.newapi.supports_websockets=true",
"exec",
"hello",
],
client="codex",
proxy_mode="reverse",
)
assert code == 0
assert captured["cmd"] == _custom_codex_http_args("newapi", "exec", "hello")
def test_parse_args_claude_uses_env_base_url(monkeypatch) -> None:
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://gateway.example.test/v1/anthropic")
args = parse_args([])
assert args.target == "https://gateway.example.test/v1/anthropic"
def test_parse_args_claude_uses_project_settings_base_url(monkeypatch, tmp_path) -> None:
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
home = tmp_path / "home"
project = tmp_path / "project"
home_settings = home / ".claude"
project_settings = project / ".claude"
home_settings.mkdir(parents=True)
project_settings.mkdir(parents=True)
(home_settings / "settings.json").write_text(
'{"env":{"ANTHROPIC_BASE_URL":"https://global.example.test/v1/anthropic"}}\n',
encoding="utf-8",
)
(project_settings / "settings.local.json").write_text(
'{"env":{"ANTHROPIC_BASE_URL":"https://project.example.test/v1/anthropic"}}\n',
encoding="utf-8",
)
monkeypatch.setattr("pathlib.Path.home", lambda: home)
monkeypatch.chdir(project)
args = parse_args([])
assert args.target == "https://project.example.test/v1/anthropic"
def test_parse_args_claude_falls_back_to_default_target(monkeypatch, tmp_path) -> None:
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path / "home")
monkeypatch.chdir(tmp_path)
args = parse_args([])
assert args.target == "https://api.anthropic.com"
def test_codex_reverse_trace_options_allow_websocket() -> None:
options = _reverse_proxy_trace_options("codex", "https://chatgpt.com/backend-api/codex")
assert options == {
"strip_path_prefix": "/v1",
"force_http": False,
}