headroom/tests/test_cli/test_wrap_hintfile_agents.py
Tejas Chopra 44136ed042
fix(wrap): make RTK opt-in (off by default) across wrap subcommands (#2344)
## Description

RTK CLI-command filtering was set up **by default** across ~16 `wrap`
subcommands (copilot, codex, aider, cursor, cline, continue, goose,
openhands, opencode, grok, omp, openclaude, vibe, …) via `if not
no_rtk:` — so users got rtk hooks / instruction injection without opting
in. `wrap claude` was the lone exception (already gated on
`--context-tool`).

This makes RTK **opt-in (off by default)** everywhere, so Headroom's own
savings are what's measured unless a user explicitly wants rtk.

Closes #

## Type of Change
- [x] Bug fix (behavior change: default flip)

## Changes Made
- **Central gate** `_rtk_opt_in()` — RTK runs only when explicitly
enabled via `--rtk` or `HEADROOM_RTK=1`. Guards the 3 RTK entry points
(`_setup_rtk`, `_ensure_rtk_binary`, `_inject_rtk_instructions`) so they
no-op by default — one small change instead of editing ~30 call sites.
- **`--rtk` opt-in flag** on all 18 tool subcommands via a shared
eager-callback option (`expose_value=False`, sets `HEADROOM_RTK=1`; no
subcommand signature changes).
- `wrap claude`'s legacy `--context-tool` still opts in (mirrored into
the gate).
- **`--no-rtk` kept** as an accepted, now-redundant no-op (back-compat).
- lean-ctx and all non-RTK behavior untouched.

## Testing
```text
pytest tests/test_wrap_rtk_opt_in.py   -> 4 passed
ruff check / format                    -> clean
mypy headroom                          -> Success: no issues found in 504 source files
```
Verified `--rtk` appears in `wrap {claude,codex,copilot} --help`;
`_rtk_opt_in()` is False by default, True with `HEADROOM_RTK=1`; entry
points no-op + write nothing when off.

## Real Behavior Proof
- Env: local `.venv`, click CliRunner.
- Steps: import wrap; assert gate default-off / env-on; assert
`_setup_rtk`/`_ensure_rtk_binary` return None and
`_inject_rtk_instructions` returns False + writes no file when not opted
in; assert `--rtk` in subcommand help.
- Observed: all pass. Not tested: a live end-to-end wrap launch (proxy
spawn).

## Notes
Part 2 of 3 (RTK opt-in). Separate PRs cover the code-graph MCP engine
and the proxy-option cleanup. No `CHANGELOG.md` edit — release-please
generates it from the PR title (per the changelog guard).

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-17 16:47:19 -07:00

188 lines
7 KiB
Python

