mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
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>
This commit is contained in:
parent
1b8c11ebfb
commit
44136ed042
12 changed files with 165 additions and 5 deletions
|
|
@ -953,6 +953,9 @@ def main() -> None:
|
||||||
"PATH": f"{shim_dir}{os.pathsep}{base_env['PATH']}",
|
"PATH": f"{shim_dir}{os.pathsep}{base_env['PATH']}",
|
||||||
"HEADROOM_E2E_LOG_DIR": str(log_dir),
|
"HEADROOM_E2E_LOG_DIR": str(log_dir),
|
||||||
"OPENAI_TARGET_API_URL": "http://127.0.0.1:19001/v1",
|
"OPENAI_TARGET_API_URL": "http://127.0.0.1:19001/v1",
|
||||||
|
# RTK is opt-in (off by default). These wrap smoke tests assert
|
||||||
|
# RTK-instruction injection, so exercise the RTK-on path.
|
||||||
|
"HEADROOM_RTK": "1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -647,8 +647,42 @@ def _start_proxy(
|
||||||
stdio_log_file.close()
|
stdio_log_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _rtk_opt_in() -> bool:
|
||||||
|
"""Whether RTK CLI-command filtering was explicitly enabled.
|
||||||
|
|
||||||
|
RTK is opt-in (off by default): turn it on with ``--rtk`` (which sets
|
||||||
|
``HEADROOM_RTK=1``) or by exporting ``HEADROOM_RTK=1``. ``--no-rtk`` remains
|
||||||
|
accepted as a deprecated no-op.
|
||||||
|
"""
|
||||||
|
return os.environ.get("HEADROOM_RTK", "").strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
|
|
||||||
|
def _rtk_flag_callback(ctx: Any, param: Any, value: bool) -> bool:
|
||||||
|
"""Click eager callback: ``--rtk`` sets HEADROOM_RTK so the central RTK gate
|
||||||
|
(:func:`_rtk_opt_in`) sees the opt-in without threading a param through every
|
||||||
|
wrap subcommand."""
|
||||||
|
if value:
|
||||||
|
os.environ["HEADROOM_RTK"] = "1"
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# Shared opt-in flag applied to every ``wrap`` subcommand. ``expose_value=False``
|
||||||
|
# so no subcommand signature changes; it works purely through HEADROOM_RTK.
|
||||||
|
_rtk_option = click.option(
|
||||||
|
"--rtk",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
expose_value=False,
|
||||||
|
is_eager=True,
|
||||||
|
callback=_rtk_flag_callback,
|
||||||
|
help="Enable RTK CLI-command filtering (opt-in; off by default). Also enabled by HEADROOM_RTK=1.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _setup_rtk(verbose: bool = False) -> Path | None:
|
def _setup_rtk(verbose: bool = False) -> Path | None:
|
||||||
"""Ensure rtk is installed and hooks are registered."""
|
"""Ensure rtk is installed and hooks are registered."""
|
||||||
|
if not _rtk_opt_in():
|
||||||
|
return None
|
||||||
from headroom.rtk import get_rtk_path
|
from headroom.rtk import get_rtk_path
|
||||||
from headroom.rtk.installer import ensure_rtk, register_claude_hooks
|
from headroom.rtk.installer import ensure_rtk, register_claude_hooks
|
||||||
|
|
||||||
|
|
@ -2123,6 +2157,8 @@ def _snapshot_codex_config_if_unwrapped(config_file: Path, backup_file: Path) ->
|
||||||
|
|
||||||
def _ensure_rtk_binary(verbose: bool = False) -> Path | None:
|
def _ensure_rtk_binary(verbose: bool = False) -> Path | None:
|
||||||
"""Ensure rtk binary is installed (download if needed). No hook registration."""
|
"""Ensure rtk binary is installed (download if needed). No hook registration."""
|
||||||
|
if not _rtk_opt_in():
|
||||||
|
return None
|
||||||
from headroom.rtk import get_rtk_path
|
from headroom.rtk import get_rtk_path
|
||||||
from headroom.rtk.installer import ensure_rtk
|
from headroom.rtk.installer import ensure_rtk
|
||||||
|
|
||||||
|
|
@ -2693,6 +2729,8 @@ def _inject_rtk_instructions(file_path: Path, verbose: bool = False) -> bool:
|
||||||
Idempotent — skips if marker already present. Appends to existing content.
|
Idempotent — skips if marker already present. Appends to existing content.
|
||||||
Returns True if instructions were written.
|
Returns True if instructions were written.
|
||||||
"""
|
"""
|
||||||
|
if not _rtk_opt_in():
|
||||||
|
return False
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
existing = _read_text(file_path)
|
existing = _read_text(file_path)
|
||||||
if _RTK_MARKER in existing:
|
if _RTK_MARKER in existing:
|
||||||
|
|
@ -4396,6 +4434,7 @@ def wrap_selfheal(marker: str | None) -> None:
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
# no "-p" short alias here: claude's own -p/--print must fall through to CLAUDE_ARGS
|
# no "-p" short alias here: claude's own -p/--print must fall through to CLAUDE_ARGS
|
||||||
"--port",
|
"--port",
|
||||||
|
|
@ -4523,7 +4562,12 @@ def claude(
|
||||||
headroom wrap claude --no-serena # Never register the Serena backup
|
headroom wrap claude --no-serena # Never register the Serena backup
|
||||||
headroom wrap claude --1m # Preserve the 1M context window
|
headroom wrap claude --1m # Preserve the 1M context window
|
||||||
"""
|
"""
|
||||||
setup_context_tool = context_tool and not no_rtk
|
# RTK/context-tool is opt-in (off by default): --context-tool (legacy) and
|
||||||
|
# --rtk both enable it. Mirror --context-tool into HEADROOM_RTK so the central
|
||||||
|
# RTK gate (_rtk_opt_in) fires for the legacy flag too.
|
||||||
|
if context_tool:
|
||||||
|
os.environ["HEADROOM_RTK"] = "1"
|
||||||
|
setup_context_tool = (context_tool or _rtk_opt_in()) and not no_rtk
|
||||||
if prepare_only:
|
if prepare_only:
|
||||||
if setup_context_tool:
|
if setup_context_tool:
|
||||||
if _selected_context_tool() == _CONTEXT_TOOL_LEAN_CTX:
|
if _selected_context_tool() == _CONTEXT_TOOL_LEAN_CTX:
|
||||||
|
|
@ -4936,6 +4980,7 @@ def unwrap_claude(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -5459,6 +5504,7 @@ def _run_codex_wrap(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -5577,6 +5623,7 @@ def codex(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -5681,6 +5728,7 @@ def aider(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)")
|
@click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--no-context-tool",
|
"--no-context-tool",
|
||||||
|
|
@ -5783,6 +5831,7 @@ def openclaude(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -5862,6 +5911,7 @@ def vibe(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -5949,6 +5999,7 @@ def kimi(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6092,6 +6143,7 @@ def grok(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6198,6 +6250,7 @@ def cursor(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command("grok-build", context_settings={"ignore_unknown_options": True})
|
@wrap.command("grok-build", context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6294,6 +6347,7 @@ def grok_build(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6396,6 +6450,7 @@ def cline(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6491,6 +6546,7 @@ def zcode(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command("continue", context_settings={"ignore_unknown_options": True})
|
@wrap.command("continue", context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6615,6 +6671,7 @@ def continue_dev(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6740,6 +6797,7 @@ def goose(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -6883,6 +6941,7 @@ def openhands(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command("openclaw")
|
@wrap.command("openclaw")
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--plugin-path",
|
"--plugin-path",
|
||||||
type=click.Path(path_type=Path, file_okay=False, dir_okay=True),
|
type=click.Path(path_type=Path, file_okay=False, dir_okay=True),
|
||||||
|
|
@ -7124,6 +7183,7 @@ def openclaw(
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
@ -7648,6 +7708,7 @@ def unwrap_codex(port: int, no_stop_proxy: bool) -> None:
|
||||||
|
|
||||||
|
|
||||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||||
|
@_rtk_option
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
"--port", "-p", default=8787, type=click.IntRange(1, 65535), help="Proxy port (default: 8787)"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ def test_wrap_codex_prepare_only_accepts_no_context_tool_alias(monkeypatch, tmp_
|
||||||
|
|
||||||
def test_wrap_aider_prepare_only_injects_conventions(monkeypatch, tmp_path: Path) -> None:
|
def test_wrap_aider_prepare_only_injects_conventions(monkeypatch, tmp_path: Path) -> None:
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1") # RTK is opt-in; exercise the RTK-on path
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
||||||
|
|
@ -180,6 +181,7 @@ def test_wrap_cursor_prepare_only_registers_native_hook(monkeypatch, tmp_path: P
|
||||||
# GH #756: when rtk's own `--agent cursor` hook registers successfully,
|
# GH #756: when rtk's own `--agent cursor` hook registers successfully,
|
||||||
# headroom must not also inject RTK_INSTRUCTIONS_BLOCK into .cursorrules.
|
# headroom must not also inject RTK_INSTRUCTIONS_BLOCK into .cursorrules.
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1") # RTK is opt-in; exercise the RTK-on path
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
# headroom trusts the on-disk hook, not rtk's exit code, so simulate rtk
|
# headroom trusts the on-disk hook, not rtk's exit code, so simulate rtk
|
||||||
|
|
@ -206,6 +208,7 @@ def test_wrap_cursor_prepare_only_falls_back_to_cursorrules_when_hook_fails(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1") # RTK is opt-in; exercise the RTK-on path
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
with runner.isolated_filesystem(temp_dir=str(tmp_path)):
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,7 @@ class TestInjectAndRestoreRoundTrip:
|
||||||
"""`wrap codex` injects the rtk block into the Codex global AGENTS.md;
|
"""`wrap codex` injects the rtk block into the Codex global AGENTS.md;
|
||||||
`unwrap codex` must take it back out (regression for #1421)."""
|
`unwrap codex` must take it back out (regression for #1421)."""
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
codex_home = tmp_path / ".codex"
|
codex_home = tmp_path / ".codex"
|
||||||
codex_home.mkdir()
|
codex_home.mkdir()
|
||||||
agents = codex_home / "AGENTS.md"
|
agents = codex_home / "AGENTS.md"
|
||||||
|
|
@ -487,6 +488,7 @@ class TestInjectAndRestoreRoundTrip:
|
||||||
"""Only the marker-fenced rtk block is removed; the user's own AGENTS.md
|
"""Only the marker-fenced rtk block is removed; the user's own AGENTS.md
|
||||||
prose survives the unwrap."""
|
prose survives the unwrap."""
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
codex_home = tmp_path / ".codex"
|
codex_home = tmp_path / ".codex"
|
||||||
codex_home.mkdir()
|
codex_home.mkdir()
|
||||||
agents = codex_home / "AGENTS.md"
|
agents = codex_home / "AGENTS.md"
|
||||||
|
|
@ -1275,6 +1277,7 @@ def test_wrap_codex_injects_rtk_globally_without_changing_project_agents(
|
||||||
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
project_dir = tmp_path / "project"
|
project_dir = tmp_path / "project"
|
||||||
project_dir.mkdir()
|
project_dir.mkdir()
|
||||||
project_agents = project_dir / "AGENTS.md"
|
project_agents = project_dir / "AGENTS.md"
|
||||||
|
|
@ -1308,6 +1311,7 @@ def test_wrap_codex_launch_injects_rtk_globally_without_changing_project_agents(
|
||||||
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
runner: CliRunner, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
_set_test_home(monkeypatch, tmp_path)
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
project_dir = tmp_path / "project"
|
project_dir = tmp_path / "project"
|
||||||
project_dir.mkdir()
|
project_dir.mkdir()
|
||||||
project_agents = project_dir / "AGENTS.md"
|
project_agents = project_dir / "AGENTS.md"
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@ def _expected_project_prefix() -> str:
|
||||||
return f"/p/{quote(Path.cwd().name, safe='')}"
|
return f"/p/{quote(Path.cwd().name, safe='')}"
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
@pytest.fixture
|
||||||
def runner() -> CliRunner:
|
def runner() -> CliRunner:
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,11 @@ _EXISTING = "Be in “happy places” — really.\n".encode() + b"legacy \x9d by
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("inject, marker", INJECTORS)
|
@pytest.mark.parametrize("inject, marker", INJECTORS)
|
||||||
def test_inject_appends_into_file_with_non_ascii_and_stray_byte(inject, marker, tmp_path: Path):
|
def test_inject_appends_into_file_with_non_ascii_and_stray_byte(
|
||||||
|
inject, marker, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
if marker == _RTK_MARKER:
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
target = tmp_path / "AGENTS.md"
|
target = tmp_path / "AGENTS.md"
|
||||||
target.write_bytes(_EXISTING)
|
target.write_bytes(_EXISTING)
|
||||||
|
|
||||||
|
|
@ -45,7 +49,11 @@ def test_inject_appends_into_file_with_non_ascii_and_stray_byte(inject, marker,
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("inject, marker", INJECTORS)
|
@pytest.mark.parametrize("inject, marker", INJECTORS)
|
||||||
def test_inject_creates_file_when_absent(inject, marker, tmp_path: Path):
|
def test_inject_creates_file_when_absent(
|
||||||
|
inject, marker, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
if marker == _RTK_MARKER:
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
target = tmp_path / "nested" / "AGENTS.md"
|
target = tmp_path / "nested" / "AGENTS.md"
|
||||||
|
|
||||||
assert inject(target) is True
|
assert inject(target) is True
|
||||||
|
|
@ -53,7 +61,9 @@ def test_inject_creates_file_when_absent(inject, marker, tmp_path: Path):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("inject, marker", INJECTORS)
|
@pytest.mark.parametrize("inject, marker", INJECTORS)
|
||||||
def test_inject_is_idempotent(inject, marker, tmp_path: Path):
|
def test_inject_is_idempotent(inject, marker, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||||
|
if marker == _RTK_MARKER:
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
target = tmp_path / "AGENTS.md"
|
target = tmp_path / "AGENTS.md"
|
||||||
target.write_bytes(_EXISTING)
|
target.write_bytes(_EXISTING)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ HINTFILE_AGENTS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
@pytest.fixture
|
||||||
def runner() -> CliRunner:
|
def runner() -> CliRunner:
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ def test_wrap_omp_rtk_injects_into_cwd_agents_md(
|
||||||
runner: CliRunner, omp_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
runner: CliRunner, omp_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "rtk")
|
monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "rtk")
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
with (
|
with (
|
||||||
patch("headroom.cli.wrap.shutil.which", return_value="omp"),
|
patch("headroom.cli.wrap.shutil.which", return_value="omp"),
|
||||||
patch("headroom.cli.wrap._launch_tool"),
|
patch("headroom.cli.wrap._launch_tool"),
|
||||||
|
|
@ -260,8 +261,9 @@ def test_wrap_omp_rtk_injects_into_cwd_agents_md(
|
||||||
|
|
||||||
|
|
||||||
def test_unwrap_omp_restored_and_cleans_agents_md(
|
def test_unwrap_omp_restored_and_cleans_agents_md(
|
||||||
runner: CliRunner, omp_home: Path, tmp_path: Path
|
runner: CliRunner, omp_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
monkeypatch.setenv("HEADROOM_RTK", "1")
|
||||||
original = "providers:\n anthropic:\n apiKey: sk-user-secret\n"
|
original = "providers:\n anthropic:\n apiKey: sk-user-secret\n"
|
||||||
omp_home.write_bytes(original.encode("utf-8"))
|
omp_home.write_bytes(original.encode("utf-8"))
|
||||||
inject_models_override(8787, "proj")
|
inject_models_override(8787, "proj")
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ def _expected_project_prefix() -> str:
|
||||||
return f"/p/{quote(Path.cwd().name, safe='')}"
|
return f"/p/{quote(Path.cwd().name, safe='')}"
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
@pytest.fixture
|
||||||
def runner() -> CliRunner:
|
def runner() -> CliRunner:
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ from headroom.cli import wrap as wrap_mod
|
||||||
from headroom.cli.main import main
|
from headroom.cli.main import main
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
@pytest.fixture
|
||||||
def runner() -> CliRunner:
|
def runner() -> CliRunner:
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ from headroom.cli import wrap as wrap_mod
|
||||||
from headroom.cli.main import main
|
from headroom.cli.main import main
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
@pytest.fixture
|
||||||
def runner() -> CliRunner:
|
def runner() -> CliRunner:
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
|
||||||
47
tests/test_wrap_rtk_opt_in.py
Normal file
47
tests/test_wrap_rtk_opt_in.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""RTK is opt-in (off by default): enabled only via --rtk / HEADROOM_RTK=1.
|
||||||
|
|
||||||
|
Regression for the RTK-default flip: the three RTK entry points must no-op
|
||||||
|
unless explicitly opted in, and every wrap subcommand must expose --rtk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from headroom.cli import wrap
|
||||||
|
|
||||||
|
|
||||||
|
def _no_rtk_env() -> dict[str, str]:
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.pop("HEADROOM_RTK", None)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtk_opt_in_off_by_default() -> None:
|
||||||
|
with patch.dict(os.environ, _no_rtk_env(), clear=True):
|
||||||
|
assert wrap._rtk_opt_in() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtk_opt_in_on_via_env() -> None:
|
||||||
|
for val in ("1", "true", "yes", "on"):
|
||||||
|
with patch.dict(os.environ, {"HEADROOM_RTK": val}):
|
||||||
|
assert wrap._rtk_opt_in() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtk_entry_points_noop_when_not_opted_in(tmp_path) -> None:
|
||||||
|
agents = tmp_path / "AGENTS.md"
|
||||||
|
with patch.dict(os.environ, _no_rtk_env(), clear=True):
|
||||||
|
assert wrap._setup_rtk() is None
|
||||||
|
assert wrap._ensure_rtk_binary() is None
|
||||||
|
assert wrap._inject_rtk_instructions(agents) is False
|
||||||
|
assert not agents.exists() # nothing written when RTK is off
|
||||||
|
|
||||||
|
|
||||||
|
def test_rtk_flag_present_on_subcommands() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
for tool in ("claude", "codex", "copilot", "aider", "continue"):
|
||||||
|
out = runner.invoke(wrap.wrap, [tool, "--help"]).output
|
||||||
|
assert "--rtk" in out, f"--rtk missing from `wrap {tool} --help`"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue