Files
Cognitive_Agent/tests/unit/test_codex_app_server_client.py
T

678 lines
23 KiB
Python

from __future__ import annotations
import json
import sys
import tomllib
import traceback
from pathlib import Path
import pytest
from cognitive_agent.infrastructure.codex.app_server import (
CodexAppServerClient,
CodexAppServerProtocolError,
CodexAppServerUnavailable,
CodexTurnHandle,
)
ROOT = Path(__file__).resolve().parents[2]
FAKE_SERVER = ROOT / "tests" / "fixtures" / "fake_codex_app_server.py"
MCP_SERVER = ROOT / "src/cognitive_agent/infrastructure/codex/vehicle_mcp_server.py"
CALLBACK_URL = "http://127.0.0.1:8765/api/internal/codex/vehicle-control"
CALLBACK_TOKEN = "test-internal-token-123456"
def _client(tmp_path: Path) -> tuple[CodexAppServerClient, Path]:
trace_path = tmp_path / "wire.jsonl"
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
model="gpt-test-fast",
reasoning_effort="low",
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_BASE_URL": "https://api.openai.com/v1"},
mcp_server_path=MCP_SERVER,
)
return client, trace_path
def _events_until_completed(handle: CodexTurnHandle) -> list[dict[str, object]]:
events: list[dict[str, object]] = []
event_queue = handle.events
while True:
event = event_queue.get(timeout=2)
events.append(event)
if event.get("method") == "turn/completed":
return events
def test_real_stdio_adapter_handshake_stream_and_transport_overrides(tmp_path: Path) -> None:
client, trace_path = _client(tmp_path)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is True
assert client.status == "ready"
thread_id = client.start_thread(
developer_instructions="Test instructions",
ephemeral=True,
)
handle = client.start_turn(
thread_id,
"普通问题",
"request-token-123456",
application_context={
"catalog_revision": "rev-test",
"device_groups": [{"id": "roof", "operations": ["roof.set_shade_motion"]}],
},
)
events = _events_until_completed(handle)
terminal = client.finish_turn(handle, timeout=1)
unsubscribe_status = client.unsubscribe_thread(thread_id, timeout=1)
finally:
client.close()
assert client.status == "stopped"
assert unsubscribe_status == "unsubscribed"
assert terminal["status"] == "completed"
assert [
event["params"]["delta"]
for event in events
if event.get("method") == "item/agentMessage/delta"
] == ["协议", "正常"]
wire = [json.loads(line) for line in trace_path.read_text(encoding="utf-8").splitlines()]
methods = [message.get("method") for message in wire]
assert methods[:4] == ["initialize", "initialized", "account/read", "config/read"]
assert "account/login/start" not in methods
thread_request = next(item for item in wire if item.get("method") == "thread/start")
params = thread_request["params"]
assert params["sandbox"] == "read-only"
assert params["approvalPolicy"] == "never"
assert params["model"] == "gpt-test-fast"
assert params["modelProvider"] == "fake-provider"
assert "model_providers" not in params["config"]
turn_request = next(item for item in wire if item.get("method") == "turn/start")
assert turn_request["params"]["sandboxPolicy"] == {
"type": "readOnly",
"networkAccess": False,
}
assert "request-token-123456" in turn_request["params"]["input"][0]["text"]
assert '"catalog_revision":"rev-test"' in turn_request["params"]["input"][0]["text"]
assert "roof.set_shade_motion" in turn_request["params"]["input"][0]["text"]
unsubscribe_request = next(item for item in wire if item.get("method") == "thread/unsubscribe")
assert unsubscribe_request["params"] == {"threadId": thread_id}
def test_interrupt_is_sent_and_waits_for_authoritative_terminal_event(tmp_path: Path) -> None:
client, _trace_path = _client(tmp_path)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
thread_id = client.start_thread(developer_instructions="Test", ephemeral=True)
handle = client.start_turn(
thread_id,
"WAIT_FOR_INTERRUPT",
"request-token-123456",
)
client.interrupt(handle, timeout=1)
events = _events_until_completed(handle)
terminal = client.finish_turn(handle, timeout=1)
finally:
client.close()
assert events[-1]["method"] == "turn/completed"
assert terminal["status"] == "interrupted"
def test_startup_bootstraps_missing_builtin_account_from_api_key_without_leaking_it(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
api_key = "test-model-provider-key-not-a-secret"
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
],
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_API_KEY": api_key},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
status = client.status
stderr = "\n".join(client.stderr_tail)
assert client.available is True
finally:
client.close()
trace_text = (tmp_path / "wire.jsonl").read_text(encoding="utf-8")
wire = [json.loads(line) for line in trace_text.splitlines()]
methods = [message.get("method") for message in wire]
assert methods[:6] == [
"initialize",
"initialized",
"account/read",
"account/login/start",
"account/read",
"config/read",
]
login = next(item for item in wire if item.get("method") == "account/login/start")
assert login["params"] == {"type": "apiKey", "apiKey": "<redacted>"}
assert api_key not in trace_text
assert api_key not in status
assert api_key not in stderr
assert api_key not in " ".join(client._build_command())
def test_startup_fails_fast_when_builtin_account_and_api_key_are_missing(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
with pytest.raises(CodexAppServerUnavailable, match="authentication is not configured"):
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is False
assert client.status == "failed"
client.close()
def test_startup_sanitizes_api_key_login_failure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
api_key = "test-rejected-provider-key-not-a-secret"
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
"--fake-login-failure",
],
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_API_KEY": api_key},
mcp_server_path=MCP_SERVER,
)
with pytest.raises(
CodexAppServerUnavailable,
match="API-key authentication failed",
) as error:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert api_key not in str(error.value)
assert "authentication rejected" not in str(error.value)
formatted_error = "".join(traceback.format_exception(error.value))
assert api_key not in formatted_error
assert "authentication rejected" not in formatted_error
assert api_key not in "\n".join(client.stderr_tail)
assert client.available is False
assert client.status == "failed"
client.close()
def test_startup_allows_provider_that_does_not_require_openai_account(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-unauthenticated",
"--fake-no-openai-auth-required",
],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is True
finally:
client.close()
wire = [
json.loads(line)
for line in (tmp_path / "wire.jsonl").read_text(encoding="utf-8").splitlines()
]
assert "account/login/start" not in [item.get("method") for item in wire]
def test_custom_base_url_uses_complete_thread_provider_and_environment_key_only(
tmp_path: Path,
) -> None:
api_key = "test-proxy-provider-key-not-a-secret"
base_url = "http://127.0.0.1:9876/v1"
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
model="gpt-test-proxy",
request_timeout=2,
startup_timeout=2,
environment={
"OPENAI_API_KEY": api_key,
"OPENAI_BASE_URL": base_url,
},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.start_thread(ephemeral=True)
assert client.available is True
finally:
client.close()
trace_text = (tmp_path / "wire.jsonl").read_text(encoding="utf-8")
wire = [json.loads(line) for line in trace_text.splitlines()]
methods = [item.get("method") for item in wire]
assert "account/read" not in methods
assert "account/login/start" not in methods
request = next(item for item in wire if item.get("method") == "thread/start")
assert request["params"]["modelProvider"] == "n7web_openai_compatible"
assert request["params"]["model"] == "gpt-test-proxy"
provider = request["params"]["config"]["model_providers"]["n7web_openai_compatible"]
assert provider == {
"name": "N7Web OpenAI-compatible",
"base_url": base_url,
"env_key": "OPENAI_API_KEY",
"wire_api": "responses",
"requires_openai_auth": False,
"supports_websockets": False,
"stream_max_retries": 1,
"request_max_retries": 1,
}
assert api_key not in trace_text
assert api_key not in " ".join(client._build_command())
def test_custom_base_url_fails_fast_without_its_environment_key(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
request_timeout=2,
startup_timeout=2,
environment={"OPENAI_BASE_URL": "http://127.0.0.1:9876/v1"},
mcp_server_path=MCP_SERVER,
)
with pytest.raises(
CodexAppServerUnavailable,
match="custom model provider authentication is not configured",
):
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.status == "failed"
assert client.available is False
client.close()
def test_explicit_environment_controls_model_and_proxy_provider(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("CODEX_ASSISTANT_MODEL", "ambient-model-must-not-win")
monkeypatch.setenv("CODEX_ASSISTANT_MODEL_PROVIDER", "ambient-provider-must-not-win")
monkeypatch.setenv("OPENAI_BASE_URL", "https://ambient.example.invalid/v1")
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
request_timeout=2,
startup_timeout=2,
environment={
"codex_assistant_model": "environment-model",
"codex_assistant_model_provider": "environment-provider",
"openai_api_key": "environment-provider-key-not-secret",
"openai_base_url": "http://localhost:9988/v1/",
},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.start_thread(ephemeral=True)
finally:
client.close()
wire = [
json.loads(line)
for line in (tmp_path / "wire.jsonl").read_text(encoding="utf-8").splitlines()
]
request = next(item for item in wire if item.get("method") == "thread/start")
assert request["params"]["model"] == "environment-model"
assert request["params"]["modelProvider"] == "n7web_openai_compatible"
provider = request["params"]["config"]["model_providers"]["n7web_openai_compatible"]
assert provider["base_url"] == "http://localhost:9988/v1"
@pytest.mark.parametrize(
"base_url",
[
"http://proxy.example.test/v1",
"https://user:password@proxy.example.test/v1",
"https://proxy.example.test/v1?api_key=not-allowed",
"file:///tmp/model",
],
)
def test_model_base_url_rejects_insecure_or_credential_bearing_values(
tmp_path: Path,
base_url: str,
) -> None:
with pytest.raises(ValueError, match="OPENAI_BASE_URL"):
CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
environment={
"OPENAI_API_KEY": "test-provider-key-not-secret",
"OPENAI_BASE_URL": base_url,
},
mcp_server_path=MCP_SERVER,
)
def test_process_exit_during_turn_fails_the_handle_immediately(tmp_path: Path) -> None:
client, _trace_path = _client(tmp_path)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
thread_id = client.start_thread(developer_instructions="Test", ephemeral=True)
handle = client.start_turn(
thread_id,
"EXIT_DURING_TURN",
"request-token-123456",
)
events = [handle.events.get(timeout=2) for _ in range(2)]
event = next(item for item in events if item.get("method") == "error")
assert event["method"] == "error"
assert event["params"]["willRetry"] is False
with pytest.raises(CodexAppServerUnavailable, match="exited unexpectedly"):
client.finish_turn(handle, timeout=1)
finally:
client.close()
def test_non_retryable_error_is_terminal_even_without_turn_completed(tmp_path: Path) -> None:
client, _trace_path = _client(tmp_path)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
thread_id = client.start_thread(developer_instructions="Test", ephemeral=True)
handle = client.start_turn(
thread_id,
"FAIL_DURING_TURN",
"request-token-123456",
)
with pytest.raises(CodexAppServerProtocolError, match="without a terminal event"):
client.finish_turn(handle, timeout=1)
finally:
client.close()
def test_malformed_stdout_fails_the_initialize_request(tmp_path: Path) -> None:
source = "import time;print('not-json',flush=True);time.sleep(1)"
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", "-c", source],
request_timeout=1,
startup_timeout=1,
mcp_server_path=MCP_SERVER,
)
with pytest.raises(CodexAppServerProtocolError):
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.close()
def test_command_exposes_only_the_allowlisted_vehicle_mcp(tmp_path: Path) -> None:
client, _trace_path = _client(tmp_path)
rendered = " ".join(client._build_command())
assert "mcp_servers={ n7_vehicle" in rendered
assert 'enabled_tools = ["get_n7web_capabilities", "execute_cockpit_request"]' in rendered
assert "required = true" in rendered
assert 'default_tools_approval_mode = "approve"' in rendered
assert "--strict-config" in rendered
assert "shell_environment_policy.inherit=none" in rendered
assert "features.shell_tool=false" in rendered
assert "features.unified_exec=false" in rendered
assert "features.code_mode=false" in rendered
assert "features.computer_use=false" in rendered
assert "features.apps=false" in rendered
assert "features.plugins=false" in rendered
assert "notify=[]" in rendered
assert "analytics.enabled=false" in rendered
assert 'otel.exporter="none"' in rendered
assert 'web_search="disabled"' in rendered
assert "node_repl" not in rendered
assert "dynamicTools" not in rendered
def test_startup_discovers_and_disables_configured_mcp_servers(tmp_path: Path) -> None:
extra_name = 'side.car"tool'
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
f"--fake-extra-mcp={extra_name}",
],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is True
rendered_command = client._build_command()
finally:
client.close()
quoted_name = json.dumps(extra_name, ensure_ascii=True)
mcp_override = next(
argument for argument in rendered_command if argument.startswith("mcp_servers={")
)
assert f", {quoted_name} = {{" in mcp_override
assert "enabled = false" in mcp_override
assert "raise SystemExit(0)" in mcp_override
parsed_override = tomllib.loads(f"mcp_servers = {mcp_override.removeprefix('mcp_servers=')}")
assert set(parsed_override["mcp_servers"]) == {"n7_vehicle", extra_name}
assert parsed_override["mcp_servers"][extra_name]["enabled"] is False
wire = [
json.loads(line)
for line in (tmp_path / "wire.jsonl").read_text(encoding="utf-8").splitlines()
]
assert [item.get("method") for item in wire].count("initialize") == 2
assert [item.get("method") for item in wire].count("config/read") == 2
def test_effective_mcp_validation_treats_missing_enabled_as_true(tmp_path: Path) -> None:
client, _trace_path = _client(tmp_path)
assert client._configured_mcp_servers(
{
"mcp_servers": {
"n7_vehicle": {"enabled": True},
"implicit-default": {},
}
}
) == {"n7_vehicle": True, "implicit-default": True}
def test_startup_fails_closed_when_effective_mcp_config_cannot_be_verified(
tmp_path: Path,
) -> None:
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[
sys.executable,
"-u",
str(FAKE_SERVER),
"--fake-invalid-mcp-config",
],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
with pytest.raises(CodexAppServerProtocolError, match="no MCP servers"):
client.start(CALLBACK_URL, CALLBACK_TOKEN)
assert client.available is False
assert client.status == "failed"
client.close()
def test_child_environment_excludes_web_auth_and_hardware_secrets(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
blocked_names = {
"DEMO_AUTH_PASSWORD_HASH",
"DEMO_AUTH_SESSION_SECRET",
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN",
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
"COGNITIVE_AGENT_ZLGCAN_DLL",
"FAKE_CODEX_TRACE",
"ALL_PROXY",
"HTTP_PROXY",
"HTTPS_PROXY",
"NO_PROXY",
"all_proxy",
"http_proxy",
"https_proxy",
"no_proxy",
}
for name in blocked_names:
monkeypatch.setenv(name, f"secret-for-{name.lower()}")
monkeypatch.setenv("OPENAI_API_KEY", "model-provider-key")
client = CodexAppServerClient(
cwd=tmp_path,
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
request_timeout=2,
startup_timeout=2,
environment={
"DEMO_AUTH_SESSION_SECRET": "explicit-override-must-still-be-filtered",
"UNRELATED_PRIVATE_VALUE": "also-filtered",
},
mcp_server_path=MCP_SERVER,
)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
finally:
client.close()
child_names = set(json.loads((tmp_path / "environment.json").read_text(encoding="utf-8")))
assert "OPENAI_API_KEY" in child_names
assert "PATH" in {name.upper() for name in child_names}
assert "COGNITIVE_AGENT_CODEX_VEHICLE_CALLBACK_URL" in child_names
assert "COGNITIVE_AGENT_CODEX_VEHICLE_CALLBACK_TOKEN" in child_names
assert blocked_names.isdisjoint(child_names)
assert "UNRELATED_PRIVATE_VALUE" not in child_names
@pytest.mark.parametrize(
"callback_url",
[
"http://127.attacker.example:8765/api/internal/codex/vehicle-control",
"http://192.0.2.1:8765/api/internal/codex/vehicle-control",
],
)
def test_callback_rejects_hosts_that_are_not_actual_loopback_addresses(
tmp_path: Path,
callback_url: str,
) -> None:
client, _trace_path = _client(tmp_path)
with pytest.raises(ValueError, match="loopback"):
client.start(callback_url, CALLBACK_TOKEN)
client.close()
def test_default_workspace_is_temporary_and_removed_on_close(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("CODEX_ASSISTANT_WORKSPACE", raising=False)
monkeypatch.delenv("COGNITIVE_AGENT_CODEX_WORKSPACE", raising=False)
client = CodexAppServerClient(
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
mcp_server_path=MCP_SERVER,
)
workspace = client._cwd
assert workspace.is_dir()
assert workspace != ROOT
assert workspace.name.startswith("n7web-codex-assistant-")
client.close()
assert not workspace.exists()
def test_client_can_restart_with_a_fresh_isolated_workspace(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("CODEX_ASSISTANT_WORKSPACE", raising=False)
monkeypatch.delenv("COGNITIVE_AGENT_CODEX_WORKSPACE", raising=False)
client = CodexAppServerClient(
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
request_timeout=2,
startup_timeout=2,
mcp_server_path=MCP_SERVER,
)
first_workspace = client._cwd
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.close()
assert not first_workspace.exists()
client.start(CALLBACK_URL, CALLBACK_TOKEN)
second_workspace = client._cwd
try:
assert client.available is True
assert second_workspace != first_workspace
assert second_workspace.is_dir()
finally:
client.close()
assert not second_workspace.exists()
def test_default_instructions_allow_implicit_comfort_normalization(
tmp_path: Path,
) -> None:
client, trace_path = _client(tmp_path)
try:
client.start(CALLBACK_URL, CALLBACK_TOKEN)
client.start_thread(ephemeral=True)
finally:
client.close()
wire = [json.loads(line) for line in trace_path.read_text(encoding="utf-8").splitlines()]
request = next(item for item in wire if item.get("method") == "thread/start")
instructions = request["params"]["developerInstructions"]
assert "Copy the opaque request_token" in instructions
assert "implicit comfort" in instructions
assert "binds it to the original" in instructions
assert "structured semantic plan" in instructions
assert "get_n7web_capabilities" in instructions
assert "original instruction unchanged" not in instructions