"""Shared hint-file agent tests for `headroom wrap {cline,goose}` (PR-G1).
Cline and Goose are different wrap patterns (cline is proxy-only watcher,
goose launches a child binary) but both inject the RTK guidance into a
*hint file* at the project root — `.clinerules` and `.goosehints` —
through the same code path (`_inject_rtk_instructions` via the shared
`_setup_context_tool_for_agent` helper).
That hint-file plumbing is the same for both, so the tests covering it
parametrize over `(agent, hint_filename)` here. Agent-specific behavior
(goose's env-var fan-out, goose's binary discovery, cline's IDE setup
print-out) lives in `test_wrap_goose.py` / `test_wrap_cline.py`
respectively — those files keep only what genuinely diverges per agent.
"""
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
# (subcommand, hint-file basename) — used by every test below.
HINTFILE_AGENTS = [
pytest.param("cline", ".clinerules", id="cline"),
pytest.param("goose", ".goosehints", id="goose"),
]
@pytest.fixture(autouse=True)
def _enable_rtk(monkeypatch: pytest.MonkeyPatch) -> None:
# RTK is opt-in (off by default); these tests exercise the RTK-on injection path.
monkeypatch.setenv("HEADROOM_RTK", "1")
@pytest.fixture
def runner() -> CliRunner:
return CliRunner()
@pytest.mark.parametrize("agent,hintfile", HINTFILE_AGENTS)
def test_prepare_only_injects_rtk_into_hintfile(
agent: str,
hintfile: str,
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`wrap <agent> --prepare-only` writes the RTK block to the hint file 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", agent, "--prepare-only"])
assert result.exit_code == 0, result.output
marker = tmp_path / hintfile
assert marker.exists(), f"{hintfile} should be created"
content = marker.read_text(encoding="utf-8")
assert wrap_mod._RTK_MARKER in content
assert "RTK (Rust Token Killer)" in content
@pytest.mark.parametrize("agent,hintfile", HINTFILE_AGENTS)
def test_prepare_only_idempotent_no_duplicate_block(
agent: str,
hintfile: str,
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Running prepare-only twice must not duplicate the RTK block in the hint file."""
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", agent, "--prepare-only"])
runner.invoke(main, ["wrap", agent, "--prepare-only"])
content = (tmp_path / hintfile).read_text(encoding="utf-8")
assert content.count(wrap_mod._RTK_MARKER) == 1
@pytest.mark.parametrize("agent,hintfile", HINTFILE_AGENTS)
def test_no_context_tool_does_not_create_hintfile(
agent: str,
hintfile: str,
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""--no-context-tool must not create the hint file and must not invoke rtk."""
monkeypatch.chdir(tmp_path)
with patch.object(wrap_mod, "_ensure_rtk_binary") as ensure:
result = runner.invoke(main, ["wrap", agent, "--prepare-only", "--no-context-tool"])
assert result.exit_code == 0, result.output
assert not (tmp_path / hintfile).exists()
ensure.assert_not_called()
@pytest.mark.parametrize("agent,hintfile", HINTFILE_AGENTS)
def test_preserves_existing_hintfile_content(
agent: str,
hintfile: str,
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Pre-existing hint-file content must be preserved when RTK is appended."""
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
marker_path = tmp_path / hintfile
original = "# Project conventions\n\nAlways use Python 3.12.\n"
marker_path.write_text(original, encoding="utf-8")
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
result = runner.invoke(main, ["wrap", agent, "--prepare-only"])
assert result.exit_code == 0, result.output
content = marker_path.read_text(encoding="utf-8")
assert "Always use Python 3.12." in content
assert wrap_mod._RTK_MARKER in content
# ---------------------------------------------------------------------------
# M4: Ctrl-C during prelude emits a clear "interrupted, marker may be on disk"
# message and exits non-zero (130, the conventional shell signal-130 code).
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("agent,hintfile", HINTFILE_AGENTS)
def test_keyboardinterrupt_during_prelude_emits_clear_message(
agent: str,
hintfile: str,
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Ctrl-C after marker injection but before proxy startup must report clearly."""
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
def raise_kbd(*args, **kwargs): # noqa: ANN002, ANN003
# Simulate the user hitting Ctrl-C right after the prelude wrote the
# hint-file marker but before _ensure_proxy returns. We trigger via
# _ensure_rtk_binary side-effect so the marker exists on disk.
marker_path = tmp_path / hintfile
marker_path.write_text(wrap_mod.RTK_INSTRUCTIONS_BLOCK, encoding="utf-8")
raise KeyboardInterrupt
with patch.object(wrap_mod, "_ensure_rtk_binary", side_effect=raise_kbd):
result = runner.invoke(main, ["wrap", agent, "--prepare-only"])
assert result.exit_code == 130
assert "interrupted" in result.output.lower()
assert "idempotent" in result.output.lower()
assert (tmp_path / hintfile).exists()
assert hintfile in result.output
@pytest.mark.parametrize("agent,hintfile", HINTFILE_AGENTS)
def test_inject_rtk_handles_utf8_content(
agent: str,
hintfile: str,
runner: CliRunner,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Existing hint files with non-ASCII UTF-8 content must not crash (#1126)."""
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
marker_path = tmp_path / hintfile
original = "# Instructions\n\nUse “smart quotes” and an em dash — here.\n"
marker_path.write_text(original, encoding="utf-8")
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
result = runner.invoke(main, ["wrap", agent, "--prepare-only"])
assert result.exit_code == 0, result.output
content = marker_path.read_text(encoding="utf-8")
assert "“smart quotes”" in content
assert wrap_mod._RTK_MARKER in content