"""Tests for native-Codex provider routing (configure harnesses parity). Covers :func:`omnigent.inner.codex_executor._provider_codex_config_overrides` and :func:`omnigent.codex_native_app_server.resolve_native_codex_launch` — the path that makes a native Codex terminal route through a ``configure harness`` provider just like the in-process codex harness, instead of only the Databricks ucode profile. Providers are constructed via the real config parser; config + ambient are isolated so resolution is deterministic. """ from __future__ import annotations from pathlib import Path import pytest import yaml from omnigent.codex_native_app_server import resolve_native_codex_launch from omnigent.inner.codex_executor import _provider_codex_config_overrides @pytest.fixture() def _isolated(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Isolate config + ambient so codex routing resolution is deterministic.""" monkeypatch.setenv("OMNIGENT_CONFIG_HOME", str(tmp_path)) monkeypatch.setenv("OMNIGENT_DISABLE_KEYRING", "1") monkeypatch.setenv("HOME", str(tmp_path)) for var in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "CODEX_HOME"): monkeypatch.delenv(var, raising=False) monkeypatch.delenv("DATABRICKS_CONFIG_PROFILE", raising=False) return tmp_path def _seed(config_home: Path, providers: dict[str, object]) -> None: """Write a ``providers:`` block into the isolated config home.""" (config_home / "config.yaml").write_text(yaml.safe_dump({"providers": providers})) def _write_codex_login(home: Path, *, logged_in: bool) -> None: """Write (or stub-empty) ``~/.codex/auth.json`` under the isolated HOME. The native subscription routing resolves ``CODEX_HOME or ~/.codex`` — with HOME redirected to *home* by the ``_isolated`` fixture, this controls whether Codex is considered logged in. :param home: The isolated HOME directory (the ``_isolated`` fixture value). :param logged_in: When ``True``, write an apikey-mode credential so ``codex_auth_has_credential`` returns ``True``; when ``False``, write an empty ``{}`` (present-but-logged-out) so it returns ``False``. """ codex_dir = home / ".codex" codex_dir.mkdir(parents=True, exist_ok=True) content = '{"auth_mode": "apikey", "OPENAI_API_KEY": "sk-codex-login"}' if logged_in else "{}" (codex_dir / "auth.json").write_text(content, encoding="utf-8") def test_provider_codex_overrides_coerce_chat_wire_to_responses() -> None: """A ``chat`` provider wire is coerced to ``responses`` in the override. codex >= 0.137 hard-fails config load on ``wire_api="chat"`` (``wire_api = "chat" is no longer supported``), so emitting it would break OSS / OpenRouter routing outright. ``responses`` is the only wire codex still accepts, so the override must carry it even for a chat-configured provider. Failure (a literal ``wire_api="chat"`` line) means a native Codex launch would refuse to start. """ overrides = _provider_codex_config_overrides( model="qwen/qwen3.7-plus", base_url="https://openrouter.ai/api/v1", auth_command="printf %s sk-or-test", wire_api="chat", ) joined = "\n".join(overrides) assert 'model="qwen/qwen3.7-plus"' in joined assert 'model_provider="omnigent_provider"' in joined assert 'base_url="https://openrouter.ai/api/v1"' in joined # chat is coerced to responses; codex >= 0.137 rejects a chat config. assert 'wire_api="responses"' in joined assert 'wire_api="chat"' not in joined # The token command is embedded as the sh auth command. assert "printf %s sk-or-test" in joined def test_provider_codex_overrides_preserve_responses_wire() -> None: """An explicit ``responses`` wire passes through unchanged.""" overrides = _provider_codex_config_overrides( model="gpt-5.5", base_url="https://api.openai.com/v1", auth_command="printf %s sk", wire_api="responses", ) assert 'wire_api="responses"' in "\n".join(overrides) def test_provider_codex_overrides_omit_model_line_when_none() -> None: """``model=None`` omits the ``model="..."`` line but still routes.""" overrides = _provider_codex_config_overrides( model=None, base_url="https://api.openai.com/v1", auth_command="printf %s sk", wire_api="responses", ) joined = "\n".join(overrides) assert "model=" not in joined.replace("model_provider=", "") # no bare model= line assert 'model_provider="omnigent_provider"' in joined def test_resolve_native_codex_launch_key_default_routes_via_overrides( _isolated: Path, ) -> None: """An openai key default → provider overrides, profile None. The P0 parity: native Codex honors `configure harnesses`. Failure means the native launch ignored the configured provider. """ _seed( _isolated, { "openai": { "kind": "key", "default": True, "openai": { "base_url": "https://api.openai.com/v1", "api_key": "sk-oai-default", "models": {"default": "gpt-5.5"}, }, } }, ) launch = resolve_native_codex_launch(model=None) assert launch.profile is None # a provider routes via overrides, not a profile assert launch.model == "gpt-5.5" joined = "\n".join(launch.config_overrides) assert 'base_url="https://api.openai.com/v1"' in joined assert "printf %s sk-oai-default" in joined def test_resolve_native_codex_launch_openrouter_coerces_chat_wire(_isolated: Path) -> None: """A chat-configured gateway (OpenRouter) routes with the coerced responses wire. The provider is persisted with ``wire_api: chat``, but codex >= 0.137 can no longer load a chat config, so the resolved launch overrides must carry ``wire_api="responses"`` (the coercion in ``_provider_codex_config_overrides``). """ _seed( _isolated, { "openrouter": { "kind": "gateway", "default": True, "openai": { "base_url": "https://openrouter.ai/api/v1", "api_key": "sk-or", "wire_api": "chat", }, } }, ) launch = resolve_native_codex_launch(model="qwen/q") joined = "\n".join(launch.config_overrides) assert 'wire_api="responses"' in joined assert 'wire_api="chat"' not in joined assert 'base_url="https://openrouter.ai/api/v1"' in joined # Explicit model override wins over the (absent) provider default. assert launch.model == "qwen/q" def test_resolve_native_codex_launch_subscription_logged_in_uses_cli_login( _isolated: Path, ) -> None: """A subscription default + a logged-in Codex → CLI login, openai pinned. When Codex actually has a stored login, deferring to its own auth is correct — the bridged ``auth.json`` authenticates it. The launch still pins the built-in ``openai`` provider: the bridged config.toml may set a custom default ``model_provider`` (e.g. isaac's Databricks AI Gateway), which would otherwise silently hijack the Subscription selection. Failure with extra overrides means we synthesized a provider route over a working subscription; failure with NO overrides means the pin regressed and a custom config.toml default can shadow the subscription again. """ _seed( _isolated, {"codex-subscription": {"kind": "subscription", "cli": "codex", "default": True}}, ) _write_codex_login(_isolated, logged_in=True) launch = resolve_native_codex_launch(model=None) # Exactly the openai pin — no base_url/auth overrides (the login carries auth). assert launch.config_overrides == ['model_provider="openai"'] assert launch.profile is None def test_resolve_native_codex_launch_subscription_ignores_private_inherited_home( _isolated: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """ A private inherited ``CODEX_HOME`` does not hide the real Codex login. Nested Omnigent runs can inherit a per-session private Codex home from the parent native terminal. Subscription routing must check the same real ``~/.codex`` source that the app-server launch will bridge from; otherwise it falls through to a key provider even though the Codex CLI is logged in. :param _isolated: Isolated HOME/config directory. :param monkeypatch: Pytest fixture used to set inherited ``CODEX_HOME``. :returns: None. """ _seed( _isolated, { "codex-subscription": {"kind": "subscription", "cli": "codex", "default": True}, "openai": { "kind": "key", "openai": { "base_url": "https://api.openai.com/v1", "api_key": "sk-oai-real", "models": {"default": "gpt-5.5"}, }, }, }, ) _write_codex_login(_isolated, logged_in=True) inherited = _isolated / ".omnigent" / "codex-native" / "abc123" / "codex-home" inherited.mkdir(parents=True) monkeypatch.setenv("CODEX_HOME", str(inherited)) launch = resolve_native_codex_launch(model=None) # The openai pin (see the logged-in test); the point here is that no # key-provider overrides were synthesized despite the private CODEX_HOME. assert launch.config_overrides == ['model_provider="openai"'] assert launch.profile is None def test_resolve_native_codex_launch_subscription_no_login_falls_through_to_key( _isolated: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Subscription default but Codex NOT logged in → falls through to a real key. This is the core fix for the reported bug: a stale/dead subscription default must not strand the user at Codex's login screen when they have a real credential configured. The key is NOT the persisted default (the subscription is), so this proves the runtime fall-through, not a default change. Failure means the dead subscription shadows the key → empty overrides → Codex login prompt. """ # No ambient providers, so the fall-through target is unambiguously the # explicitly-configured key (not a detected env key / Ollama). monkeypatch.setattr("omnigent.onboarding.ambient._ollama_reachable", lambda: False) _seed( _isolated, { "codex-subscription": {"kind": "subscription", "cli": "codex", "default": True}, "openai": { "kind": "key", "openai": { "base_url": "https://api.openai.com/v1", "api_key": "sk-oai-real", "models": {"default": "gpt-5.5"}, }, }, }, ) _write_codex_login(_isolated, logged_in=False) launch = resolve_native_codex_launch(model=None) # Routed through the real key, not Codex's login. assert launch.profile is None assert launch.model == "gpt-5.5" joined = "\n".join(launch.config_overrides) assert 'base_url="https://api.openai.com/v1"' in joined assert "printf %s sk-oai-real" in joined def test_resolve_native_codex_launch_subscription_no_login_no_alternative_uses_login( _isolated: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Subscription default, NOT logged in, no other provider → Codex login. With no usable Codex login and nothing to fall through to, dropping to Codex's own login is the correct outcome (the user must re-authenticate) — still pinned to the built-in ``openai`` provider so the login screen the user lands on is ChatGPT's, not a custom config.toml provider's. Failure with base_url/auth overrides would mean we fabricated a route from nothing. """ monkeypatch.setattr("omnigent.onboarding.ambient._ollama_reachable", lambda: False) _seed( _isolated, {"codex-subscription": {"kind": "subscription", "cli": "codex", "default": True}}, ) _write_codex_login(_isolated, logged_in=False) launch = resolve_native_codex_launch(model=None) assert launch.config_overrides == ['model_provider="openai"'] assert launch.profile is None def test_resolve_native_codex_launch_databricks_provider_uses_profile(_isolated: Path) -> None: """A databricks provider default → the ucode profile path (its profile).""" _seed( _isolated, {"databricks": {"kind": "databricks", "default": True, "profile": "oss"}}, ) launch = resolve_native_codex_launch(model=None) assert launch.config_overrides == [] # Routes via the Databricks profile path, not provider overrides. assert launch.profile == "oss" def test_resolve_native_codex_launch_global_auth_when_no_provider(_isolated: Path) -> None: """No provider configured + a global Databricks ``auth:`` block → ucode. With the ``--profile`` flag removed, the global ``auth:`` block in ``config.yaml`` is the only spec-less way to route native Codex through a Databricks profile. Failure means the global auth fallback was skipped and the launch dropped to ambient detection / Codex's own login. """ (_isolated / "config.yaml").write_text( yaml.safe_dump({"auth": {"type": "databricks", "profile": "oss"}}) ) launch = resolve_native_codex_launch(model=None) # Routes via the Databricks ucode profile path, not provider overrides. assert launch.config_overrides == [] assert launch.profile == "oss" def test_resolve_native_codex_launch_ambient_key_routes( _isolated: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Spec-less with only an ambient OPENAI_API_KEY → provider overrides. First run without configure: native Codex still routes through the detected env key (api.openai.com), not the CLI login. """ monkeypatch.setenv("OPENAI_API_KEY", "sk-oai-ambient") launch = resolve_native_codex_launch(model=None) assert launch.profile is None joined = "\n".join(launch.config_overrides) assert 'base_url="https://api.openai.com/v1"' in joined assert "printf %s sk-oai-ambient" in joined def test_resolve_native_codex_launch_cli_config_default_pins_provider( _isolated: Path, ) -> None: """A cli-config default routes native codex via a model_provider pin only. The provider table + credential live in ~/.codex/config.toml (bridged into the session CODEX_HOME), so the launch must carry exactly the pin — no synthesized base_url/auth overrides, no profile, no forced model. Failure on the pin means an adopted isaac-style provider launches the native terminal on codex's built-in (unauthenticated) path; extra overrides mean we fabricated a transport over the config.toml one. """ _seed( _isolated, { "codex-databricks": { "kind": "cli-config", "cli": "codex", "model_provider": "Databricks", "display_name": "Databricks AI Gateway", "default": True, } }, ) launch = resolve_native_codex_launch(model=None) assert launch.config_overrides == ['model_provider="Databricks"'] assert launch.profile is None assert launch.model is None _DISMISSIBLE_CODEX_CONFIG = """ model_provider = "Databricks" [model_providers.Databricks] name = "Databricks AI Gateway" base_url = "https://example.ai-gateway.cloud.databricks.com/codex/v1" [model_providers.Databricks.auth] command = "jq" """ def test_resolve_native_codex_launch_dismissed_config_provider_pins_openai( _isolated: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A Removed (dismissed) config.toml provider is neutralized at launch. With the detection dismissed and nothing else configured, the launch resolves NO provider — but the bridged ~/.codex/config.toml still sets ``model_provider = "Databricks"``, so an unpinned launch would silently route through the very credential the user removed (the reported bug: codex kept answering through the gateway after Remove). The launch must pin codex's built-in ``openai`` provider instead. """ monkeypatch.setattr("omnigent.onboarding.ambient._ollama_reachable", lambda: False) codex_dir = _isolated / ".codex" codex_dir.mkdir() (codex_dir / "config.toml").write_text(_DISMISSIBLE_CODEX_CONFIG) (_isolated / "config.yaml").write_text( yaml.safe_dump({"dismissed_detections": ["codex-databricks"]}) ) launch = resolve_native_codex_launch(model=None) assert launch.config_overrides == ['model_provider="openai"'] assert launch.profile is None def test_resolve_native_codex_launch_undismissed_config_provider_routes_via_pin( _isolated: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The same config WITHOUT a dismissal routes through the detected provider. Counterpart to the dismissal test above: an isaac-configured machine that never Removed anything must keep routing through the gateway (via the detected cli-config provider's pin), not get force-pinned to ``openai``. Failure here means the no-provider neutralization fires too broadly and breaks the feature's golden path. """ monkeypatch.setattr("omnigent.onboarding.ambient._ollama_reachable", lambda: False) codex_dir = _isolated / ".codex" codex_dir.mkdir() (codex_dir / "config.toml").write_text(_DISMISSIBLE_CODEX_CONFIG) launch = resolve_native_codex_launch(model=None) assert launch.config_overrides == ['model_provider="Databricks"'] assert launch.profile is None