168 lines
6.3 KiB
Python
168 lines
6.3 KiB
Python
"""Tests for ``_codex_native_launch_config`` in ``omnigent/runner/app.py``.
|
|
|
|
The runner fetches a session snapshot over HTTP and validates it before
|
|
launching a runner-owned Codex terminal. Each malformed field is meant to
|
|
fail loud with a RuntimeError rather than launch Codex with garbage; those
|
|
guards were previously unexercised by any direct test. These tests drive the
|
|
function with a stub async client returning controlled snapshots.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from omnigent.runner.app import _codex_native_launch_config
|
|
|
|
|
|
class _Resp:
|
|
"""Minimal stand-in for an httpx response carrying a fixed status + payload."""
|
|
|
|
def __init__(self, status_code: int, payload: Any, *, json_raises: bool = False) -> None:
|
|
self.status_code = status_code
|
|
self._payload = payload
|
|
self._json_raises = json_raises
|
|
|
|
def json(self) -> Any:
|
|
if self._json_raises:
|
|
raise ValueError("not json")
|
|
return self._payload
|
|
|
|
|
|
class _Client:
|
|
"""Async client stub whose ``get`` returns a fixed response or raises."""
|
|
|
|
def __init__(self, resp: _Resp | None = None, raise_exc: Exception | None = None) -> None:
|
|
self._resp = resp
|
|
self._raise_exc = raise_exc
|
|
|
|
async def get(self, url: str, timeout: float | None = None) -> _Resp:
|
|
if self._raise_exc is not None:
|
|
raise self._raise_exc
|
|
assert self._resp is not None
|
|
return self._resp
|
|
|
|
|
|
async def _run(client: _Client | None, session_id: str = "conv_1") -> Any:
|
|
return await _codex_native_launch_config(session_id=session_id, server_client=client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_client_raises() -> None:
|
|
"""No server client means there is no way to fetch config — fail loud."""
|
|
with pytest.raises(RuntimeError, match="server_client is required"):
|
|
await _run(None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_error_raises() -> None:
|
|
"""A transport error fetching the snapshot surfaces as a RuntimeError."""
|
|
client = _Client(raise_exc=httpx.ConnectError("boom"))
|
|
with pytest.raises(RuntimeError, match="Could not fetch Codex launch config"):
|
|
await _run(client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_200_raises() -> None:
|
|
"""A non-200 status is rejected and names the status in the error."""
|
|
client = _Client(_Resp(404, None))
|
|
with pytest.raises(RuntimeError, match="returned 404"):
|
|
await _run(client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_json_raises() -> None:
|
|
"""A body that does not parse as JSON is rejected."""
|
|
client = _Client(_Resp(200, None, json_raises=True))
|
|
with pytest.raises(RuntimeError, match="invalid JSON"):
|
|
await _run(client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_dict_snapshot_raises() -> None:
|
|
"""A JSON array (not an object) is not a valid session snapshot."""
|
|
client = _Client(_Resp(200, ["not", "a", "dict"]))
|
|
with pytest.raises(RuntimeError, match="not a JSON object"):
|
|
await _run(client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("field", "value", "match"),
|
|
[
|
|
("terminal_launch_args", "not-a-list", "terminal_launch_args"),
|
|
("terminal_launch_args", [1, 2], "terminal_launch_args"),
|
|
("model_override", "", "model_override"),
|
|
("model_override", 5, "model_override"),
|
|
("external_session_id", "", "external_session_id"),
|
|
("workspace", "", "workspace"),
|
|
],
|
|
)
|
|
async def test_invalid_field_raises(field: str, value: Any, match: str) -> None:
|
|
"""Each malformed optional field is rejected with a field-specific error."""
|
|
client = _Client(_Resp(200, {field: value}))
|
|
with pytest.raises(RuntimeError, match=match):
|
|
await _run(client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_happy_path_parses_full_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A well-formed snapshot (with fork labels) parses into a launch config."""
|
|
monkeypatch.setenv("RUNNER_SERVER_URL", "http://127.0.0.1:8123")
|
|
snapshot = {
|
|
"workspace": "/tmp/repo",
|
|
"terminal_launch_args": ["--config", "approval_policy=on-request"],
|
|
"model_override": "gpt-5.4-mini",
|
|
"external_session_id": "thread_abc",
|
|
"labels": {
|
|
"omnigent.fork.source_id": "conv_source",
|
|
"omnigent.fork.source_external_session_id": "thread_src",
|
|
"omnigent.fork.carry_history": "1",
|
|
"omnigent.codex_native.bypass_sandbox": "1",
|
|
},
|
|
}
|
|
cfg = await _run(_Client(_Resp(200, snapshot)))
|
|
assert cfg.policy_server_url == "http://127.0.0.1:8123"
|
|
assert cfg.terminal_launch_args == ["--config", "approval_policy=on-request"]
|
|
assert cfg.model_override == "gpt-5.4-mini"
|
|
assert cfg.external_session_id == "thread_abc"
|
|
assert cfg.fork_source_id == "conv_source", "Fork source id should be read from labels."
|
|
assert cfg.fork_source_external_id == "thread_src"
|
|
assert cfg.fork_carry_history is True, "carry_history label '1' should parse to True."
|
|
assert cfg.bypass_sandbox is True, "bypass_sandbox label '1' should parse to True."
|
|
assert cfg.workspace.name == "repo", (
|
|
f"Workspace path should resolve from snapshot, got {cfg.workspace}."
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"labels",
|
|
[
|
|
None, # no labels at all
|
|
{}, # labels present but no bypass key
|
|
{"omnigent.codex_native.bypass_sandbox": "0"}, # explicit off
|
|
{"omnigent.codex_native.bypass_sandbox": "true"}, # only "1" arms it
|
|
{"omnigent.codex_native.bypass_sandbox": ""}, # empty string
|
|
],
|
|
)
|
|
async def test_bypass_sandbox_defaults_off_unless_label_is_one(
|
|
monkeypatch: pytest.MonkeyPatch, labels: Any
|
|
) -> None:
|
|
"""
|
|
Fail-safe: ``bypass_sandbox`` is False unless the label is exactly ``"1"``.
|
|
|
|
The dangerous full-bypass stance must never be entered by accident, so an
|
|
absent label, an unrelated value, or any near-miss (``"0"``, ``"true"``,
|
|
``""``) leaves Codex in its normal approval/sandbox stance. Only the
|
|
canonical ``"1"`` (set by the guarded web toggle) arms it.
|
|
"""
|
|
monkeypatch.setenv("RUNNER_SERVER_URL", "http://127.0.0.1:8123")
|
|
snapshot: dict[str, Any] = {"workspace": "/tmp/repo"}
|
|
if labels is not None:
|
|
snapshot["labels"] = labels
|
|
cfg = await _run(_Client(_Resp(200, snapshot)))
|
|
assert cfg.bypass_sandbox is False
|