😏
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
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)
|
||||
Executable
+1426
File diff suppressed because it is too large
Load Diff
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small Switchyard-compatible health/models stand-in for integration tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import tomllib
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def route_ids(path: Path) -> list[str]:
|
||||
with path.open("rb") as handle:
|
||||
data = tomllib.load(handle)
|
||||
return [str(value["id"]) for value in data.get("routes", {}).values()]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--version", action="version", version="switchyard-server 0.2.0")
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=4000)
|
||||
parser.add_argument("--routing-log-file")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
models = route_ids(args.config)
|
||||
if args.dry_run:
|
||||
print(json.dumps({"valid": True, "routes": models}))
|
||||
return 0
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
def respond(self, status: int, value: object) -> None:
|
||||
body = json.dumps(value).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
if self.path == "/health":
|
||||
self.respond(200, {"status": "ok"})
|
||||
elif self.path in {"/v1/models", "/models"}:
|
||||
self.respond(
|
||||
200,
|
||||
{
|
||||
"object": "list",
|
||||
"data": [{"id": item, "object": "model"} for item in models],
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.respond(404, {"error": "not found"})
|
||||
|
||||
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
||||
|
||||
def stop(_signum: int, _frame: object) -> None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
try:
|
||||
server.serve_forever(poll_interval=0.1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
server.server_close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enter an allocated pseudo-terminal and invoke the real MMO TUI launcher."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
|
||||
from mmo_runtime import launch_interactive
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
# Popen(start_new_session=True) made this process a session leader. The
|
||||
# slave PTY descriptors were inherited, so explicitly claim the terminal
|
||||
# before MMO performs normal foreground process-group handoff.
|
||||
fcntl.ioctl(sys.stdin.fileno(), termios.TIOCSCTTY, 0)
|
||||
os.tcsetpgrp(sys.stdin.fileno(), os.getpgrp())
|
||||
initial = termios.tcgetattr(sys.stdin.fileno())
|
||||
exit_code = launch_interactive(profile=sys.argv[1], cwd=sys.argv[2])
|
||||
restored = termios.tcgetattr(sys.stdin.fileno())
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"event": "launcher_returned",
|
||||
"exit_code": exit_code,
|
||||
"echo_enabled": bool(restored[3] & termios.ECHO),
|
||||
"attributes_restored": restored == initial,
|
||||
"pgrp": os.getpgrp(),
|
||||
"foreground_pgrp": os.tcgetpgrp(sys.stdin.fileno()),
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return exit_code
|
||||
except BaseException as exc:
|
||||
print(json.dumps({"event": "launcher_error", "error": repr(exc)}), flush=True)
|
||||
return 97
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,322 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import mmo_cli_output
|
||||
import mmoctl
|
||||
|
||||
import scripts.install as install_script
|
||||
|
||||
|
||||
class TTYBuffer(io.StringIO):
|
||||
def isatty(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class UnreadableTTY(TTYBuffer):
|
||||
def read(self, *_args: object, **_kwargs: object) -> str:
|
||||
raise AssertionError("interactive stdin must not be read implicitly")
|
||||
|
||||
|
||||
def parser_tree(
|
||||
root: argparse.ArgumentParser,
|
||||
) -> list[tuple[tuple[str, ...], argparse.ArgumentParser]]:
|
||||
found: list[tuple[tuple[str, ...], argparse.ArgumentParser]] = []
|
||||
pending: list[tuple[tuple[str, ...], argparse.ArgumentParser]] = [((), root)]
|
||||
while pending:
|
||||
path, parser = pending.pop()
|
||||
found.append((path, parser))
|
||||
for action in parser._actions:
|
||||
if isinstance(action, argparse._SubParsersAction):
|
||||
pending.extend(((*path, name), child) for name, child in action.choices.items())
|
||||
return found
|
||||
|
||||
|
||||
class CLIUXTests(unittest.TestCase):
|
||||
def test_complete_command_surface_has_descriptions_and_option_help(self) -> None:
|
||||
parsers = parser_tree(mmoctl._parser())
|
||||
self.assertEqual(len(parsers), 69)
|
||||
for path, parser in parsers:
|
||||
with self.subTest(command=" ".join(path) or "root"):
|
||||
self.assertTrue(parser.description)
|
||||
help_text = parser.format_help()
|
||||
self.assertIn("usage:", help_text)
|
||||
self.assertNotIn("\x1b[", help_text)
|
||||
for action in parser._actions:
|
||||
if isinstance(action, (argparse._HelpAction, argparse._SubParsersAction)):
|
||||
continue
|
||||
self.assertNotIn(action.help, (None, "", argparse.SUPPRESS))
|
||||
|
||||
def test_entrypoint_routing_has_one_source_of_truth(self) -> None:
|
||||
parser = mmoctl._parser(prog="codex-mmo")
|
||||
self.assertEqual(mmoctl._route_argv(parser, [], implicit_run=True), ["run"])
|
||||
self.assertEqual(
|
||||
mmoctl._route_argv(parser, ["profile", "list"], implicit_run=True),
|
||||
["profile", "list"],
|
||||
)
|
||||
self.assertEqual(
|
||||
mmoctl._route_argv(parser, ["profile", "list", "--json"], implicit_run=True),
|
||||
["--json", "profile", "list"],
|
||||
)
|
||||
self.assertEqual(
|
||||
mmoctl._route_argv(parser, ["--quiet", "--", "--search"], implicit_run=True),
|
||||
["--quiet", "run", "--", "--search"],
|
||||
)
|
||||
self.assertEqual(
|
||||
mmoctl._route_argv(parser, ["run", "--", "--json"], implicit_run=True),
|
||||
["run", "--", "--json"],
|
||||
)
|
||||
|
||||
def test_usage_errors_are_local_actionable_and_machine_readable(self) -> None:
|
||||
stdout = io.StringIO()
|
||||
stderr = io.StringIO()
|
||||
with mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", stderr):
|
||||
status = mmoctl.main(["session", "list", "--limt", "1"])
|
||||
self.assertEqual(status, 2)
|
||||
self.assertEqual(stdout.getvalue(), "")
|
||||
self.assertIn("usage: codex-mmoctl session list", stderr.getvalue())
|
||||
self.assertIn("Did you mean '--limit'?", stderr.getvalue())
|
||||
|
||||
stdout = io.StringIO()
|
||||
stderr = io.StringIO()
|
||||
with mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", stderr):
|
||||
status = mmoctl.main(["session", "list", "--limt", "1", "--json"])
|
||||
self.assertEqual(status, 2)
|
||||
self.assertEqual(stdout.getvalue(), "")
|
||||
error = json.loads(stderr.getvalue())
|
||||
self.assertEqual(error["error_type"], "usage")
|
||||
self.assertIn("--limit", error["hint"])
|
||||
|
||||
def test_cross_option_errors_fail_before_runtime_initialization(self) -> None:
|
||||
cases = (
|
||||
(["resume"], "exactly one"),
|
||||
(["resume", "session-id", "--last"], "exactly one"),
|
||||
(["resume", "session-id", "--all"], "only with --last"),
|
||||
(["doctor", "--probe"], "requires --live"),
|
||||
(["eval", "compare", "one-run"], "at least two"),
|
||||
(
|
||||
["catalog", "refresh", "--no-codex", "--install-codex-overlay"],
|
||||
"cannot be combined",
|
||||
),
|
||||
(
|
||||
[
|
||||
"catalog",
|
||||
"refresh",
|
||||
"--no-remote",
|
||||
"--openrouter-url",
|
||||
"https://example.invalid/models",
|
||||
],
|
||||
"cannot be combined",
|
||||
),
|
||||
(
|
||||
["catalog", "refresh", "--no-codex", "--codex-bin", "/bin/false"],
|
||||
"cannot be combined",
|
||||
),
|
||||
(["catalog", "discover", "codex", "--timeout", "2"], "not valid"),
|
||||
(
|
||||
[
|
||||
"catalog",
|
||||
"discover",
|
||||
"opencode-go",
|
||||
"--url",
|
||||
"https://one.invalid/models",
|
||||
"--opencode-url",
|
||||
"https://two.invalid/models",
|
||||
],
|
||||
"not both",
|
||||
),
|
||||
(
|
||||
["catalog", "discover", "openrouter", "--codex-bin", "/bin/false"],
|
||||
"not valid",
|
||||
),
|
||||
(["run", "--json"], "not supported"),
|
||||
)
|
||||
for argv, expected in cases:
|
||||
with self.subTest(argv=argv):
|
||||
stderr = io.StringIO()
|
||||
with mock.patch("sys.stdout", io.StringIO()), mock.patch("sys.stderr", stderr):
|
||||
status = mmoctl.main(argv)
|
||||
self.assertEqual(status, 2)
|
||||
self.assertIn(expected, stderr.getvalue())
|
||||
|
||||
def test_exec_never_blocks_for_an_implicit_interactive_prompt(self) -> None:
|
||||
stderr = io.StringIO()
|
||||
with (
|
||||
mock.patch("sys.stdin", UnreadableTTY()),
|
||||
mock.patch("sys.stdout", io.StringIO()),
|
||||
mock.patch("sys.stderr", stderr),
|
||||
):
|
||||
status = mmoctl.main(["exec"])
|
||||
self.assertEqual(status, 2)
|
||||
self.assertIn("requires PROMPT or piped stdin", stderr.getvalue())
|
||||
|
||||
stdout = io.StringIO()
|
||||
with (
|
||||
mock.patch("sys.stdin", io.StringIO("inspect this\n")),
|
||||
mock.patch("sys.stdout", stdout),
|
||||
mock.patch("sys.stderr", io.StringIO()),
|
||||
mock.patch(
|
||||
"mmoctl.run_root_exec",
|
||||
return_value={"exit_code": 0, "result": "complete"},
|
||||
) as execute,
|
||||
):
|
||||
status = mmoctl.main(["exec", "--profile", "fixture"])
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(stdout.getvalue(), "complete\n")
|
||||
self.assertEqual(execute.call_args.kwargs["prompt"], "inspect this\n")
|
||||
|
||||
def test_structured_output_is_human_on_tty_and_json_in_pipelines(self) -> None:
|
||||
session_id = "20260823-120000-reverse-engineering-0123456789"
|
||||
sessions = [
|
||||
{
|
||||
"session_id": session_id,
|
||||
"status": "paused",
|
||||
"profile_id": "reverse-engineering",
|
||||
"session_kind": "interactive",
|
||||
"last_active_at": "2026-08-23T12:00:00+00:00",
|
||||
"resumable": True,
|
||||
}
|
||||
]
|
||||
human = TTYBuffer()
|
||||
with mock.patch("sys.stdout", human), mock.patch.dict(os.environ, {"COLUMNS": "50"}):
|
||||
mmo_cli_output.emit_structured("session.list", sessions)
|
||||
self.assertIn(session_id, human.getvalue())
|
||||
self.assertIn("Status: paused", human.getvalue())
|
||||
self.assertNotIn("\x1b[", human.getvalue())
|
||||
|
||||
machine = io.StringIO()
|
||||
with mock.patch("sys.stdout", machine):
|
||||
mmo_cli_output.emit_structured("session.list", sessions)
|
||||
self.assertEqual(json.loads(machine.getvalue()), sessions)
|
||||
|
||||
forced = TTYBuffer()
|
||||
with mock.patch("sys.stdout", forced):
|
||||
mmo_cli_output.emit_structured("session.list", sessions, force_json=True)
|
||||
self.assertEqual(json.loads(forced.getvalue()), sessions)
|
||||
|
||||
def test_human_empty_states_are_unambiguous(self) -> None:
|
||||
output = TTYBuffer()
|
||||
with mock.patch("sys.stdout", output):
|
||||
mmo_cli_output.emit_structured("jobs.list", [])
|
||||
self.assertEqual(output.getvalue(), "No jobs found.\n")
|
||||
|
||||
def test_catalog_tables_show_domain_fields_instead_of_empty_generic_columns(self) -> None:
|
||||
resources = {
|
||||
"chatgpt_subscription": {
|
||||
"max_active": 4,
|
||||
"lock_key": "provider:chatgpt",
|
||||
"description": "Built-in ChatGPT account concurrency",
|
||||
}
|
||||
}
|
||||
output = TTYBuffer()
|
||||
with mock.patch("sys.stdout", output), mock.patch.dict(os.environ, {"COLUMNS": "120"}):
|
||||
mmo_cli_output.emit_structured("catalog.resources", resources)
|
||||
rendered = output.getvalue()
|
||||
self.assertIn("CAPACITY", rendered)
|
||||
self.assertIn("provider:chatgpt", rendered)
|
||||
self.assertIn("Built-in ChatGPT", rendered)
|
||||
|
||||
def test_progress_uses_only_interactive_stderr_and_honors_quiet(self) -> None:
|
||||
stdout = TTYBuffer()
|
||||
stderr = TTYBuffer()
|
||||
with mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", stderr):
|
||||
mmo_cli_output.progress("Checking profile...")
|
||||
self.assertEqual(stdout.getvalue(), "")
|
||||
self.assertEqual(stderr.getvalue(), "Checking profile...\n")
|
||||
|
||||
quiet = TTYBuffer()
|
||||
with mock.patch("sys.stderr", quiet):
|
||||
mmo_cli_output.progress("Checking profile...", quiet=True)
|
||||
self.assertEqual(quiet.getvalue(), "")
|
||||
|
||||
redirected = io.StringIO()
|
||||
with mock.patch("sys.stderr", redirected):
|
||||
mmo_cli_output.progress("Checking profile...")
|
||||
self.assertEqual(redirected.getvalue(), "")
|
||||
|
||||
def test_runtime_errors_never_pollute_structured_stdout(self) -> None:
|
||||
stdout = io.StringIO()
|
||||
stderr = io.StringIO()
|
||||
with (
|
||||
mock.patch("sys.stdout", stdout),
|
||||
mock.patch("sys.stderr", stderr),
|
||||
mock.patch("mmoctl._handle_catalog", side_effect=RuntimeError("provider failed")),
|
||||
):
|
||||
status = mmoctl.main(["catalog", "summary", "--json"])
|
||||
self.assertEqual(status, 1)
|
||||
self.assertEqual(stdout.getvalue(), "")
|
||||
error = json.loads(stderr.getvalue())
|
||||
self.assertEqual(error["error_type"], "runtime")
|
||||
self.assertEqual(error["error"], "provider failed")
|
||||
|
||||
def test_scalar_commands_offer_explicit_json_without_changing_text_mode(self) -> None:
|
||||
stdout = io.StringIO()
|
||||
with (
|
||||
mock.patch("sys.stdout", stdout),
|
||||
mock.patch("mmoctl.active_profile_id", return_value="reverse-engineering"),
|
||||
):
|
||||
self.assertEqual(mmoctl.main(["profile", "current"]), 0)
|
||||
self.assertEqual(stdout.getvalue(), "reverse-engineering\n")
|
||||
|
||||
stdout = io.StringIO()
|
||||
with (
|
||||
mock.patch("sys.stdout", stdout),
|
||||
mock.patch("mmoctl.active_profile_id", return_value="reverse-engineering"),
|
||||
):
|
||||
self.assertEqual(mmoctl.main(["profile", "current", "--json"]), 0)
|
||||
self.assertEqual(json.loads(stdout.getvalue()), {"profile_id": "reverse-engineering"})
|
||||
|
||||
def test_clean_reports_scope_and_preserves_dry_run_semantics(self) -> None:
|
||||
stdout = io.StringIO()
|
||||
with (
|
||||
mock.patch("sys.stdout", stdout),
|
||||
mock.patch(
|
||||
"mmoctl.load_settings",
|
||||
return_value={"job_retention_days": 30, "session_retention_days": 90},
|
||||
),
|
||||
mock.patch(
|
||||
"mmoctl.clean_state", return_value={"jobs_removed": 4, "sessions_removed": 2}
|
||||
) as clean,
|
||||
):
|
||||
self.assertEqual(mmoctl.main(["clean", "--dry-run"]), 0)
|
||||
clean.assert_called_once_with(job_days=30, session_days=90, dry_run=True)
|
||||
self.assertEqual(
|
||||
json.loads(stdout.getvalue()),
|
||||
{
|
||||
"dry_run": True,
|
||||
"job_days": 30,
|
||||
"jobs_removed": 4,
|
||||
"session_days": 90,
|
||||
"sessions_removed": 2,
|
||||
},
|
||||
)
|
||||
|
||||
def test_generated_wrappers_delegate_all_routing_to_the_python_cli(self) -> None:
|
||||
entrypoint = Path("/tmp/codex mmo/mmoctl.py")
|
||||
primary = install_script.main_wrapper(Path("/usr/bin/python3"), entrypoint)
|
||||
control = install_script.wrapper(
|
||||
Path("/usr/bin/python3"),
|
||||
entrypoint,
|
||||
environment={"MMO_CLI_ENTRYPOINT": "codex-mmoctl"},
|
||||
)
|
||||
self.assertIn("export MMO_CLI_ENTRYPOINT=codex-mmo", primary)
|
||||
self.assertIn("export MMO_CLI_ENTRYPOINT=codex-mmoctl", control)
|
||||
self.assertNotIn("case ", primary)
|
||||
self.assertIn("'$@'".replace("'", '"'), primary)
|
||||
|
||||
def test_installer_help_is_descriptive_and_color_free(self) -> None:
|
||||
help_text = install_script.parser().format_help()
|
||||
self.assertIn("atomically replace", help_text)
|
||||
self.assertIn("--install-root", help_text)
|
||||
self.assertIn("--debug", help_text)
|
||||
self.assertNotIn("\x1b[", help_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,428 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from common import ROOT as PACKAGE_ROOT
|
||||
|
||||
from scripts import build_release
|
||||
from scripts.verify_release import _strict_json_loads as release_strict_json_loads
|
||||
|
||||
VERSION = (PACKAGE_ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
||||
PACKAGE_NAME = f"codex-multimodel-orchestrator-{VERSION}"
|
||||
|
||||
|
||||
class ReleaseIntegrityTests(unittest.TestCase):
|
||||
def test_release_manifest_json_rejects_ambiguous_members_and_invalid_unicode(self) -> None:
|
||||
for payload in (
|
||||
'{"schema_version": 2, "schema_version": 1}',
|
||||
'{"path": "\\ud800"}',
|
||||
'{"size": 1e400}',
|
||||
):
|
||||
with self.subTest(payload=payload), self.assertRaises(ValueError):
|
||||
release_strict_json_loads(payload)
|
||||
|
||||
def test_archive_set_publication_rolls_back_as_a_unit(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-publish-") as temporary_raw:
|
||||
root = Path(temporary_raw)
|
||||
source_paths = tuple(root / f"source-{index}" for index in range(4))
|
||||
final_paths = tuple(root / f"final-{index}" for index in range(4))
|
||||
for index, path in enumerate(source_paths):
|
||||
path.write_bytes(f"new {index}".encode())
|
||||
for index, path in enumerate(final_paths):
|
||||
path.write_bytes(f"old {index}".encode())
|
||||
real_replace = os.replace
|
||||
|
||||
def fail_metadata_publish(
|
||||
source: str | os.PathLike[str], destination: str | os.PathLike[str]
|
||||
) -> None:
|
||||
source_path = Path(source)
|
||||
destination_path = Path(destination)
|
||||
if destination_path == final_paths[2] and ".publish-" in source_path.name:
|
||||
raise OSError("synthetic metadata publication failure")
|
||||
real_replace(source, destination)
|
||||
|
||||
with mock.patch.object(build_release.os, "replace", side_effect=fail_metadata_publish):
|
||||
with self.assertRaisesRegex(OSError, "metadata publication"):
|
||||
build_release._publish_artifacts(
|
||||
tuple(zip(source_paths, final_paths, strict=True))
|
||||
)
|
||||
for index, path in enumerate(final_paths):
|
||||
self.assertEqual(path.read_bytes(), f"old {index}".encode())
|
||||
self.assertFalse(any(path.name.startswith(".final") for path in root.iterdir()))
|
||||
|
||||
symlink_target = root / "symlink-target"
|
||||
symlink_target.write_bytes(b"must remain untouched")
|
||||
symlink_destination = root / "symlink-destination"
|
||||
symlink_destination.symlink_to(symlink_target)
|
||||
with self.assertRaisesRegex(RuntimeError, "non-regular release artifact"):
|
||||
build_release._publish_artifacts(((source_paths[0], symlink_destination),))
|
||||
self.assertTrue(symlink_destination.is_symlink())
|
||||
self.assertEqual(symlink_target.read_bytes(), b"must remain untouched")
|
||||
|
||||
def interrupt_final_verification() -> None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
with self.assertRaises(KeyboardInterrupt):
|
||||
build_release._publish_artifacts(
|
||||
tuple(zip(source_paths, final_paths, strict=True)),
|
||||
verify=interrupt_final_verification,
|
||||
)
|
||||
for index, path in enumerate(final_paths):
|
||||
self.assertEqual(path.read_bytes(), f"old {index}".encode())
|
||||
|
||||
def fail_publish_and_restore(
|
||||
source: str | os.PathLike[str], destination: str | os.PathLike[str]
|
||||
) -> None:
|
||||
source_path = Path(source)
|
||||
destination_path = Path(destination)
|
||||
if destination_path == final_paths[2] and ".publish-" in source_path.name:
|
||||
raise OSError("synthetic publication failure")
|
||||
if destination_path == final_paths[0] and ".backup-" in source_path.name:
|
||||
raise OSError("synthetic restoration failure")
|
||||
real_replace(source, destination)
|
||||
|
||||
with mock.patch.object(
|
||||
build_release.os,
|
||||
"replace",
|
||||
side_effect=fail_publish_and_restore,
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, "rollback was incomplete"):
|
||||
build_release._publish_artifacts(
|
||||
tuple(zip(source_paths, final_paths, strict=True))
|
||||
)
|
||||
retained = list(root.glob(".final-0.backup-*"))
|
||||
self.assertEqual(len(retained), 1)
|
||||
self.assertEqual(retained[0].read_bytes(), b"old 0")
|
||||
final_paths[0].write_bytes(retained[0].read_bytes())
|
||||
retained[0].unlink()
|
||||
|
||||
def fail_final_verification() -> None:
|
||||
raise RuntimeError("synthetic final verification failure")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "final verification"):
|
||||
build_release._publish_artifacts(
|
||||
tuple(zip(source_paths, final_paths, strict=True)),
|
||||
verify=fail_final_verification,
|
||||
)
|
||||
for index, path in enumerate(final_paths):
|
||||
self.assertEqual(path.read_bytes(), f"old {index}".encode())
|
||||
self.assertFalse(any(path.name.startswith(".final") for path in root.iterdir()))
|
||||
|
||||
def test_release_builder_publishes_complete_verified_archives(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-test-") as temporary_raw:
|
||||
output = Path(temporary_raw)
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "build_release.py"),
|
||||
"--output-dir",
|
||||
str(output),
|
||||
"--skip-validation",
|
||||
"--no-reproducibility-check",
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
|
||||
tar_path = output / f"{PACKAGE_NAME}-linux.tar.gz"
|
||||
zip_path = output / f"{PACKAGE_NAME}-linux.zip"
|
||||
integrity_path = output / f"{PACKAGE_NAME}-INTEGRITY.json"
|
||||
for path in (tar_path, zip_path, integrity_path):
|
||||
self.assertTrue(path.is_file(), path)
|
||||
self.assertGreater(path.stat().st_size, 1_000)
|
||||
|
||||
verify = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
|
||||
"--source-tree",
|
||||
str(PACKAGE_ROOT),
|
||||
str(tar_path),
|
||||
str(zip_path),
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertEqual(verify.returncode, 0, verify.stderr or verify.stdout)
|
||||
integrity = json.loads(integrity_path.read_text(encoding="utf-8"))
|
||||
self.assertGreaterEqual(integrity["source_files"], 100)
|
||||
self.assertEqual(len(integrity["archives"]), 2)
|
||||
self.assertEqual(
|
||||
{item["manifest_files"] for item in integrity["archives"]},
|
||||
{integrity["source_files"]},
|
||||
)
|
||||
|
||||
def test_builder_rejects_output_inside_source_tree(self) -> None:
|
||||
output = PACKAGE_ROOT / ".forbidden-release-output"
|
||||
self.assertFalse(output.exists())
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "build_release.py"),
|
||||
"--output-dir",
|
||||
str(output),
|
||||
"--skip-validation",
|
||||
"--no-reproducibility-check",
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("inside the source tree", result.stderr + result.stdout)
|
||||
self.assertFalse(output.exists())
|
||||
|
||||
def test_release_verifier_rejects_tampering_and_wrong_root(self) -> None:
|
||||
package = PACKAGE_NAME
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-tamper-") as temporary_raw:
|
||||
output = Path(temporary_raw)
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "build_release.py"),
|
||||
"--output-dir",
|
||||
str(output),
|
||||
"--skip-validation",
|
||||
"--no-reproducibility-check",
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
|
||||
original = output / f"{package}-linux.zip"
|
||||
|
||||
tampered = output / "tampered.zip"
|
||||
with (
|
||||
zipfile.ZipFile(original) as source,
|
||||
zipfile.ZipFile(tampered, "w", compression=zipfile.ZIP_DEFLATED) as destination,
|
||||
):
|
||||
for info in source.infolist():
|
||||
data = source.read(info.filename)
|
||||
if info.filename == f"{package}/README.md":
|
||||
data += b"\nTAMPERED\n"
|
||||
destination.writestr(copy.copy(info), data)
|
||||
|
||||
wrong_root = output / "wrong-root.zip"
|
||||
with (
|
||||
zipfile.ZipFile(original) as source,
|
||||
zipfile.ZipFile(wrong_root, "w", compression=zipfile.ZIP_DEFLATED) as destination,
|
||||
):
|
||||
for info in source.infolist():
|
||||
renamed = copy.copy(info)
|
||||
renamed.filename = info.filename.replace(package, "wrong-package-root", 1)
|
||||
destination.writestr(renamed, source.read(info.filename))
|
||||
|
||||
mode_drift = output / "mode-drift.zip"
|
||||
with (
|
||||
zipfile.ZipFile(original) as source,
|
||||
zipfile.ZipFile(mode_drift, "w", compression=zipfile.ZIP_DEFLATED) as destination,
|
||||
):
|
||||
for info in source.infolist():
|
||||
changed = copy.copy(info)
|
||||
if info.filename == f"{package}/README.md":
|
||||
changed.external_attr = (stat.S_IFREG | 0o666) << 16
|
||||
destination.writestr(changed, source.read(info.filename))
|
||||
|
||||
directory_mode_drift = output / "directory-mode-drift.zip"
|
||||
with (
|
||||
zipfile.ZipFile(original) as source,
|
||||
zipfile.ZipFile(
|
||||
directory_mode_drift, "w", compression=zipfile.ZIP_DEFLATED
|
||||
) as destination,
|
||||
):
|
||||
for info in source.infolist():
|
||||
changed = copy.copy(info)
|
||||
if info.filename == f"{package}/docs/":
|
||||
changed.external_attr = (stat.S_IFDIR | 0o777) << 16
|
||||
destination.writestr(changed, source.read(info.filename))
|
||||
|
||||
missing_directory = output / "missing-directory-entry.zip"
|
||||
with (
|
||||
zipfile.ZipFile(original) as source,
|
||||
zipfile.ZipFile(
|
||||
missing_directory, "w", compression=zipfile.ZIP_DEFLATED
|
||||
) as destination,
|
||||
):
|
||||
for info in source.infolist():
|
||||
if info.filename != f"{package}/docs/":
|
||||
destination.writestr(copy.copy(info), source.read(info.filename))
|
||||
|
||||
extra_directory = output / "extra-empty-directory.zip"
|
||||
with (
|
||||
zipfile.ZipFile(original) as source,
|
||||
zipfile.ZipFile(
|
||||
extra_directory, "w", compression=zipfile.ZIP_DEFLATED
|
||||
) as destination,
|
||||
):
|
||||
for info in source.infolist():
|
||||
destination.writestr(copy.copy(info), source.read(info.filename))
|
||||
empty = zipfile.ZipInfo(f"{package}/unexpected-empty/")
|
||||
empty.create_system = 3
|
||||
empty.external_attr = (stat.S_IFDIR | 0o755) << 16
|
||||
destination.writestr(empty, b"")
|
||||
|
||||
original_tar = output / f"{package}-linux.tar.gz"
|
||||
tar_directory_mode_drift = output / "directory-mode-drift.tar.gz"
|
||||
with (
|
||||
tarfile.open(original_tar, "r:gz") as source,
|
||||
tarfile.open(tar_directory_mode_drift, "w:gz") as destination,
|
||||
):
|
||||
for tar_info in source:
|
||||
changed_tar = copy.copy(tar_info)
|
||||
if tar_info.name == f"{package}/docs":
|
||||
changed_tar.mode = 0o777
|
||||
payload = source.extractfile(tar_info) if tar_info.isfile() else None
|
||||
if payload is None:
|
||||
destination.addfile(changed_tar)
|
||||
else:
|
||||
with payload:
|
||||
destination.addfile(changed_tar, payload)
|
||||
|
||||
verify = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
|
||||
str(tampered),
|
||||
str(wrong_root),
|
||||
str(mode_drift),
|
||||
str(directory_mode_drift),
|
||||
str(missing_directory),
|
||||
str(extra_directory),
|
||||
str(tar_directory_mode_drift),
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertNotEqual(verify.returncode, 0)
|
||||
report = json.loads(verify.stdout)
|
||||
self.assertEqual(len(report["errors"]), 7)
|
||||
self.assertTrue(any("sha256" in item for item in report["errors"]))
|
||||
self.assertTrue(any("top-level directory" in item for item in report["errors"]))
|
||||
self.assertTrue(any("mode" in item for item in report["errors"]))
|
||||
self.assertEqual(sum("invalid permission mode" in item for item in report["errors"]), 3)
|
||||
self.assertEqual(sum("directory set differs" in item for item in report["errors"]), 2)
|
||||
|
||||
def test_release_verifier_rejects_path_traversal(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-traversal-") as temporary_raw:
|
||||
archive = Path(temporary_raw) / "traversal.zip"
|
||||
with zipfile.ZipFile(archive, "w") as handle:
|
||||
handle.writestr("../escape.txt", "bad")
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
|
||||
str(archive),
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("unsafe archive member path", result.stdout)
|
||||
|
||||
def test_release_verifier_rejects_canonical_path_aliases(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-alias-") as temporary_raw:
|
||||
archive = Path(temporary_raw) / "alias.zip"
|
||||
with zipfile.ZipFile(archive, "w") as handle:
|
||||
handle.writestr(f"{PACKAGE_NAME}/nested/file.txt", "first")
|
||||
handle.writestr(f"{PACKAGE_NAME}/nested/./file.txt", "second")
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
|
||||
str(archive),
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("unsafe archive member path", result.stdout)
|
||||
|
||||
def test_builder_rejects_invalid_source_epoch(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-epoch-") as temporary_raw:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "build_release.py"),
|
||||
"--output-dir",
|
||||
temporary_raw,
|
||||
"--source-date-epoch",
|
||||
"-1",
|
||||
"--skip-validation",
|
||||
"--no-reproducibility-check",
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("source-date-epoch", result.stderr + result.stdout)
|
||||
|
||||
def test_one_file_archives_are_rejected(self) -> None:
|
||||
package = PACKAGE_NAME
|
||||
with tempfile.TemporaryDirectory(prefix="mmo-release-corrupt-") as temporary_raw:
|
||||
temporary = Path(temporary_raw)
|
||||
zip_path = temporary / "incomplete.zip"
|
||||
tar_path = temporary / "incomplete.tar.gz"
|
||||
payload = temporary / "PLAN-COVERAGE.md"
|
||||
payload.write_text("incomplete\n", encoding="utf-8")
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.write(payload, f"{package}/docs/PLAN-COVERAGE.md")
|
||||
with tarfile.open(tar_path, "w:gz") as archive:
|
||||
archive.add(payload, arcname=f"{package}/docs/PLAN-COVERAGE.md")
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(PACKAGE_ROOT / "scripts" / "verify_release.py"),
|
||||
str(zip_path),
|
||||
str(tar_path),
|
||||
],
|
||||
cwd=PACKAGE_ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=60,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
report = json.loads(result.stdout)
|
||||
self.assertFalse(report["passed"])
|
||||
self.assertEqual(len(report["errors"]), 2)
|
||||
self.assertTrue(
|
||||
all("archive directory set differs" in item for item in report["errors"])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,306 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tomllib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import mmo_codex_home
|
||||
import mmo_runtime
|
||||
from common import ROOT, RuntimeSandbox
|
||||
from mmo_runtime import create_session, finish_session
|
||||
from mmo_util import read_toml, toml_dumps
|
||||
from mmo_version import MMO_SCHEMA_VERSION
|
||||
|
||||
|
||||
class TuiAndModelMetadataTests(unittest.TestCase):
|
||||
def _config(self, home: str) -> dict[str, Any]:
|
||||
with (Path(home) / "config.toml").open("rb") as handle:
|
||||
return tomllib.load(handle)
|
||||
|
||||
def _catalog(self, config: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
path = Path(config["model_catalog_json"])
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return {item["slug"]: item for item in payload["models"]}
|
||||
|
||||
def test_switchyard_bindings_have_exact_codex_model_metadata(self) -> None:
|
||||
expected: dict[str, dict[str, Any]] = {
|
||||
"routine_engineer": {
|
||||
"context_window": 1000000,
|
||||
"default_reasoning_level": "high",
|
||||
"apply_patch_tool_type": "freeform",
|
||||
},
|
||||
"literal_scout": {
|
||||
"context_window": 32768,
|
||||
"default_reasoning_level": None,
|
||||
"apply_patch_tool_type": "freeform",
|
||||
},
|
||||
}
|
||||
with RuntimeSandbox() as box:
|
||||
session = create_session(profile="access-efficient-escalation-lab", cwd=box.workspace)
|
||||
try:
|
||||
for agent_id, required in expected.items():
|
||||
home_record = session["homes"][agent_id]
|
||||
config = self._config(home_record["home"])
|
||||
self.assertIn("model_catalog_json", config)
|
||||
self.assertEqual(
|
||||
config["model_catalog_json"], home_record["model_catalog_json"]
|
||||
)
|
||||
entries = self._catalog(config)
|
||||
selected = config["model"]
|
||||
self.assertIn(selected, entries)
|
||||
metadata = entries[selected]
|
||||
self.assertEqual(metadata["slug"], selected)
|
||||
self.assertEqual(metadata["context_window"], required["context_window"])
|
||||
self.assertEqual(metadata["max_context_window"], required["context_window"])
|
||||
self.assertEqual(
|
||||
metadata["default_reasoning_level"],
|
||||
required["default_reasoning_level"],
|
||||
)
|
||||
self.assertEqual(
|
||||
metadata["apply_patch_tool_type"],
|
||||
required["apply_patch_tool_type"],
|
||||
)
|
||||
self.assertEqual(metadata["shell_type"], "shell_command")
|
||||
self.assertTrue(metadata["supported_in_api"])
|
||||
self.assertIn("text", metadata["input_modalities"])
|
||||
self.assertEqual(metadata["default_reasoning_summary"], "none")
|
||||
self.assertNotIn("base_instructions", metadata)
|
||||
self.assertTrue(metadata["model_messages"]["instructions_template"])
|
||||
self.assertEqual(
|
||||
set(metadata["model_messages"]),
|
||||
{
|
||||
"instructions_template",
|
||||
"instructions_variables",
|
||||
"approvals",
|
||||
"collaboration_modes",
|
||||
"auto_review",
|
||||
"permissions",
|
||||
"token_budget",
|
||||
},
|
||||
)
|
||||
self.assertNotIn("node_repl_auto_review_required", metadata)
|
||||
self.assertNotIn("node_repl_disabled", metadata)
|
||||
self.assertEqual(
|
||||
metadata["supports_reasoning_summary_parameter"],
|
||||
required["default_reasoning_level"] is not None
|
||||
and agent_id != "routine_engineer",
|
||||
)
|
||||
self.assertNotIn("used_fallback_model_metadata", metadata)
|
||||
self.assertIn("truncation_policy", metadata)
|
||||
finally:
|
||||
finish_session(session["session_id"], exit_code=0)
|
||||
|
||||
def test_function_only_model_omits_custom_patch_but_keeps_shell(self) -> None:
|
||||
with RuntimeSandbox() as box:
|
||||
profile = box.root / "function-only-model"
|
||||
shutil.copytree(ROOT / "profiles" / "access-efficient-escalation-lab", profile)
|
||||
profile_path = profile / "profile.toml"
|
||||
profile_data = read_toml(profile_path)
|
||||
profile_data["id"] = "function-only-model"
|
||||
profile_data["catalog"] = "catalog.toml"
|
||||
profile_path.write_text(toml_dumps(profile_data), encoding="utf-8")
|
||||
(profile / "catalog.toml").write_text(
|
||||
toml_dumps(
|
||||
{
|
||||
"schema_version": MMO_SCHEMA_VERSION,
|
||||
"models": {
|
||||
"opencode_go_openai_chat__deepseek_v4_flash": {
|
||||
"supports_custom_tools": False,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
session = create_session(profile=profile, cwd=box.workspace)
|
||||
try:
|
||||
home = session["homes"]["routine_engineer"]["home"]
|
||||
config = self._config(home)
|
||||
metadata = self._catalog(config)[config["model"]]
|
||||
self.assertEqual(metadata["shell_type"], "shell_command")
|
||||
self.assertIsNone(metadata["apply_patch_tool_type"])
|
||||
finally:
|
||||
finish_session(session["session_id"], exit_code=0)
|
||||
|
||||
def test_hybrid_catalog_preserves_bundled_models_and_adds_external_bindings(self) -> None:
|
||||
with RuntimeSandbox() as box:
|
||||
mmo_codex_home._BUNDLED_CODEX_CATALOGS.clear()
|
||||
profile = box.root / "hybrid-native-external"
|
||||
shutil.copytree(ROOT / "profiles" / "adaptive-engineering", profile)
|
||||
profile_path = profile / "profile.toml"
|
||||
profile_data = read_toml(profile_path)
|
||||
profile_data["id"] = "hybrid-native-external"
|
||||
profile_data["agents"]["implementation_specialist"]["backends"] = [
|
||||
"mcp",
|
||||
"native",
|
||||
]
|
||||
specialist = profile_data["agents"]["implementation_specialist"]
|
||||
specialist["permissions"] = "read-only"
|
||||
specialist["execution_mode"] = "turn"
|
||||
specialist.pop("goal_token_budget")
|
||||
specialist.pop("max_goal_token_budget")
|
||||
profile_data["coordination"]["max_active_writers"] = 0
|
||||
profile_path.write_text(toml_dumps(profile_data), encoding="utf-8")
|
||||
original = mmo_runtime.bundled_codex_catalog_for_profile
|
||||
with mock.patch(
|
||||
"mmo_runtime.bundled_codex_catalog_for_profile", wraps=original
|
||||
) as load_catalog:
|
||||
session = create_session(profile=profile, cwd=box.workspace)
|
||||
try:
|
||||
# The active Codex catalog is queried once per session, not once
|
||||
# for every generated agent home.
|
||||
load_catalog.assert_called_once()
|
||||
self.assertEqual(load_catalog.call_args.args[1], Path(session["codex_binary"]))
|
||||
root_home = session["homes"][session["root_agent"]]["home"]
|
||||
config = self._config(root_home)
|
||||
self.assertIn("model_catalog_json", config)
|
||||
entries = self._catalog(config)
|
||||
# model_catalog_json is a startup replacement, so MMO preserves
|
||||
# the exact bundled rows before adding external route bindings.
|
||||
self.assertIn("gpt-5.6-terra", entries)
|
||||
self.assertIn(config["model"], entries)
|
||||
native_dir = Path(root_home) / "agents"
|
||||
native_configs = [
|
||||
tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
for path in native_dir.glob("*.toml")
|
||||
]
|
||||
external = [
|
||||
item["model"] for item in native_configs if item["model"].startswith("mmo-")
|
||||
]
|
||||
self.assertTrue(external)
|
||||
self.assertTrue(all(slug in entries for slug in external))
|
||||
patch_types = {entries[slug]["apply_patch_tool_type"] for slug in external}
|
||||
self.assertEqual(patch_types, {"freeform"})
|
||||
finally:
|
||||
finish_session(session["session_id"], exit_code=0)
|
||||
|
||||
def test_builtin_only_process_uses_codex_native_catalog(self) -> None:
|
||||
with RuntimeSandbox() as box:
|
||||
session = create_session(profile="codex-harness-team", cwd=box.workspace)
|
||||
try:
|
||||
config = self._config(session["homes"][session["root_agent"]]["home"])
|
||||
self.assertNotIn("model_catalog_json", config)
|
||||
finally:
|
||||
finish_session(session["session_id"], exit_code=0)
|
||||
|
||||
def test_interactive_launch_preserves_tty_with_redirected_stderr(self) -> None:
|
||||
with RuntimeSandbox() as box:
|
||||
old = {key: os.environ.get(key) for key in ("NO_COLOR", "TERM", "FAKE_TUI_PROBE")}
|
||||
os.environ.pop("NO_COLOR", None)
|
||||
os.environ["TERM"] = "xterm-256color"
|
||||
os.environ["FAKE_TUI_PROBE"] = "1"
|
||||
try:
|
||||
master, slave = pty.openpty()
|
||||
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
|
||||
environment = {
|
||||
**os.environ,
|
||||
"PYTHONPATH": os.pathsep.join([str(ROOT / "libexec"), str(ROOT / "tests")]),
|
||||
}
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
str(ROOT / "tests" / "helpers" / "tui_probe_launcher.py"),
|
||||
"access-efficient-escalation-lab",
|
||||
str(box.workspace),
|
||||
],
|
||||
stdin=slave,
|
||||
stdout=slave,
|
||||
stderr=subprocess.PIPE,
|
||||
env=environment,
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
)
|
||||
os.close(slave)
|
||||
buffer = b""
|
||||
all_output = b""
|
||||
records: list[dict] = []
|
||||
resized = False
|
||||
deadline = time.monotonic() + 30
|
||||
while time.monotonic() < deadline:
|
||||
readable, _, _ = select.select([master], [], [], 0.25)
|
||||
if readable:
|
||||
try:
|
||||
chunk = os.read(master, 65536)
|
||||
except OSError:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
all_output += chunk
|
||||
buffer += chunk
|
||||
while b"\n" in buffer:
|
||||
raw, buffer = buffer.split(b"\n", 1)
|
||||
text = raw.decode("utf-8", errors="replace").strip().strip("\r")
|
||||
if not text.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
record = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
records.append(record)
|
||||
if record.get("event") == "ready" and not resized:
|
||||
fcntl.ioctl(
|
||||
master,
|
||||
termios.TIOCSWINSZ,
|
||||
struct.pack("HHHH", 42, 132, 0, 0),
|
||||
)
|
||||
resized = True
|
||||
if any(
|
||||
item.get("event") in {"launcher_returned", "launcher_error"}
|
||||
for item in records
|
||||
):
|
||||
break
|
||||
_unused_stdout, stderr_output = process.communicate(timeout=10)
|
||||
exit_code = int(process.returncode or 0)
|
||||
os.close(master)
|
||||
diagnostic = {
|
||||
"records": records,
|
||||
"output": all_output.decode("utf-8", errors="replace"),
|
||||
"stderr": stderr_output.decode("utf-8", errors="replace"),
|
||||
}
|
||||
self.assertEqual(exit_code, 0, diagnostic)
|
||||
self.assertIn(b"\x1b[32mFAKE_TUI_COLOR\x1b[0m", all_output, diagnostic)
|
||||
ready = next((item for item in records if item.get("event") == "ready"), None)
|
||||
resize = next((item for item in records if item.get("event") == "resized"), None)
|
||||
returned = next(
|
||||
(item for item in records if item.get("event") == "launcher_returned"),
|
||||
None,
|
||||
)
|
||||
self.assertIsNotNone(ready, diagnostic)
|
||||
self.assertIsNotNone(resize, diagnostic)
|
||||
self.assertIsNotNone(returned, diagnostic)
|
||||
assert ready is not None and resize is not None and returned is not None
|
||||
self.assertTrue(ready["stdin_isatty"])
|
||||
self.assertTrue(ready["stdout_isatty"])
|
||||
self.assertFalse(ready["stderr_isatty"])
|
||||
self.assertIsNone(ready["no_color"])
|
||||
self.assertEqual(ready["term"], "xterm-256color")
|
||||
self.assertEqual(ready["pgrp"], ready["foreground_pgrp"])
|
||||
self.assertEqual((resize["columns"], resize["lines"]), (132, 42))
|
||||
self.assertEqual(resize["pgrp"], resize["foreground_pgrp"])
|
||||
self.assertEqual(returned["exit_code"], 0)
|
||||
self.assertTrue(returned["echo_enabled"], diagnostic)
|
||||
self.assertTrue(returned["attributes_restored"], diagnostic)
|
||||
self.assertEqual(returned["pgrp"], returned["foreground_pgrp"])
|
||||
finally:
|
||||
for key, value in old.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user