182 lines
6.6 KiB
Python
182 lines
6.6 KiB
Python
|
|
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)
|