From 4e2bbfee3f65e3287ab7693da70f0d7b20dada28 Mon Sep 17 00:00:00 2001 From: Tanmay Garg <102200932+Tanmay9223@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:21:21 +0530 Subject: [PATCH] fix(opencode): Use opencode.jsonc when present (#1590) ## Description Fix OpenCode proxy injection so it respects user configurations that use the `.jsonc` extension, preventing Headroom from creating a duplicate `.json` file that overrides it. Closes #1588 ## 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 - Updated `opencode_config_path` in `paths.py` to check for `.jsonc` - Updated backup creation in `config.py` to preserve the original extension ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text N/A ``` ## Real Behavior Proof - Environment: local headroom dev - Exact command / steps: creating a dummy `.config/opencode/opencode.jsonc` and running `headroom wrap opencode`. - Observed result: Headroom successfully injects into `.jsonc` and creates a backup named `opencode.jsonc.headroom-backup`. - Not tested: N/A ## 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 - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) ## Additional Notes --- headroom/install/paths.py | 11 +++++++-- headroom/providers/opencode/config.py | 2 +- tests/test_cli/test_wrap_opencode.py | 27 +++++++++++++++++++- tests/test_providers_opencode_config.py | 33 +++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/headroom/install/paths.py b/headroom/install/paths.py index 985f705eb..bcca00b0e 100644 --- a/headroom/install/paths.py +++ b/headroom/install/paths.py @@ -125,13 +125,20 @@ def opencode_config_path() -> Path: """Return the OpenCode config path. Resolves ``~/.config/opencode/opencode.json`` when ``OPENCODE_CONFIG`` - is unset; otherwise the value of that environment variable. + is unset; otherwise the value of that environment variable. Checks for + ``opencode.jsonc`` as well. """ env_path = os.environ.get("OPENCODE_CONFIG", "").strip() if env_path: return Path(env_path).expanduser() - return Path.home() / ".config" / "opencode" / "opencode.json" + base_dir = Path.home() / ".config" / "opencode" + jsonc_path = base_dir / "opencode.jsonc" + + if jsonc_path.exists(): + return jsonc_path + + return base_dir / "opencode.json" def zcode_config_dir() -> Path: diff --git a/headroom/providers/opencode/config.py b/headroom/providers/opencode/config.py index d0f321ef1..6c64544fe 100644 --- a/headroom/providers/opencode/config.py +++ b/headroom/providers/opencode/config.py @@ -81,7 +81,7 @@ def _opencode_home_dir() -> Path: def opencode_config_paths() -> tuple[Path, Path]: """Return ``(config_file, backup_file)`` for OpenCode.""" config_file = opencode_config_path() - backup_file = config_file.with_suffix(".json.headroom-backup") + backup_file = config_file.with_name(config_file.name + ".headroom-backup") return config_file, backup_file diff --git a/tests/test_cli/test_wrap_opencode.py b/tests/test_cli/test_wrap_opencode.py index 95f4e0ecc..b74a7d936 100644 --- a/tests/test_cli/test_wrap_opencode.py +++ b/tests/test_cli/test_wrap_opencode.py @@ -346,7 +346,7 @@ def test_unwrap_opencode_restores_from_backup( _set_test_home(monkeypatch, tmp_path) config_file = tmp_path / ".config" / "opencode" / "opencode.json" - backup_file = config_file.with_suffix(".json.headroom-backup") + backup_file = config_file.with_name("opencode.json.headroom-backup") config_file.parent.mkdir(parents=True, exist_ok=True) original = '{"model": "openai/gpt-4o"}' config_file.write_text(original) @@ -361,6 +361,31 @@ def test_unwrap_opencode_restores_from_backup( assert config_file.read_text(encoding="utf-8") == original +def test_unwrap_opencode_restores_from_backup_jsonc( + runner: CliRunner, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Unwrap restores the pre-wrap backup and removes it for jsonc files.""" + monkeypatch.chdir(tmp_path) + _set_test_home(monkeypatch, tmp_path) + + config_file = tmp_path / ".config" / "opencode" / "opencode.jsonc" + backup_file = config_file.with_name("opencode.jsonc.headroom-backup") + config_file.parent.mkdir(parents=True, exist_ok=True) + original = '{\n // User comment\n "model": "openai/gpt-4o"\n}' + config_file.write_text(original) + backup_file.write_text(original) + + with patch.object(wrap_mod, "_stop_local_proxy_for_unwrap", return_value="stopped"): + result = runner.invoke(main, ["unwrap", "opencode"]) + + assert result.exit_code == 0, result.output + assert "Restored prior" in result.output + assert not backup_file.exists() + assert config_file.read_text(encoding="utf-8") == original + + def test_unwrap_opencode_strips_blocks_when_no_backup( runner: CliRunner, tmp_path: Path, diff --git a/tests/test_providers_opencode_config.py b/tests/test_providers_opencode_config.py index ef1d142fe..658ba1ea4 100644 --- a/tests/test_providers_opencode_config.py +++ b/tests/test_providers_opencode_config.py @@ -49,6 +49,39 @@ def test_opencode_config_paths_from_env(tmp_path: Path, monkeypatch: pytest.Monk assert backup_file == tmp_path / "custom" / "opencode.json.headroom-backup" +def test_opencode_config_paths_default_jsonc( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Default config path resolves to opencode.jsonc when it exists.""" + _set_test_home(monkeypatch, tmp_path) + base_dir = tmp_path / ".config" / "opencode" + base_dir.mkdir(parents=True, exist_ok=True) + jsonc_path = base_dir / "opencode.jsonc" + jsonc_path.write_text("{}") + + config_file, backup_file = opencode_config_paths() + assert config_file == jsonc_path + assert backup_file == base_dir / "opencode.jsonc.headroom-backup" + + +def test_opencode_config_paths_env_overrides_jsonc( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """OPENCODE_CONFIG takes precedence even if opencode.jsonc exists.""" + _set_test_home(monkeypatch, tmp_path) + base_dir = tmp_path / ".config" / "opencode" + base_dir.mkdir(parents=True, exist_ok=True) + jsonc_path = base_dir / "opencode.jsonc" + jsonc_path.write_text("{}") + + custom_path = tmp_path / "custom" / "opencode.json" + monkeypatch.setenv("OPENCODE_CONFIG", str(custom_path)) + + config_file, backup_file = opencode_config_paths() + assert config_file == custom_path + assert backup_file == tmp_path / "custom" / "opencode.json.headroom-backup" + + # --------------------------------------------------------------------------- # Snapshot # ---------------------------------------------------------------------------