mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Adds four new `headroom wrap <agent>` subcommands so the proxy can front-end Cline (VS Code), Continue (VS Code/JetBrains), Goose (Block CLI), and OpenHands (CLI), extending the existing claude/codex/aider/copilot/ cursor pattern. Phase G PR-G1 of the realignment work. Architectural decision: extended the existing `headroom/cli/wrap.py` module in-place rather than splitting it into a `headroom/cli/wrap/` package. The spec at REALIGNMENT/09-phase-G-rtk-observability.md mentions per-agent files under a package, but the existing five wrap subcommands all live in the single module and the extension is small relative to the file. Keeping the file together preserves the simple import surface used by tests (`from headroom.cli import wrap as wrap_mod`). Per-agent wiring: - cline → injects RTK block into `.clinerules` at project root (Cline is a VS Code extension; API base URL is configured in the UI, so the command prints config instructions and blocks on the proxy). - continue → injects RTK guidance into the `systemMessage` field of `.continue/config.json` (idempotent; refuses malformed JSON or non-object roots; supports `--config` for custom paths). - goose → injects RTK block into `.goosehints` at project root and launches the goose CLI with OPENAI_BASE_URL / OPENAI_API_BASE / ANTHROPIC_BASE_URL env vars pointed at the proxy. - openhands → injects RTK guidance via the `OPENHANDS_INSTRUCTIONS` env var at launch (no on-disk artifact) and sets OPENAI_BASE_URL / ANTHROPIC_BASE_URL / LLM_BASE_URL. Preserves any pre-existing OPENHANDS_INSTRUCTIONS content. Tests: - tests/test_cli/test_wrap_cline.py: 4 tests covering prepare-only injection, idempotence, --no-context-tool, and existing content preservation. - tests/test_cli/test_wrap_continue.py: 8 tests covering the new `_inject_continue_rtk_systemmessage` helper (new-file, existing keys, idempotence, malformed JSON, non-object roots) and the click command surface (default path, custom --config). - tests/test_cli/test_wrap_goose.py: 5 tests covering env-var wiring, `.goosehints` injection, idempotence, missing-binary error, and --no-context-tool. - tests/test_cli/test_wrap_openhands.py: 6 tests covering env-var wiring, OPENHANDS_INSTRUCTIONS injection, preservation of existing instructions, idempotence, missing-binary error, and --no-context-tool. E2E: extended `e2e/wrap/run.py` with `--prepare-only` smoke tests for all four new wrappers (full launches require agent CLIs not present in the e2e image; unit tests cover the env-var wiring).
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
"""Tests for `headroom wrap cline` 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_cline_prepare_only_injects_rtk_into_clinerules(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""`wrap cline --prepare-only` writes RTK guidance to .clinerules at cwd."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(main, ["wrap", "cline", "--prepare-only"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
clinerules = tmp_path / ".clinerules"
|
|
assert clinerules.exists(), ".clinerules should be created"
|
|
content = clinerules.read_text()
|
|
assert wrap_mod._RTK_MARKER in content
|
|
assert "RTK (Rust Token Killer)" in content
|
|
|
|
|
|
def test_wrap_cline_prepare_only_idempotent_no_duplicate_block(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Running wrap cline twice must not duplicate the RTK block."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
runner.invoke(main, ["wrap", "cline", "--prepare-only"])
|
|
runner.invoke(main, ["wrap", "cline", "--prepare-only"])
|
|
|
|
clinerules = tmp_path / ".clinerules"
|
|
content = clinerules.read_text()
|
|
assert content.count(wrap_mod._RTK_MARKER) == 1
|
|
|
|
|
|
def test_wrap_cline_no_context_tool_does_not_create_clinerules(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""--no-context-tool must not create .clinerules."""
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
# Patch RTK to fail if accidentally called.
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary") as ensure:
|
|
result = runner.invoke(main, ["wrap", "cline", "--prepare-only", "--no-context-tool"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert not (tmp_path / ".clinerules").exists()
|
|
ensure.assert_not_called()
|
|
|
|
|
|
def test_wrap_cline_preserves_existing_clinerules_content(
|
|
runner: CliRunner,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Pre-existing .clinerules content must be preserved when RTK is appended."""
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
|
clinerules = tmp_path / ".clinerules"
|
|
original = "# Project conventions\n\nAlways use Python 3.12.\n"
|
|
clinerules.write_text(original)
|
|
|
|
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
|
result = runner.invoke(main, ["wrap", "cline", "--prepare-only"])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
content = clinerules.read_text()
|
|
assert "Always use Python 3.12." in content
|
|
assert wrap_mod._RTK_MARKER in content
|