from __future__ import annotations import contextlib import os import tempfile from collections.abc import Mapping from pathlib import Path from typing import Any from unittest import mock ROOT = Path(__file__).resolve().parents[1] FAKE_CODEX = ROOT / "tests" / "helpers" / "fake_codex.py" FAKE_SWITCHYARD = ROOT / "tests" / "helpers" / "fake_switchyard.py" def root_thread_binding(thread_id: str | None) -> dict[str, object]: """Return one internally consistent root-thread lineage for state-fixture tests.""" if thread_id is None: return { "root_thread_id": None, "root_thread_generation": 0, "root_thread_lineage": [], "root_thread_transition": None, } return { "root_thread_id": thread_id, "root_thread_generation": 1, "root_thread_lineage": [ { "generation": 1, "thread_id": thread_id, "codex_session_id": thread_id, "adopted_at": "2000-01-01T00:00:00+00:00", "reason": "test_fixture", } ], "root_thread_transition": None, } def create_access_lab_session( *, cwd: Path, profile: str | Path = "access-efficient-escalation-lab" ) -> dict[str, Any]: """Create the local-first fixture without depending on a host llama.cpp process.""" import mmo_runtime real_availability = mmo_runtime.route_availability def available_local_route(snapshot: Mapping[str, Any]) -> dict[str, dict[str, Any]]: availability = real_availability(snapshot) availability["llama_cpp_local_openai_chat"] = { "available": True, "selected_credential_env": None, "reason": None, } return availability with mock.patch.object( mmo_runtime, "route_availability", side_effect=available_local_route, ): return mmo_runtime.create_session(profile=profile, cwd=cwd) class RuntimeSandbox: def __init__(self) -> None: self.temp = tempfile.TemporaryDirectory(prefix="codex-mmo-test-") self.root = Path(self.temp.name) self.config = self.root / "config with spaces" self.state = self.root / "state with spaces" self.runtime = self.root / "runtime with spaces" self.workspace = self.root / "workspace with spaces" self.base_codex_home = self.root / "base codex home" self.old_env: dict[str, str | None] = {} def __enter__(self) -> RuntimeSandbox: for path in ( self.config, self.state, self.runtime, self.workspace, self.base_codex_home, ): path.mkdir(parents=True, exist_ok=True) self.old_env = { key: os.environ.get(key) for key in ( "MMO_INSTALL_ROOT", "MMO_CONFIG_ROOT", "MMO_STATE_ROOT", "XDG_RUNTIME_DIR", "MMO_CODEX_BIN", "ZAI_CODING_API_KEY", "OPENCODE_API_KEY", "OPENROUTER_API_KEY", "OPENAI_API_KEY", "FIRECRAWL_API_KEY", "IDA_MCP_TOKEN", "CODEX_HOME", ) } os.environ.update( { "MMO_INSTALL_ROOT": str(ROOT), "MMO_CONFIG_ROOT": str(self.config), "MMO_STATE_ROOT": str(self.state), "XDG_RUNTIME_DIR": str(self.runtime), "MMO_CODEX_BIN": str(FAKE_CODEX), "ZAI_CODING_API_KEY": "fake-zai-coding", "OPENCODE_API_KEY": "fake-opencode", "OPENROUTER_API_KEY": "fake-openrouter", "OPENAI_API_KEY": "fake-openai", } ) (self.config / "profiles.d").mkdir() (self.config / "catalog.d").mkdir() (self.config / "tool-mcp.d").mkdir() settings = (ROOT / "config" / "settings.toml").read_text(encoding="utf-8") settings = settings.replace( 'base_codex_home = "~/.codex"', f"base_codex_home = {self.base_codex_home.as_posix()!r}" ) # TOML accepts JSON-style double quoted paths, not Python repr single quotes. settings = settings.replace( f"base_codex_home = '{self.base_codex_home.as_posix()}'", f'base_codex_home = "{self.base_codex_home.as_posix()}"', ) settings = settings.replace( 'switchyard_bin = "switchyard-server"', f'switchyard_bin = "{FAKE_SWITCHYARD.as_posix()}"', ) settings = settings.replace( "gateway_start_timeout_seconds = 15", "gateway_start_timeout_seconds = 5" ) settings = settings.replace( "gateway_idle_timeout_seconds = 3600", "gateway_idle_timeout_seconds = 1" ) (self.config / "settings.toml").write_text(settings, encoding="utf-8") (self.config / "credentials.env").write_text( "ZAI_CODING_API_KEY=fake-zai-coding\nOPENCODE_API_KEY=fake-opencode\n" "OPENROUTER_API_KEY=fake-openrouter\nOPENAI_API_KEY=fake-openai\n", encoding="utf-8", ) (self.base_codex_home / "auth.json").write_text('{"fake":true}\n', encoding="utf-8") return self def __exit__(self, *_exc: object) -> None: with contextlib.suppress(Exception): from mmo_runtime import cancel_session, iter_sessions for session in iter_sessions(strict=False): with contextlib.suppress(Exception): cancel_session(str(session["session_id"])) with contextlib.suppress(Exception): from mmo_gateway import list_gateways, stop_gateway for gateway in list_gateways(): stop_gateway(str(gateway["snapshot_hash"])) for key, value in self.old_env.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value self.temp.cleanup() def init_git(self) -> None: import subprocess subprocess.run(["git", "init", "-q", str(self.workspace)], check=True) subprocess.run( ["git", "-C", str(self.workspace), "config", "user.email", "test@example.invalid"], check=True, ) subprocess.run( ["git", "-C", str(self.workspace), "config", "user.name", "MMO Test"], check=True ) (self.workspace / "README.md").write_text("fixture\n", encoding="utf-8") subprocess.run(["git", "-C", str(self.workspace), "add", "."], check=True) subprocess.run(["git", "-C", str(self.workspace), "commit", "-qm", "fixture"], check=True)