| """Tests for the bundled ``openai-codex`` image_gen plugin. |
| |
| Mirrors ``test_openai_provider.py`` but targets the standalone |
| Codex/ChatGPT-OAuth-backed provider that uses the Responses |
| ``image_generation`` tool path instead of the ``images.generate`` REST |
| endpoint. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import importlib |
| from pathlib import Path |
| from types import SimpleNamespace |
|
|
| import pytest |
|
|
| |
| |
| |
| codex_plugin = importlib.import_module("plugins.image_gen.openai-codex") |
|
|
|
|
| |
| _PNG_HEX = ( |
| "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4" |
| "890000000d49444154789c6300010000000500010d0a2db40000000049454e44" |
| "ae426082" |
| ) |
|
|
|
|
| def _b64_png() -> str: |
| import base64 |
| return base64.b64encode(bytes.fromhex(_PNG_HEX)).decode() |
|
|
|
|
| class _FakeStream: |
| def __init__(self, events, final_response): |
| self._events = list(events) |
| self._final = final_response |
|
|
| def __enter__(self): |
| return self |
|
|
| def __exit__(self, exc_type, exc, tb): |
| return False |
|
|
| def __iter__(self): |
| return iter(self._events) |
|
|
| def get_final_response(self): |
| return self._final |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def _tmp_hermes_home(tmp_path, monkeypatch): |
| monkeypatch.setenv("HERMES_HOME", str(tmp_path)) |
| yield tmp_path |
|
|
|
|
| @pytest.fixture |
| def provider(monkeypatch): |
| |
| monkeypatch.delenv("OPENAI_API_KEY", raising=False) |
| return codex_plugin.OpenAICodexImageGenProvider() |
|
|
|
|
| |
|
|
|
|
| class TestMetadata: |
| def test_name(self, provider): |
| assert provider.name == "openai-codex" |
|
|
| def test_display_name(self, provider): |
| assert provider.display_name == "OpenAI (Codex auth)" |
|
|
| def test_default_model(self, provider): |
| assert provider.default_model() == "gpt-image-2-medium" |
|
|
| def test_list_models_three_tiers(self, provider): |
| ids = [m["id"] for m in provider.list_models()] |
| assert ids == ["gpt-image-2-low", "gpt-image-2-medium", "gpt-image-2-high"] |
|
|
| def test_setup_schema_has_no_required_env_vars(self, provider): |
| schema = provider.get_setup_schema() |
| assert schema["env_vars"] == [] |
| assert schema["badge"] == "free" |
|
|
|
|
| |
|
|
|
|
| class TestAvailability: |
| def test_unavailable_without_codex_token(self, monkeypatch): |
| monkeypatch.delenv("OPENAI_API_KEY", raising=False) |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: None) |
| assert codex_plugin.OpenAICodexImageGenProvider().is_available() is False |
|
|
| def test_available_with_codex_token(self, monkeypatch): |
| monkeypatch.delenv("OPENAI_API_KEY", raising=False) |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
| assert codex_plugin.OpenAICodexImageGenProvider().is_available() is True |
|
|
| def test_openai_api_key_alone_is_not_enough(self, monkeypatch): |
| |
| |
| monkeypatch.setenv("OPENAI_API_KEY", "sk-test") |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: None) |
| assert codex_plugin.OpenAICodexImageGenProvider().is_available() is False |
|
|
|
|
| |
|
|
|
|
| class TestGenerate: |
| def test_returns_auth_error_without_codex_token(self, provider, monkeypatch): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: None) |
| result = provider.generate("a cat") |
| assert result["success"] is False |
| assert result["error_type"] == "auth_required" |
|
|
| def test_returns_invalid_argument_for_empty_prompt(self, provider, monkeypatch): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
| result = provider.generate(" ") |
| assert result["success"] is False |
| assert result["error_type"] == "invalid_argument" |
|
|
| def test_generate_uses_codex_stream_path(self, provider, monkeypatch, tmp_path): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
|
|
| output_item = SimpleNamespace( |
| type="image_generation_call", |
| status="generating", |
| id="ig_test", |
| result=_b64_png(), |
| ) |
| done_event = SimpleNamespace(type="response.output_item.done", item=output_item) |
| final_response = SimpleNamespace(output=[], status="completed", output_text="") |
|
|
| fake_client = SimpleNamespace( |
| responses=SimpleNamespace( |
| stream=lambda **kwargs: _FakeStream([done_event], final_response) |
| ) |
| ) |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: fake_client) |
|
|
| result = provider.generate("a cat", aspect_ratio="landscape") |
|
|
| assert result["success"] is True |
| assert result["model"] == "gpt-image-2-medium" |
| assert result["provider"] == "openai-codex" |
| assert result["quality"] == "medium" |
|
|
| saved = Path(result["image"]) |
| assert saved.exists() |
| assert saved.parent == tmp_path / "cache" / "images" |
| |
| |
| assert saved.name.startswith("openai_codex_") |
|
|
| def test_codex_stream_request_shape(self, provider, monkeypatch): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
|
|
| captured = {} |
|
|
| def _stream(**kwargs): |
| captured.update(kwargs) |
| output_item = SimpleNamespace( |
| type="image_generation_call", |
| status="generating", |
| id="ig_test", |
| result=_b64_png(), |
| ) |
| done_event = SimpleNamespace(type="response.output_item.done", item=output_item) |
| final_response = SimpleNamespace(output=[], status="completed", output_text="") |
| return _FakeStream([done_event], final_response) |
|
|
| fake_client = SimpleNamespace(responses=SimpleNamespace(stream=_stream)) |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: fake_client) |
|
|
| result = provider.generate("a cat", aspect_ratio="portrait") |
| assert result["success"] is True |
|
|
| assert captured["model"] == "gpt-5.4" |
| assert captured["store"] is False |
| assert captured["input"][0]["type"] == "message" |
| assert captured["input"][0]["role"] == "user" |
| assert captured["input"][0]["content"][0]["type"] == "input_text" |
| assert captured["tool_choice"]["type"] == "allowed_tools" |
| assert captured["tool_choice"]["mode"] == "required" |
| assert captured["tool_choice"]["tools"] == [{"type": "image_generation"}] |
|
|
| tool = captured["tools"][0] |
| assert tool["type"] == "image_generation" |
| assert tool["model"] == "gpt-image-2" |
| assert tool["quality"] == "medium" |
| assert tool["size"] == "1024x1536" |
| assert tool["output_format"] == "png" |
| assert tool["background"] == "opaque" |
| assert tool["partial_images"] == 1 |
|
|
| def test_partial_image_event_used_when_done_missing(self, provider, monkeypatch): |
| """If the stream never emits output_item.done, fall back to the |
| partial_image event so users at least get the latest preview frame.""" |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
|
|
| partial_event = SimpleNamespace( |
| type="response.image_generation_call.partial_image", |
| partial_image_b64=_b64_png(), |
| ) |
| final_response = SimpleNamespace(output=[], status="completed", output_text="") |
|
|
| fake_client = SimpleNamespace( |
| responses=SimpleNamespace( |
| stream=lambda **kwargs: _FakeStream([partial_event], final_response) |
| ) |
| ) |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: fake_client) |
|
|
| result = provider.generate("a cat") |
| assert result["success"] is True |
| assert Path(result["image"]).exists() |
|
|
| def test_final_response_sweep_recovers_image(self, provider, monkeypatch): |
| """If no image_generation_call event arrives mid-stream, the |
| post-stream final-response sweep should still find the image.""" |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
|
|
| final_item = SimpleNamespace( |
| type="image_generation_call", |
| status="completed", |
| id="ig_final", |
| result=_b64_png(), |
| ) |
| final_response = SimpleNamespace(output=[final_item], status="completed", output_text="") |
|
|
| fake_client = SimpleNamespace( |
| responses=SimpleNamespace( |
| stream=lambda **kwargs: _FakeStream([], final_response) |
| ) |
| ) |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: fake_client) |
|
|
| result = provider.generate("a cat") |
| assert result["success"] is True |
| assert Path(result["image"]).exists() |
|
|
| def test_empty_response_returns_error(self, provider, monkeypatch): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
|
|
| final_response = SimpleNamespace(output=[], status="completed", output_text="") |
| fake_client = SimpleNamespace( |
| responses=SimpleNamespace( |
| stream=lambda **kwargs: _FakeStream([], final_response) |
| ) |
| ) |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: fake_client) |
|
|
| result = provider.generate("a cat") |
| assert result["success"] is False |
| assert result["error_type"] == "empty_response" |
|
|
| def test_client_init_failure_returns_auth_error(self, provider, monkeypatch): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: None) |
|
|
| result = provider.generate("a cat") |
| assert result["success"] is False |
| assert result["error_type"] == "auth_required" |
|
|
| def test_stream_exception_returns_api_error(self, provider, monkeypatch): |
| monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token") |
|
|
| def _boom(**kwargs): |
| raise RuntimeError("cloudflare 403") |
|
|
| fake_client = SimpleNamespace(responses=SimpleNamespace(stream=_boom)) |
| monkeypatch.setattr(codex_plugin, "_build_codex_client", lambda: fake_client) |
|
|
| result = provider.generate("a cat") |
| assert result["success"] is False |
| assert result["error_type"] == "api_error" |
| assert "cloudflare 403" in result["error"] |
|
|
|
|
| |
|
|
|
|
| class TestRegistration: |
| def test_register_calls_register_image_gen_provider(self): |
| registered = [] |
|
|
| class _Ctx: |
| def register_image_gen_provider(self, prov): |
| registered.append(prov) |
|
|
| codex_plugin.register(_Ctx()) |
| assert len(registered) == 1 |
| assert registered[0].name == "openai-codex" |
|
|