"""Unit tests for the ``omni opencode`` launcher helpers (``opencode_native.py``). Covers the pure spec/payload/tmux helpers plus the httpx-backed session and terminal helpers over a fake ``AsyncClient`` — the daemon/tmux attach plumbing itself stays for the live host e2e. """ from __future__ import annotations from pathlib import Path from typing import Any import click import httpx import pytest import yaml from omnigent.opencode_native import ( LaunchedOpenCodeTerminal, PreparedOpenCodeTerminal, _create_opencode_session, _direct_tmux_unavailable_reason, _ensure_opencode_terminal_on_runner, _fetch_opencode_session, _find_running_opencode_terminal, _launched_opencode_terminal_from_payload, _materialize_opencode_agent_spec, _resolve_session_id_for_resume, opencode_terminal_resource_id, ) class _FakeClient: """Async httpx stand-in returning one preset response per call.""" def __init__(self, response: httpx.Response) -> None: self._response = response self.requests: list[tuple[str, str, dict[str, Any]]] = [] async def post(self, url: str, **kwargs: Any) -> httpx.Response: self.requests.append(("POST", url, kwargs)) return self._response async def get(self, url: str, **kwargs: Any) -> httpx.Response: self.requests.append(("GET", url, kwargs)) return self._response # ── _materialize_opencode_agent_spec ──────────────────────────────────────── def test_materialize_spec_defaults_no_model(tmp_path: Path, monkeypatch) -> None: # Pin the host shells so the declared terminals are deterministic. monkeypatch.setattr("shutil.which", lambda name: f"/usr/bin/{name}") monkeypatch.setenv("SHELL", "/bin/zsh") spec = yaml.safe_load(_materialize_opencode_agent_spec(tmp_path).read_text()) assert spec["executor"] == {"harness": "opencode-native"} assert spec["spawn"] is True # One terminal per installed shell, $SHELL first — a non-empty block gates # the relay's sys_terminal_* advertisement. assert list(spec["terminals"]) == ["zsh", "bash", "fish"] assert spec["terminals"]["zsh"]["command"] == "zsh" def test_materialize_spec_pins_model(tmp_path: Path) -> None: spec = yaml.safe_load( _materialize_opencode_agent_spec(tmp_path, model="anthropic/claude-opus-4").read_text() ) assert spec["executor"] == {"harness": "opencode-native", "model": "anthropic/claude-opus-4"} def test_terminal_resource_id_is_deterministic() -> None: assert opencode_terminal_resource_id() == opencode_terminal_resource_id() # ── _launched_opencode_terminal_from_payload ──────────────────────────────── def test_launched_terminal_parses_tmux_metadata() -> None: launched = _launched_opencode_terminal_from_payload( {"id": "term_1", "metadata": {"tmux_socket": "/tmp/s.sock", "tmux_target": "sess:0.0"}} ) assert launched.terminal_id == "term_1" assert launched.tmux_socket == Path("/tmp/s.sock") assert launched.tmux_target == "sess:0.0" def test_launched_terminal_without_metadata_has_no_tmux() -> None: launched = _launched_opencode_terminal_from_payload({"id": "term_1"}) assert launched.tmux_socket is None and launched.tmux_target is None def test_launched_terminal_missing_id_raises() -> None: with pytest.raises(click.ClickException): _launched_opencode_terminal_from_payload({"metadata": {}}) with pytest.raises(click.ClickException): _launched_opencode_terminal_from_payload("not-a-dict") # ── _direct_tmux_unavailable_reason ───────────────────────────────────────── def _prepared(socket: Path | None, target: str | None) -> PreparedOpenCodeTerminal: return PreparedOpenCodeTerminal( session_id="conv_1", terminal_id="term_1", tmux_socket=socket, tmux_target=target, reattached=False, ) def test_tmux_reason_missing_socket() -> None: assert "tmux socket" in (_direct_tmux_unavailable_reason(_prepared(None, "t")) or "") def test_tmux_reason_missing_target() -> None: assert "tmux target" in (_direct_tmux_unavailable_reason(_prepared(Path("/x"), None)) or "") def test_tmux_reason_socket_not_reachable(tmp_path: Path) -> None: reason = _direct_tmux_unavailable_reason(_prepared(tmp_path / "missing.sock", "t")) assert reason is not None and "not reachable" in reason # ── _resolve_session_id_for_resume ────────────────────────────────────────── def test_resolve_session_id_passthrough() -> None: assert ( _resolve_session_id_for_resume( base_url="http://x", headers={}, session_id="conv_9", resume_picker=False ) == "conv_9" ) def test_resolve_session_id_none_without_picker() -> None: assert ( _resolve_session_id_for_resume( base_url="http://x", headers={}, session_id=None, resume_picker=False ) is None ) # ── httpx-backed session/terminal helpers ─────────────────────────────────── async def test_create_session_returns_id() -> None: client = _FakeClient(httpx.Response(200, json={"session_id": "conv_new"})) sid = await _create_opencode_session(client, b"bundle", terminal_launch_args=["--foo"]) # type: ignore[arg-type] assert sid == "conv_new" assert client.requests[0][1] == "/v1/sessions" async def test_create_session_errors_on_http_failure() -> None: client = _FakeClient(httpx.Response(500, json={"error": "boom"})) with pytest.raises(click.ClickException): await _create_opencode_session(client, b"bundle") # type: ignore[arg-type] async def test_create_session_errors_without_session_id() -> None: client = _FakeClient(httpx.Response(200, json={})) with pytest.raises(click.ClickException): await _create_opencode_session(client, b"bundle") # type: ignore[arg-type] async def test_fetch_session_returns_payload() -> None: client = _FakeClient(httpx.Response(200, json={"id": "conv_1", "title": "t"})) assert (await _fetch_opencode_session(client, "conv_1"))["id"] == "conv_1" # type: ignore[arg-type] async def test_fetch_session_404_raises() -> None: client = _FakeClient(httpx.Response(404, json={"error": "nope"})) with pytest.raises(click.ClickException): await _fetch_opencode_session(client, "conv_1") # type: ignore[arg-type] async def test_ensure_terminal_ok_then_error() -> None: ok = _FakeClient(httpx.Response(200, json={})) await _ensure_opencode_terminal_on_runner(ok, "conv_1") # type: ignore[arg-type] assert ok.requests[0][0] == "POST" bad = _FakeClient(httpx.Response(503, json={"error": "x"})) with pytest.raises(click.ClickException): await _ensure_opencode_terminal_on_runner(bad, "conv_1") # type: ignore[arg-type] async def test_find_terminal_404_returns_none() -> None: client = _FakeClient(httpx.Response(404, json={})) assert await _find_running_opencode_terminal(client, "conv_1") is None # type: ignore[arg-type] async def test_find_terminal_not_running_returns_none() -> None: client = _FakeClient( httpx.Response(200, json={"id": "term_1", "metadata": {"running": False}}) ) assert await _find_running_opencode_terminal(client, "conv_1") is None # type: ignore[arg-type] async def test_find_terminal_returns_launched() -> None: client = _FakeClient( httpx.Response(200, json={"id": "term_1", "metadata": {"tmux_target": "s:0.0"}}) ) launched = await _find_running_opencode_terminal(client, "conv_1") # type: ignore[arg-type] assert isinstance(launched, LaunchedOpenCodeTerminal) assert launched.tmux_target == "s:0.0" async def test_find_terminal_offline_runner_returns_none() -> None: client = _FakeClient(httpx.Response(409, text="session not bound to a runner")) assert await _find_running_opencode_terminal(client, "conv_1") is None # type: ignore[arg-type] # ── launcher local-preflight / progress / tmux-reason / wait helpers ───────── def test_preflight_local_tools_ok(monkeypatch: pytest.MonkeyPatch) -> None: import omnigent.opencode_native as on monkeypatch.setattr(on.shutil, "which", lambda _x: "/usr/bin/tmux") on._preflight_local_tools() # tmux present → no raise def test_preflight_local_tools_missing_tmux_raises(monkeypatch: pytest.MonkeyPatch) -> None: import omnigent.opencode_native as on monkeypatch.setattr(on.shutil, "which", lambda _x: None) with pytest.raises(click.ClickException): on._preflight_local_tools() def test_update_startup_progress_handles_none_and_active() -> None: from unittest.mock import Mock from omnigent.opencode_native import _update_startup_progress _update_startup_progress(None, "boot") # no renderer → no-op branch _update_startup_progress(Mock(), "boot") # active renderer → update branch def test_tmux_reason_tmux_not_on_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: import omnigent.opencode_native as on sock = tmp_path / "s.sock" sock.write_text("") monkeypatch.setattr(on.shutil, "which", lambda _x: None) reason = on._direct_tmux_unavailable_reason(_prepared(sock, "t")) assert reason is not None and "tmux is not available" in reason def test_tmux_reason_none_when_socket_and_tmux_present( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: import omnigent.opencode_native as on sock = tmp_path / "s.sock" sock.write_text("") monkeypatch.setattr(on.shutil, "which", lambda _x: "/usr/bin/tmux") assert on._direct_tmux_unavailable_reason(_prepared(sock, "t")) is None async def test_wait_for_terminal_returns_when_found(monkeypatch: pytest.MonkeyPatch) -> None: import omnigent.opencode_native as on term = on.LaunchedOpenCodeTerminal(terminal_id="t", tmux_socket=None, tmux_target=None) async def _fake_find(_client: object, _sid: str) -> on.LaunchedOpenCodeTerminal: return term monkeypatch.setattr(on, "_find_running_opencode_terminal", _fake_find) got = await on._wait_for_opencode_terminal_ready(object(), "conv_1", timeout_s=5) # type: ignore[arg-type] assert got is term async def test_wait_for_terminal_times_out(monkeypatch: pytest.MonkeyPatch) -> None: import omnigent.opencode_native as on async def _never(_client: object, _sid: str) -> None: return None monkeypatch.setattr(on, "_find_running_opencode_terminal", _never) with pytest.raises(click.ClickException): await on._wait_for_opencode_terminal_ready(object(), "conv_1", timeout_s=0) # type: ignore[arg-type] # --- Resume workspace alignment (launch.json record + cwd realign) --- def test_record_launch_for_fresh_session_persists_current_cwd( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ A fresh session records its launch cwd for later resumes. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace and state root. :returns: None. """ import omnigent.opencode_native as on from omnigent.opencode_native_state import read_launch_state workspace = tmp_path / "workspace" workspace.mkdir() monkeypatch.chdir(workspace) monkeypatch.setenv("OMNIGENT_OPENCODE_NATIVE_STATE_DIR", str(tmp_path / "state")) on._record_launch_for_fresh_session("conv_abc") state = read_launch_state("conv_abc") assert state is not None assert state.working_directory == str(workspace.resolve()) def test_record_launch_for_fresh_session_swallows_oserror( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ A failed launch-state write warns rather than breaking the launch. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace. :returns: None. """ import omnigent.opencode_native as on monkeypatch.chdir(tmp_path) def _raise(*_args: object, **_kwargs: object) -> None: raise OSError("disk full") monkeypatch.setattr(on, "write_launch_state", _raise) # Best-effort recording: a write failure must not propagate. on._record_launch_for_fresh_session("conv_abc") def test_align_no_recorded_state_is_noop( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ Resume with no recorded launch state neither prompts nor moves cwd. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace and state root. :returns: None. """ import omnigent.opencode_native as on monkeypatch.chdir(tmp_path) monkeypatch.setenv("OMNIGENT_OPENCODE_NATIVE_STATE_DIR", str(tmp_path / "state")) def _fail_prompt(**_kwargs: object) -> str: raise AssertionError("absent launch state should not prompt") monkeypatch.setattr(on, "_prompt_opencode_resume_workspace_action", _fail_prompt) on._align_working_directory_with_session("conv_missing") assert Path.cwd().resolve() == tmp_path.resolve() def test_align_matching_cwd_is_noop( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ Resume from the recorded cwd must not prompt or move cwd. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace and state root. :returns: None. """ import omnigent.opencode_native as on from omnigent.opencode_native_state import write_launch_state monkeypatch.chdir(tmp_path) monkeypatch.setenv("OMNIGENT_OPENCODE_NATIVE_STATE_DIR", str(tmp_path / "state")) write_launch_state("conv_abc", str(tmp_path.resolve())) def _fail_prompt(**_kwargs: object) -> str: raise AssertionError("matching cwd should not prompt") monkeypatch.setattr(on, "_prompt_opencode_resume_workspace_action", _fail_prompt) on._align_working_directory_with_session("conv_abc") assert Path.cwd().resolve() == tmp_path.resolve() def test_align_switches_to_recorded_cwd( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ Choosing ``switch`` changes cwd to the recorded directory. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace and state root. :returns: None. """ import omnigent.opencode_native as on from omnigent.opencode_native_state import write_launch_state recorded = tmp_path / "recorded" current = tmp_path / "current" recorded.mkdir() current.mkdir() monkeypatch.chdir(current) monkeypatch.setenv("OMNIGENT_OPENCODE_NATIVE_STATE_DIR", str(tmp_path / "state")) write_launch_state("conv_abc", str(recorded.resolve())) monkeypatch.setattr( on, "_prompt_opencode_resume_workspace_action", lambda **_kwargs: "switch", ) on._align_working_directory_with_session("conv_abc") assert Path.cwd().resolve() == recorded.resolve() def test_align_cancel_raises( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ Choosing ``cancel`` aborts the resume without moving cwd. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace and state root. :returns: None. """ import omnigent.opencode_native as on from omnigent.opencode_native_state import write_launch_state recorded = tmp_path / "recorded" current = tmp_path / "current" recorded.mkdir() current.mkdir() monkeypatch.chdir(current) monkeypatch.setenv("OMNIGENT_OPENCODE_NATIVE_STATE_DIR", str(tmp_path / "state")) write_launch_state("conv_abc", str(recorded.resolve())) monkeypatch.setattr( on, "_prompt_opencode_resume_workspace_action", lambda **_kwargs: "cancel", ) with pytest.raises(click.ClickException) as excinfo: on._align_working_directory_with_session("conv_abc") assert "cancel" in excinfo.value.message.lower() assert Path.cwd().resolve() == current.resolve() def test_align_missing_recorded_cwd_raises( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ A recorded-but-missing cwd fails loud instead of resuming wrong. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace and state root. :returns: None. """ import omnigent.opencode_native as on from omnigent.opencode_native_state import write_launch_state current = tmp_path / "current" missing = tmp_path / "missing" current.mkdir() monkeypatch.chdir(current) monkeypatch.setenv("OMNIGENT_OPENCODE_NATIVE_STATE_DIR", str(tmp_path / "state")) write_launch_state("conv_abc", str(missing)) def _fail_prompt(**_kwargs: object) -> str: raise AssertionError("missing recorded dir should raise before prompting") monkeypatch.setattr(on, "_prompt_opencode_resume_workspace_action", _fail_prompt) with pytest.raises(click.ClickException) as excinfo: on._align_working_directory_with_session("conv_abc") assert "conv_abc" in excinfo.value.message assert str(missing.resolve()) in excinfo.value.message def test_prompt_offers_switch_and_cancel_choices( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ The cwd-mismatch prompt offers ``switch``/``cancel`` and defaults to switch. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary recorded/current paths. :returns: None. """ import omnigent.opencode_native as on captured: dict[str, Any] = {} def _fake_prompt(text: str, **kwargs: Any) -> str: captured["text"] = text captured["kwargs"] = kwargs return "switch" monkeypatch.setattr(click, "prompt", _fake_prompt) result = on._prompt_opencode_resume_workspace_action( recorded_path=tmp_path / "recorded", current=tmp_path / "current", ) assert result == "switch" choice = captured["kwargs"]["type"] assert isinstance(choice, click.Choice) assert list(choice.choices) == ["switch", "cancel"] assert captured["kwargs"]["default"] == "switch" # --- _run_with_remote_server control-flow (daemon/server mocked) --- def test_run_with_remote_server_aligns_cwd_before_daemon_prepare( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ Remote resume aligns cwd before the daemon prepare samples it. Drives the real ``_run_with_remote_server`` with the daemon/server mocked: asserts ``align`` runs first and the aligned cwd is the workspace handed to prepare. :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary start/aligned dirs. :returns: None. """ import os from types import SimpleNamespace import omnigent.chat as chat_mod import omnigent.cli as cli_mod import omnigent.host.identity as identity_mod import omnigent.opencode_native as on from omnigent._runner_startup import RunnerStartupProgress start_dir = tmp_path / "start" aligned_dir = tmp_path / "aligned" start_dir.mkdir() aligned_dir.mkdir() monkeypatch.chdir(start_dir) order: list[str] = [] def fake_align(_session_id: str) -> None: order.append("align") os.chdir(aligned_dir) async def fake_prepare(**kwargs: Any) -> PreparedOpenCodeTerminal: assert kwargs["host_id"] == "host_local" assert kwargs["session_id"] == "conv_abc" assert kwargs["workspace"] == str(aligned_dir.resolve()) assert isinstance(kwargs["startup_progress"], RunnerStartupProgress) order.append("prepare") return PreparedOpenCodeTerminal( session_id="conv_abc", terminal_id="term_main", tmux_socket=None, tmux_target=None, reattached=True, ) async def fake_attach(_prepared: object) -> None: order.append("attach") monkeypatch.setattr(chat_mod, "_remote_headers", lambda *_a, **_k: {}) monkeypatch.setattr( cli_mod, "_ensure_host_daemon", lambda *_a, **_k: order.append("ensure-daemon") ) monkeypatch.setattr( identity_mod, "load_or_create_host_identity", lambda: SimpleNamespace(host_id="host_local"), ) monkeypatch.setattr(on, "_resolve_session_id_for_resume", lambda **_k: "conv_abc") monkeypatch.setattr(on, "_align_working_directory_with_session", fake_align) monkeypatch.setattr(on, "_prepare_opencode_terminal_via_daemon", fake_prepare) monkeypatch.setattr(on, "_attach_terminal_resource", fake_attach) monkeypatch.setattr(on, "open_conversation_link_if_enabled", lambda **_k: None) on._run_with_remote_server( "http://server", tmp_path / "spec.yaml", session_id="conv_abc", resume_picker=False, opencode_args=(), ) assert order == ["align", "ensure-daemon", "prepare", "attach"] def test_run_with_remote_server_records_launch_after_create( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: """ A fresh remote session records its launch cwd after prepare (no align). :param monkeypatch: Pytest monkeypatch fixture. :param tmp_path: Temporary workspace. :returns: None. """ from types import SimpleNamespace import omnigent.chat as chat_mod import omnigent.cli as cli_mod import omnigent.host.identity as identity_mod import omnigent.opencode_native as on monkeypatch.chdir(tmp_path) order: list[str] = [] async def fake_prepare(**_k: Any) -> PreparedOpenCodeTerminal: order.append("prepare") return PreparedOpenCodeTerminal( session_id="conv_new", terminal_id="term_main", tmux_socket=None, tmux_target=None, reattached=False, ) async def fake_attach(_prepared: object) -> None: order.append("attach") def fake_record(session_id: str) -> None: assert session_id == "conv_new" order.append("record") def fail_align(_session_id: str) -> None: raise AssertionError("create path must not align cwd") monkeypatch.setattr(chat_mod, "_remote_headers", lambda *_a, **_k: {}) monkeypatch.setattr(chat_mod, "_bundle_agent", lambda _spec: b"bundle") monkeypatch.setattr( cli_mod, "_ensure_host_daemon", lambda *_a, **_k: order.append("ensure-daemon") ) monkeypatch.setattr( identity_mod, "load_or_create_host_identity", lambda: SimpleNamespace(host_id="host_local"), ) monkeypatch.setattr(on, "_resolve_session_id_for_resume", lambda **_k: None) monkeypatch.setattr(on, "_align_working_directory_with_session", fail_align) monkeypatch.setattr(on, "_prepare_opencode_terminal_via_daemon", fake_prepare) monkeypatch.setattr(on, "_attach_terminal_resource", fake_attach) monkeypatch.setattr(on, "_record_launch_for_fresh_session", fake_record) monkeypatch.setattr(on, "open_conversation_link_if_enabled", lambda **_k: None) monkeypatch.setattr(on, "echo_native_resume_hint", lambda **_k: None) on._run_with_remote_server( "http://server", tmp_path / "spec.yaml", session_id=None, resume_picker=False, opencode_args=(), ) assert order == ["ensure-daemon", "prepare", "record", "attach"]