mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
Addresses 1 High + 4 Medium findings from the PR-G1 code review. H1: `_inject_continue_rtk_systemmessage` previously fell through to an unconditional `data["systemMessage"] = RTK_INSTRUCTIONS_BLOCK` when the existing value was non-string (dict / list / number), silently clobbering user data despite a docstring promising otherwise. Extracted a small helper `_apply_rtk_to_systemmessage_field` that returns `(changed, ok)` and refuses loudly on non-string user data with guidance to clear the field before re-running. The injecting helper reports `ok=False` on any refusal so the caller surfaces it as a warning instead of pretending the injection succeeded. Tests cover dict, list, and int values for both top-level and per-model sites. M2: Continue overrides top-level `systemMessage` with per-model `systemMessage` when set, so users with per-model configs were silently getting no RTK guidance. The helper now visits every `models[i]` dict in addition to the top-level field, applying the same idempotency and non- string-clobber rules at each site. Non-dict entries in `models[]` are skipped. M3: The openhands subcommand previously called `_ensure_rtk_binary()` and ignored the result, then proceeded to inject `OPENHANDS_INSTRUCTIONS` even when rtk install had failed. Mirrored the cline/continue/goose pattern — if rtk install fails (and `--no-context-tool` was not passed), exit 1 with a clear error explaining how to install rtk manually or skip rtk. No silent fallback to env-only injection. M4: Wrapped the marker-injection + rtk-setup prelude of all four new subcommands (cline, continue, goose, openhands) in a try/except for KeyboardInterrupt. On Ctrl-C between marker injection and proxy startup, we emit a clear "wrap was interrupted; marker file at <path> is on disk; rerun to retry — it's idempotent" message and exit 130. Pre-compute the marker path so the message can name it even if the interrupt fires before `_inject_rtk_instructions` returns. Introduces a small `_emit_wrap_ interrupted` helper. M1 + M5: Documented the uninstall procedure (hand-remove the `<!-- headroom:rtk-instructions -->` block) and the lean-ctx agent-name caveat in each of the four new subcommand docstrings. We chose docstring guidance over `unwrap cline|continue|goose|openhands` subcommands to keep the PR scoped. Also documented Continue's modern YAML-first config in the `continue` docstring so users on the YAML schema know this command only handles the JSON variant. Tests: +9 new tests across the 4 wrap test files exercising H1 refusal (dict/list/int parametrized × top-level + per-model), M2 per-model injection + idempotency + non-dict-entry skip, M3 rtk install failure abort + `--no-context-tool` bypass, and M4 KeyboardInterrupt-during- prelude flows for all four agents. Cosmetic: Removed the misleading "re-invocation in the same shell session" comment from openhands; the marker guard is for pre-existing env vars.
258 lines
9.7 KiB
Python
258 lines
9.7 KiB
Python
"""Tests for `headroom wrap openhands` command (PR-G1, Phase G)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from headroom.cli import wrap as wrap_mod
|
|
from headroom.cli.main import main
|
|
|
|
|
|
@pytest.fixture
|
|
def runner() -> CliRunner:
|
|
return CliRunner()
|
|
|
|
|
|
def test_wrap_openhands_sets_provider_envs(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""OPENAI_BASE_URL, ANTHROPIC_BASE_URL, LLM_BASE_URL are set on launch."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
captured.update(kwargs)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(
|
|
main, ["wrap", "openhands", "--port", "9000", "--", "--task", "demo"]
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
env = captured["env"]
|
|
assert isinstance(env, dict)
|
|
assert env["OPENAI_BASE_URL"] == "http://127.0.0.1:9000/v1"
|
|
assert env["OPENAI_API_BASE"] == "http://127.0.0.1:9000/v1"
|
|
assert env["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9000"
|
|
assert env["LLM_BASE_URL"] == "http://127.0.0.1:9000/v1"
|
|
assert captured["tool_label"] == "OPENHANDS"
|
|
assert captured["agent_type"] == "openhands"
|
|
assert captured["args"] == ("--task", "demo")
|
|
|
|
|
|
def test_wrap_openhands_injects_rtk_via_env_var(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""OPENHANDS_INSTRUCTIONS env var must contain the RTK block at launch."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
monkeypatch.delenv("OPENHANDS_INSTRUCTIONS", raising=False)
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
captured.update(kwargs)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(main, ["wrap", "openhands"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
env = captured["env"]
|
|
assert isinstance(env, dict)
|
|
instructions = env.get("OPENHANDS_INSTRUCTIONS", "")
|
|
assert wrap_mod._RTK_MARKER in instructions
|
|
assert "RTK (Rust Token Killer)" in instructions
|
|
|
|
|
|
def test_wrap_openhands_preserves_existing_openhands_instructions(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Pre-existing OPENHANDS_INSTRUCTIONS env content is preserved, rtk is appended."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
monkeypatch.setenv("OPENHANDS_INSTRUCTIONS", "Prefer typed Python.")
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
captured.update(kwargs)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(main, ["wrap", "openhands"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
env = captured["env"]
|
|
instructions = env.get("OPENHANDS_INSTRUCTIONS", "")
|
|
assert "Prefer typed Python." in instructions
|
|
assert wrap_mod._RTK_MARKER in instructions
|
|
|
|
|
|
def test_wrap_openhands_idempotent_already_injected(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""If OPENHANDS_INSTRUCTIONS already contains the marker, do not re-append."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
pre_existing = "Prefer typed Python.\n\n" + wrap_mod.RTK_INSTRUCTIONS_BLOCK
|
|
monkeypatch.setenv("OPENHANDS_INSTRUCTIONS", pre_existing)
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
captured.update(kwargs)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(main, ["wrap", "openhands"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
env = captured["env"]
|
|
instructions = env.get("OPENHANDS_INSTRUCTIONS", "")
|
|
assert instructions.count(wrap_mod._RTK_MARKER) == 1
|
|
|
|
|
|
def test_wrap_openhands_missing_binary_errors_clearly(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""If the openhands binary is missing the command must fail with a clear error."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value=None):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(main, ["wrap", "openhands"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "'openhands' not found in PATH" in result.output
|
|
|
|
|
|
def test_wrap_openhands_no_context_tool_does_not_inject(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""--no-context-tool must skip OPENHANDS_INSTRUCTIONS injection."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("OPENHANDS_INSTRUCTIONS", raising=False)
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
captured.update(kwargs)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary") as ensure:
|
|
result = runner.invoke(main, ["wrap", "openhands", "--no-context-tool"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
ensure.assert_not_called()
|
|
env = captured["env"]
|
|
assert isinstance(env, dict)
|
|
assert "OPENHANDS_INSTRUCTIONS" not in env or env["OPENHANDS_INSTRUCTIONS"] == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# M3: rtk install failure must fail loudly — no silent fallback to env
|
|
# injection without rtk on disk.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_wrap_openhands_rtk_install_failure_aborts_loudly(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""If rtk install fails, command must exit non-zero with a clear error."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
monkeypatch.delenv("OPENHANDS_INSTRUCTIONS", raising=False)
|
|
|
|
launch_called: list[bool] = []
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
launch_called.append(True)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=None):
|
|
result = runner.invoke(main, ["wrap", "openhands"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "rtk install failed" in result.output
|
|
assert "--no-context-tool" in result.output
|
|
# _launch_tool must NOT have been invoked when rtk install fails.
|
|
assert launch_called == []
|
|
|
|
|
|
def test_wrap_openhands_rtk_install_failure_with_no_context_tool_still_launches(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""--no-context-tool bypasses rtk entirely — should still launch."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("OPENHANDS_INSTRUCTIONS", raising=False)
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_launch_tool(**kwargs): # noqa: ANN003
|
|
captured.update(kwargs)
|
|
|
|
with patch.object(wrap_mod.shutil, "which", return_value="openhands"):
|
|
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=None) as ensure:
|
|
result = runner.invoke(main, ["wrap", "openhands", "--no-context-tool"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
# rtk should never have been queried.
|
|
ensure.assert_not_called()
|
|
env = captured["env"]
|
|
assert "OPENHANDS_INSTRUCTIONS" not in env or env["OPENHANDS_INSTRUCTIONS"] == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# M4: Ctrl-C during prelude emits a clear "no on-disk changes" message.
|
|
# openhands never writes to disk (env-var injection only).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_wrap_openhands_keyboardinterrupt_during_prelude_emits_clear_message(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Ctrl-C during the prelude must signal cleanly with no on-disk artifact."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
monkeypatch.delenv("OPENHANDS_INSTRUCTIONS", raising=False)
|
|
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", side_effect=KeyboardInterrupt):
|
|
result = runner.invoke(main, ["wrap", "openhands"])
|
|
|
|
assert result.exit_code == 130
|
|
assert "interrupted" in result.output.lower()
|
|
assert "idempotent" in result.output.lower()
|