From b4fde0c3a4c2585d4aeda2c6987fe509a5296fe5 Mon Sep 17 00:00:00 2001 From: Terminal Chai Date: Tue, 23 Jun 2026 09:25:36 +0530 Subject: [PATCH] fix(wrap): add Copilot unwrap command (#1251) ## Description Adds the missing `headroom unwrap copilot` command so the durable setup created by `headroom wrap copilot` can be removed without touching user-authored Copilot instructions. Closes #1172 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add `headroom unwrap copilot` with `--port` and `--no-stop-proxy` options. - Remove only Headroom's marker-fenced RTK block from `.github/copilot-instructions.md`. - Preserve user-authored content and leave malformed/unmatched markers unchanged. - Remove an instruction file that contains only Headroom's generated block. - Update the changelog. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text > .\.venv\Scripts\python.exe -X utf8 -m pytest tests/test_cli/test_wrap_copilot.py -q 30 passed in 1.11s > .\.venv\Scripts\ruff.exe check . All checks passed! > uv run --extra dev ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_copilot.py 2 files already formatted > uv run --extra dev mypy headroom/cli/wrap.py Success: no issues found in 1 source file ``` The new command test failed before the implementation with: ```text Error: No such command 'copilot'. ``` ## Real Behavior Proof - Environment: Windows, Python 3.12.12, local editable Headroom checkout. - Exact command / steps: created an isolated project containing user guidance plus a Headroom marker-fenced RTK block, then ran `.venv\Scripts\headroom.exe unwrap copilot --no-stop-proxy`. - Observed result: command exited `0`, printed `Removed Headroom rtk instructions from Copilot.`, and the resulting file contained only `Keep user guidance.`. - Not tested: a live Copilot CLI session or terminating a real proxy process; proxy shutdown delegates to the existing tested unwrap helper and is covered here with a command-level mock. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) Not applicable; this is a CLI-only change. ## Additional Notes No dependencies were added. The unchecked comment item is not applicable because the cleanup helper and command are straightforward and documented with docstrings. This pull request includes code written with the assistance of AI. The changes have not yet been reviewed by a human. Co-authored-by: JerrettDavis --- headroom/cli/wrap.py | 48 ++++++++++++ tests/test_cli/test_wrap_copilot.py | 111 ++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/headroom/cli/wrap.py b/headroom/cli/wrap.py index 846f38a58..d0bc0f76d 100644 --- a/headroom/cli/wrap.py +++ b/headroom/cli/wrap.py @@ -1663,6 +1663,34 @@ def _inject_rtk_instructions(file_path: Path, verbose: bool = False) -> bool: return True +def _remove_rtk_instructions(file_path: Path) -> bool: + """Remove Headroom's marker-fenced rtk guidance from an instruction file.""" + if not file_path.exists(): + return False + + content = file_path.read_text(encoding="utf-8") + end_marker = "" + start = content.find(_RTK_MARKER) + if start < 0: + return False + + end = content.find(end_marker, start) + if end < 0: + return False + end += len(end_marker) + prefix = content[:start].rstrip() + suffix = content[end:].lstrip("\r\n") + cleaned = "\n\n".join(part for part in (prefix, suffix) if part) + if cleaned: + cleaned = cleaned.rstrip() + "\n" + + if cleaned: + file_path.write_text(cleaned, encoding="utf-8") + else: + file_path.unlink() + return True + + def _inject_memory_mcp_config(db_path: str, user_id: str) -> None: """Register headroom memory as an MCP server in Codex's config.toml. @@ -3632,6 +3660,26 @@ def copilot( ) +# ============================================================================= +# GitHub Copilot CLI (unwrap) +# ============================================================================= + + +@unwrap.command("copilot") +@click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)") +@click.option("--no-stop-proxy", is_flag=True, help="Do not stop the local Headroom proxy") +def unwrap_copilot(port: int, no_stop_proxy: bool) -> None: + """Undo durable setup from ``headroom wrap copilot``.""" + instructions = Path.cwd() / ".github" / "copilot-instructions.md" + if _remove_rtk_instructions(instructions): + click.echo(" Removed Headroom rtk instructions from Copilot.") + else: + click.echo(" No Headroom rtk instructions found for Copilot.") + + if not no_stop_proxy: + _echo_unwrap_proxy_stop_status(_stop_local_proxy_for_unwrap(port), port) + + # ============================================================================= # OpenAI Codex CLI # ============================================================================= diff --git a/tests/test_cli/test_wrap_copilot.py b/tests/test_cli/test_wrap_copilot.py index f42903e5e..568e0aac2 100644 --- a/tests/test_cli/test_wrap_copilot.py +++ b/tests/test_cli/test_wrap_copilot.py @@ -668,6 +668,117 @@ def test_wrap_copilot_fails_when_binary_missing( assert "Install GitHub Copilot CLI" in result.output +def test_unwrap_copilot_removes_rtk_instructions_and_stops_proxy( + runner: CliRunner, + wrap_modules: tuple[types.ModuleType, click.Group], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + wrap_cli, main = wrap_modules + monkeypatch.chdir(tmp_path) + instructions = tmp_path / ".github" / "copilot-instructions.md" + instructions.parent.mkdir() + instructions.write_text( + "Keep user guidance.\n\n" + wrap_cli.RTK_INSTRUCTIONS_BLOCK, + encoding="utf-8", + ) + + with patch( + "headroom.cli.wrap._stop_local_proxy_for_unwrap", + return_value="stopped", + ) as stop_proxy: + result = runner.invoke(main, ["unwrap", "copilot", "--port", "9999"]) + + assert result.exit_code == 0, result.output + assert instructions.read_text(encoding="utf-8") == "Keep user guidance.\n" + stop_proxy.assert_called_once_with(9999) + assert "Removed Headroom rtk instructions from Copilot." in result.output + assert "Stopped local Headroom proxy on port 9999" in result.output + + +def test_unwrap_copilot_preserves_instructions_after_rtk_block( + runner: CliRunner, + wrap_modules: tuple[types.ModuleType, click.Group], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + wrap_cli, main = wrap_modules + monkeypatch.chdir(tmp_path) + instructions = tmp_path / ".github" / "copilot-instructions.md" + instructions.parent.mkdir() + instructions.write_text( + wrap_cli.RTK_INSTRUCTIONS_BLOCK + "\nKeep trailing guidance.\n", + encoding="utf-8", + ) + + result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"]) + + assert result.exit_code == 0, result.output + assert instructions.read_text(encoding="utf-8") == "Keep trailing guidance.\n" + + +def test_unwrap_copilot_leaves_malformed_marker_content_unchanged( + runner: CliRunner, + wrap_modules: tuple[types.ModuleType, click.Group], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + wrap_cli, main = wrap_modules + monkeypatch.chdir(tmp_path) + instructions = tmp_path / ".github" / "copilot-instructions.md" + instructions.parent.mkdir() + content = f"\nKeep user guidance.\n{wrap_cli._RTK_MARKER}\n" + instructions.write_text(content, encoding="utf-8") + + result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"]) + + assert result.exit_code == 0, result.output + assert instructions.read_text(encoding="utf-8") == content + assert "No Headroom rtk instructions found for Copilot." in result.output + + +def test_unwrap_copilot_deletes_generated_only_instruction_file( + runner: CliRunner, + wrap_modules: tuple[types.ModuleType, click.Group], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + wrap_cli, main = wrap_modules + monkeypatch.chdir(tmp_path) + instructions = tmp_path / ".github" / "copilot-instructions.md" + instructions.parent.mkdir() + instructions.write_text(wrap_cli.RTK_INSTRUCTIONS_BLOCK, encoding="utf-8") + + result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"]) + + assert result.exit_code == 0, result.output + assert not instructions.exists() + + +@pytest.mark.parametrize("create_user_file", [False, True]) +def test_unwrap_copilot_is_noop_without_managed_instructions( + runner: CliRunner, + wrap_modules: tuple[types.ModuleType, click.Group], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + create_user_file: bool, +) -> None: + _wrap_cli, main = wrap_modules + monkeypatch.chdir(tmp_path) + instructions = tmp_path / ".github" / "copilot-instructions.md" + if create_user_file: + instructions.parent.mkdir() + instructions.write_text("Keep user guidance.\n", encoding="utf-8") + + result = runner.invoke(main, ["unwrap", "copilot", "--no-stop-proxy"]) + + assert result.exit_code == 0, result.output + assert instructions.exists() is create_user_file + if create_user_file: + assert instructions.read_text(encoding="utf-8") == "Keep user guidance.\n" + assert "No Headroom rtk instructions found for Copilot." in result.output + + # --------------------------------------------------------------------------- # Regression suite for #610 — GitHub Copilot endpoint routing per auth mode. #