ci: restore green lint (reformat for ruff 0.15.17, fix mypy no-any-return, pin linters) (#1295)

## Description

The CI lint job (`ruff check .` → `ruff format --check .` → `mypy
headroom`) was red on `main`
and therefore on every open PR, for two unrelated reasons that the early
ruff failure was masking:

1. **ruff**: the lint job installs `ruff` unpinned, and ruff 0.15.17
began enforcing import-block
sorting (`I001`) and formatting that older ruff accepted → `ruff check
.` / `ruff format --check .`
   fail on files nobody touched.
2. **mypy**: `headroom/providers/opencode/config.py` had two `return
json.loads(...)` statements in
a function declared `-> dict[str, Any]`; `json.loads` is typed `Any`, so
`mypy headroom` fails
with `no-any-return` (reproduced on mypy 1.20.2 — not a version-specific
quirk).

This restores a green lint baseline and pins both linters so a future
release can't silently break
CI again.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `.github/workflows/ci.yml`: pin `ruff==0.15.17` and `mypy==1.20.2` in
the lint job.
- Applied `ruff check --fix .` (6 `I001` import-sort fixes) and `ruff
format .` (10 files) across
  the repo — import ordering and whitespace only, no behavior change.
- `headroom/providers/opencode/config.py`: narrow both
`_parse_json_loose` return sites with an
`isinstance(parsed, dict)` guard, so the `dict[str, Any]` annotation is
true at runtime
(non-dict JSON falls back to `{}`) and mypy's `no-any-return` is
resolved.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`, `ruff format --check .`, `mypy
headroom --ignore-missing-imports`)

### Test Output

```text
$ python -m ruff check .
All checks passed!

$ python -m ruff format --check .
913 files already formatted

$ python -m mypy headroom/providers/opencode/config.py --ignore-missing-imports
Success: no issues found in 1 source file

$ python -m pytest tests/test_providers_opencode_config.py -q
37 passed
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, ruff 0.15.17, mypy 1.20.2,
branch ci/fix-ruff-lint off
  headroomlabs-ai/main
- Exact command / steps: reproduced the red lint (latest ruff: 6 `I001`
+ 10 unformatted files;
the mypy failure was read from the #1295 CI lint log —
`config.py:125,133 no-any-return`, and
re-confirmed locally on mypy 1.20.2). Applied the ruff auto-fix/format,
added the dict guard,
  pinned both linters, and re-ran each lint step.
- Observed result: `ruff check .` → "All checks passed!"; `ruff format
--check .` → "913 files
already formatted"; `mypy` on the fixed file → "Success: no issues
found"; full `mypy headroom`
reports only Unix `fcntl` attributes that don't exist on this Windows
box (present on the Linux
CI runner, where the prior run showed exactly the two now-fixed errors).
37 opencode-config
  tests pass.
- Not tested: did not run the full OS/Python test matrix — the change is
formatting + two CI
dependency pins + a two-line type-narrowing guard, with no runtime
behavior change for dict JSON.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Parideboy 2026-06-22 22:14:40 +02:00 committed by GitHub
parent dee3500db6
commit 487aa71a3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 132 additions and 103 deletions

View file

@ -85,7 +85,7 @@ jobs:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-lint-${{ hashFiles('pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-lint-
- run: python -m pip install --upgrade pip ruff mypy
- run: python -m pip install --upgrade pip "ruff==0.15.17" "mypy==1.20.2"
- name: ruff check
run: ruff check .
- name: ruff format --check

View file

@ -934,9 +934,7 @@ def main() -> None:
log("All Docker wrap e2e checks passed.")
def verify_opencode_wrap(
base_env: dict[str, str], project_dir: Path, log_dir: Path
) -> None:
def verify_opencode_wrap(base_env: dict[str, str], project_dir: Path, log_dir: Path) -> None:
port = OPENCODE_PORT
run(
["headroom", "wrap", "opencode", "--port", str(port), "--", "--help"],
@ -948,7 +946,10 @@ def verify_opencode_wrap(
project_agents = project_dir / "AGENTS.md"
assert_true(global_agents.exists(), "Opencode wrap should create ~/.config/opencode/AGENTS.md")
assert_true(project_agents.exists(), "Opencode wrap should create project AGENTS.md")
assert_true(RTK_MARKER in global_agents.read_text(encoding="utf-8"), "Missing RTK marker in global AGENTS.md")
assert_true(
RTK_MARKER in global_agents.read_text(encoding="utf-8"),
"Missing RTK marker in global AGENTS.md",
)
assert_true(
RTK_MARKER in project_agents.read_text(encoding="utf-8"),
"Missing RTK marker in project AGENTS.md",
@ -967,7 +968,12 @@ def verify_opencode_wrap(
"Opencode wrap should inject headroom provider baseURL",
)
run(["headroom", "unwrap", "opencode", "--port", str(port)], env=base_env, cwd=project_dir, timeout=120)
run(
["headroom", "unwrap", "opencode", "--port", str(port)],
env=base_env,
cwd=project_dir,
timeout=120,
)
config_path = Path(base_env["HOME"]) / ".config" / "opencode" / "opencode.json"
if config_path.exists():
content = config_path.read_text(encoding="utf-8")

View file

@ -17,7 +17,6 @@ from .base import MCPRegistrar, RegisterResult, RegisterStatus, ServerSpec
from .claude import ClaudeRegistrar
from .codex import CodexRegistrar
from .display import any_succeeded, format_result, format_results
from .opencode import OpencodeRegistrar
from .install import (
DEFAULT_PROXY_URL,
build_headroom_spec,
@ -25,6 +24,7 @@ from .install import (
get_all_registrars,
install_everywhere,
)
from .opencode import OpencodeRegistrar
__all__ = [
"DEFAULT_PROXY_URL",

View file

@ -21,15 +21,11 @@ _MCP_MARKER_END = "// --- end Headroom MCP server ---"
# Regex to strip headroom blocks (including the marker comments).
_PROVIDER_BLOCK_RE = re.compile(
re.escape(_PROVIDER_MARKER_START)
+ r".*?"
+ re.escape(_PROVIDER_MARKER_END),
re.escape(_PROVIDER_MARKER_START) + r".*?" + re.escape(_PROVIDER_MARKER_END),
re.DOTALL,
)
_MCP_BLOCK_RE = re.compile(
re.escape(_MCP_MARKER_START)
+ r".*?"
+ re.escape(_MCP_MARKER_END),
re.escape(_MCP_MARKER_START) + r".*?" + re.escape(_MCP_MARKER_END),
re.DOTALL,
)
HEADROOM_OPENCODE_PLUGIN = "headroom-opencode"
@ -126,7 +122,8 @@ def _parse_json_loose(text: str) -> dict[str, Any]:
comments that follow a comma.
"""
try:
return json.loads(text)
parsed = json.loads(text)
return parsed if isinstance(parsed, dict) else {}
except json.JSONDecodeError:
pass
# Pass 1: remove lines that are ONLY a comment.
@ -134,14 +131,13 @@ def _parse_json_loose(text: str) -> dict[str, Any]:
# Pass 2: remove inline trailing comments (", // comment").
cleaned = re.sub(r",\s*//[^\n]*", ",", cleaned)
try:
return json.loads(cleaned)
parsed = json.loads(cleaned)
return parsed if isinstance(parsed, dict) else {}
except json.JSONDecodeError:
return {}
def _inject_key_into_json(
data: dict[str, Any], key: str, value: Any
) -> dict[str, Any]:
def _inject_key_into_json(data: dict[str, Any], key: str, value: Any) -> dict[str, Any]:
"""Merge ``value`` into ``data[key]`` idempotently."""
existing = data.get(key)
if isinstance(existing, dict) and isinstance(value, dict):

View file

@ -283,7 +283,10 @@ def write_github_outputs(info: ReleaseVersionInfo, output_path: str) -> None:
def main() -> None:
root = Path.cwd()
manual_version = os.environ.get("MANUAL_VER", "").strip()
manual_match = re.fullmatch(r"v?(\d+\.\d+\.\d+(?:[abrc]\d+)?)", (os.environ.get("MANUAL_VER") or os.environ.get("LEVEL", "patch")).strip())
manual_match = re.fullmatch(
r"v?(\d+\.\d+\.\d+(?:[abrc]\d+)?)",
(os.environ.get("MANUAL_VER") or os.environ.get("LEVEL", "patch")).strip(),
)
if manual_match:
version = manual_match.group(1)
info = ReleaseVersionInfo(

View file

@ -21,7 +21,9 @@ from typing import Any
logger = logging.getLogger(__name__)
_KNOWN_WRAP_AGENTS = frozenset({"claude", "copilot", "codex", "aider", "cursor", "openclaw", "opencode"})
_KNOWN_WRAP_AGENTS = frozenset(
{"claude", "copilot", "codex", "aider", "cursor", "openclaw", "opencode"}
)
# Stack slugs must start with a letter and contain only [a-z0-9_], max 64 chars.
# Applied at every ingress (env var, HTTP header, stats aggregation) so downstream

View file

@ -52,7 +52,8 @@ def test_wrap_opencode_sets_config_content_env(
with patch.object(wrap_mod, "_launch_tool", side_effect=fake_launch_tool):
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
result = runner.invoke(
main, ["wrap", "opencode", "--port", "9000", "--no-mcp", "--", "--model", "gpt-4o"]
main,
["wrap", "opencode", "--port", "9000", "--no-mcp", "--", "--model", "gpt-4o"],
)
assert result.exit_code == 0, result.output
@ -491,7 +492,9 @@ def test_wrap_opencode_respects_opencode_config_env(
assert result.exit_code == 0, result.output
assert custom_config.exists()
default_config = tmp_path / ".config" / "opencode" / "opencode.json"
assert not default_config.exists(), "default config should not be created when OPENCODE_CONFIG is set"
assert not default_config.exists(), (
"default config should not be created when OPENCODE_CONFIG is set"
)
def test_wrap_opencode_headroom_project_from_cwd(
@ -590,9 +593,7 @@ def test_unwrap_opencode_removes_config_when_only_headroom_content(
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
config_file.parent.mkdir(parents=True, exist_ok=True)
wrapped_content = (
wrap_mod._PROVIDER_MARKER_START
+ '\n"provider": {},\n'
+ wrap_mod._PROVIDER_MARKER_END
wrap_mod._PROVIDER_MARKER_START + '\n"provider": {},\n' + wrap_mod._PROVIDER_MARKER_END
)
config_file.write_text(wrapped_content)
@ -766,11 +767,18 @@ def test_wrap_opencode_with_backend_and_anyllm_provider(
with patch.object(wrap_mod, "_launch_tool", side_effect=SystemExit(0)):
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
result = runner.invoke(
main, [
"wrap", "opencode", "--port", "9000",
"--backend", "anyllm", "--anyllm-provider", "groq",
main,
[
"wrap",
"opencode",
"--port",
"9000",
"--backend",
"anyllm",
"--anyllm-provider",
"groq",
"--no-mcp",
]
],
)
assert result.exit_code == 0, result.output
@ -831,9 +839,7 @@ def test_wrap_opencode_respects_opencode_home_env(
with patch.object(wrap_mod.shutil, "which", return_value="opencode"):
with patch.object(wrap_mod, "_launch_tool", side_effect=SystemExit(0)):
with patch.object(wrap_mod, "_ensure_rtk_binary", return_value=Path("/tmp/rtk")):
result = runner.invoke(
main, ["wrap", "opencode", "--port", "9000", "--no-mcp"]
)
result = runner.invoke(main, ["wrap", "opencode", "--port", "9000", "--no-mcp"])
assert result.exit_code == 0, result.output
agents_md = Path(custom_home) / "AGENTS.md"

View file

@ -66,4 +66,6 @@ def test_env_target_and_config_paths(monkeypatch, tmp_path: Path) -> None:
assert install_paths.claude_settings_path() == tmp_path / ".claude" / "settings.json"
assert install_paths.codex_config_path() == tmp_path / ".codex" / "config.toml"
assert install_paths.openclaw_config_path() == tmp_path / ".openclaw" / "openclaw.json"
assert install_paths.opencode_config_path() == tmp_path / ".config" / "opencode" / "opencode.json"
assert (
install_paths.opencode_config_path() == tmp_path / ".config" / "opencode" / "opencode.json"
)

View file

@ -16,9 +16,13 @@ from headroom.providers.codex.install import apply_provider_scope as apply_codex
from headroom.providers.codex.install import build_install_env as build_codex_install_env
from headroom.providers.codex.install import revert_provider_scope as revert_codex_provider_scope
from headroom.providers.copilot.install import build_install_env as build_copilot_install_env
from headroom.providers.opencode.install import apply_provider_scope as apply_opencode_provider_scope
from headroom.providers.opencode.install import (
apply_provider_scope as apply_opencode_provider_scope,
)
from headroom.providers.opencode.install import build_install_env as build_opencode_install_env
from headroom.providers.opencode.install import revert_provider_scope as revert_opencode_provider_scope
from headroom.providers.opencode.install import (
revert_provider_scope as revert_opencode_provider_scope,
)
def _manifest(tmp_path: Path) -> DeploymentManifest:
@ -800,7 +804,7 @@ def test_planner_resolves_opencode_as_install_target() -> None:
def test_planner_opencode_in_supported_targets_enum() -> None:
from headroom.install.models import ToolTarget
from headroom.install.planner import SUPPORTED_TARGETS, PROVIDER_SCOPE_TARGETS
from headroom.install.planner import PROVIDER_SCOPE_TARGETS, SUPPORTED_TARGETS
assert ToolTarget.OPENCODE in SUPPORTED_TARGETS
assert ToolTarget.OPENCODE in PROVIDER_SCOPE_TARGETS
@ -831,6 +835,7 @@ def test_planner_resolve_all_includes_opencode() -> None:
def test_planner_provider_scope_unsupported_error_excludes_opencode() -> None:
import click
import pytest
from headroom.install.planner import resolve_targets
with pytest.raises(click.ClickException, match="unsupported targets"):
@ -842,9 +847,7 @@ def test_planner_provider_scope_unsupported_error_excludes_opencode() -> None:
# ---------------------------------------------------------------------------
def test_revert_opencode_provider_scope_fallback_on_oserror(
monkeypatch, tmp_path: Path
) -> None:
def test_revert_opencode_provider_scope_fallback_on_oserror(monkeypatch, tmp_path: Path) -> None:
"""revert_opencode_provider_scope falls back to strip when backup copy fails."""
config_path = tmp_path / "opencode.json"
backup_path = config_path.with_suffix(".json.headroom-backup")
@ -852,9 +855,10 @@ def test_revert_opencode_provider_scope_fallback_on_oserror(
from headroom.install.models import ManagedMutation
from headroom.providers.opencode.config import (
_PROVIDER_MARKER_START,
_PROVIDER_MARKER_END,
_PROVIDER_MARKER_START,
)
original = '{"model": "openai/gpt-4o"}'
backup_path.write_text(original)
@ -879,9 +883,7 @@ def test_revert_opencode_provider_scope_fallback_on_oserror(
monkeypatch.setattr("shutil.copy2", _fail_copy2)
revert_provider_scope(
ManagedMutation(
target="opencode", kind="json-block", path=str(config_path)
),
ManagedMutation(target="opencode", kind="json-block", path=str(config_path)),
manifest,
)

View file

@ -8,6 +8,7 @@ from typing import Any
import pytest
from headroom.mcp_registry.base import RegisterStatus
from headroom.mcp_registry.opencode import (
OpencodeRegistrar,
_diff_specs,
@ -15,7 +16,6 @@ from headroom.mcp_registry.opencode import (
_spec_to_entry,
_specs_equivalent,
)
from headroom.mcp_registry.base import RegisterStatus
def _write_json(path: Path, data: dict[str, Any]) -> None:
@ -164,6 +164,7 @@ def test_unregister_removes_mcp_key_when_empty(tmp_path: Path) -> None:
assert registrar.get_server("headroom") is None
# mcp key should be removed entirely
import json
data = json.loads((tmp_path / "opencode.json").read_text())
assert "mcp" not in data
@ -174,11 +175,18 @@ def test_register_server_leaves_other_mcp_servers(tmp_path: Path) -> None:
from headroom.mcp_registry.base import ServerSpec
# Pre-populate with a user-managed MCP server
_write_json(tmp_path / "opencode.json", {
"mcp": {
"existing-server": {"type": "remote", "url": "https://example.com", "enabled": True},
}
})
_write_json(
tmp_path / "opencode.json",
{
"mcp": {
"existing-server": {
"type": "remote",
"url": "https://example.com",
"enabled": True,
},
}
},
)
spec = ServerSpec(name="headroom", command="headroom", args=("mcp", "serve"))
registrar.register_server(spec)
@ -193,11 +201,14 @@ def test_unregister_preserves_other_mcp_servers(tmp_path: Path) -> None:
registrar = _registrar(tmp_path)
from headroom.mcp_registry.base import ServerSpec
_write_json(tmp_path / "opencode.json", {
"mcp": {
"existing-server": {"type": "remote", "url": "https://example.com"},
}
})
_write_json(
tmp_path / "opencode.json",
{
"mcp": {
"existing-server": {"type": "remote", "url": "https://example.com"},
}
},
)
spec = ServerSpec(name="headroom", command="headroom", args=("mcp", "serve"))
registrar.register_server(spec)
registrar.unregister_server("headroom")
@ -364,9 +375,7 @@ def test_register_server_returns_already_status(
assert result.status == RegisterStatus.ALREADY
def test_unregister_server_handles_oserror(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_unregister_server_handles_oserror(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
r = _registrar(tmp_path)
from headroom.mcp_registry.base import ServerSpec
@ -377,9 +386,7 @@ def test_unregister_server_handles_oserror(
msg = "permission denied"
raise OSError(msg)
monkeypatch.setattr(
"headroom.mcp_registry.opencode._write_json", _fail_write
)
monkeypatch.setattr("headroom.mcp_registry.opencode._write_json", _fail_write)
ok = r.unregister_server("bad-unregister")
assert ok is False

View file

@ -32,9 +32,7 @@ def _set_test_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# ---------------------------------------------------------------------------
def test_opencode_config_paths_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_opencode_config_paths_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Default config path resolves to ~/.config/opencode/opencode.json."""
_set_test_home(monkeypatch, tmp_path)
config_file, backup_file = opencode_config_paths()
@ -42,9 +40,7 @@ def test_opencode_config_paths_default(
assert backup_file == tmp_path / ".config" / "opencode" / "opencode.json.headroom-backup"
def test_opencode_config_paths_from_env(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def test_opencode_config_paths_from_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""OPENCODE_CONFIG env var overrides the default path."""
custom_path = tmp_path / "custom" / "opencode.json"
monkeypatch.setenv("OPENCODE_CONFIG", str(custom_path))
@ -82,7 +78,7 @@ def test_snapshot_skips_if_markers_present(tmp_path: Path) -> None:
"""snapshot skips if the config already contains Headroom markers."""
config_file = tmp_path / "opencode.json"
backup_file = tmp_path / "opencode.json.headroom-backup"
config_file.write_text('// --- Headroom proxy provider ---\n{}')
config_file.write_text("// --- Headroom proxy provider ---\n{}")
snapshot_opencode_config_if_unwrapped(config_file, backup_file)
assert not backup_file.exists()
@ -158,7 +154,9 @@ def test_inject_key_overwrites_non_dict() -> None:
# ---------------------------------------------------------------------------
def test_inject_provider_config_creates_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def test_inject_provider_config_creates_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""inject_opencode_provider_config creates the config file when missing."""
_set_test_home(monkeypatch, tmp_path)
inject_opencode_provider_config(port=8787)
@ -192,13 +190,7 @@ def test_parse_json_loose_handles_valid_json() -> None:
def test_parse_json_loose_handles_jsonc_with_comments() -> None:
"""_parse_json_loose strips comments and returns valid data."""
text = (
'{\n'
' "model": "gpt-4o",\n'
' // this is a comment\n'
' "provider": {}\n'
'}'
)
text = '{\n "model": "gpt-4o",\n // this is a comment\n "provider": {}\n}'
data = _parse_json_loose(text)
assert data["model"] == "gpt-4o"
assert data["provider"] == {}
@ -214,11 +206,11 @@ def test_parse_json_loose_handles_urls_in_json() -> None:
def test_parse_json_loose_handles_comments_and_urls() -> None:
"""_parse_json_loose handles both comments and URLs in the same file."""
text = (
'{\n'
' // proxy configuration\n'
"{\n"
" // proxy configuration\n"
' "baseURL": "http://127.0.0.1:8787/v1",\n'
' "name": "Headroom // Proxy"\n'
'}'
"}"
)
data = _parse_json_loose(text)
assert data["baseURL"] == "http://127.0.0.1:8787/v1"
@ -262,22 +254,24 @@ def test_strip_blocks_handles_whitespace_only() -> None:
def test_strip_blocks_preserves_non_headroom_jsonc() -> None:
"""strip_opencode_headroom_blocks preserves JSONC comments not from Headroom."""
content = (
'// user comment\n'
'{"model": "gpt-4o"}\n'
'// another user comment\n'
)
content = '// user comment\n{"model": "gpt-4o"}\n// another user comment\n'
cleaned = strip_opencode_headroom_blocks(content)
assert '// user comment' in cleaned
assert "// user comment" in cleaned
assert '{"model": "gpt-4o"}' in cleaned
def test_strip_blocks_removes_only_one_of_two_identical_blocks() -> None:
"""strip_opencode_headroom_blocks removes all provider blocks, not just the first."""
from headroom.providers.opencode.config import _PROVIDER_MARKER_END, _PROVIDER_MARKER_START
content = (
_PROVIDER_MARKER_START + "\nblock1\n" + _PROVIDER_MARKER_END + "\n"
+ _PROVIDER_MARKER_START + "\nblock2\n" + _PROVIDER_MARKER_END
_PROVIDER_MARKER_START
+ "\nblock1\n"
+ _PROVIDER_MARKER_END
+ "\n"
+ _PROVIDER_MARKER_START
+ "\nblock2\n"
+ _PROVIDER_MARKER_END
)
cleaned = strip_opencode_headroom_blocks(content)
assert _PROVIDER_MARKER_START not in cleaned
@ -288,9 +282,8 @@ def test_strip_blocks_removes_only_one_of_two_identical_blocks() -> None:
def test_strip_blocks_handles_only_mcp_markers() -> None:
"""strip_opencode_headroom_blocks also strips MCP markers."""
from headroom.providers.opencode.config import _MCP_MARKER_END, _MCP_MARKER_START
content = (
_MCP_MARKER_START + "\nmcp data\n" + _MCP_MARKER_END
)
content = _MCP_MARKER_START + "\nmcp data\n" + _MCP_MARKER_END
cleaned = strip_opencode_headroom_blocks(content)
assert _MCP_MARKER_START not in cleaned
@ -307,7 +300,9 @@ def test_inject_provider_config_merges_with_existing_mcp(
_set_test_home(monkeypatch, tmp_path)
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text('{"mcp": {"existing-server": {"type": "remote", "url": "https://example.com"}}}')
config_file.write_text(
'{"mcp": {"existing-server": {"type": "remote", "url": "https://example.com"}}}'
)
inject_opencode_provider_config(port=8787)
@ -323,11 +318,15 @@ def test_inject_provider_config_idempotent_with_complex_config(
_set_test_home(monkeypatch, tmp_path)
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(json.dumps({
"model": "openai/gpt-4o",
"provider": {"openai": {"models": {"gpt-4o": {}}}},
"mcp": {"myserver": {"type": "local", "command": ["echo"]}},
}))
config_file.write_text(
json.dumps(
{
"model": "openai/gpt-4o",
"provider": {"openai": {"models": {"gpt-4o": {}}}},
"mcp": {"myserver": {"type": "local", "command": ["echo"]}},
}
)
)
inject_opencode_provider_config(port=8787)
inject_opencode_provider_config(port=8787)
@ -346,11 +345,15 @@ def test_inject_provider_config_preserves_unrelated_top_level_keys(
_set_test_home(monkeypatch, tmp_path)
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
config_file.parent.mkdir(parents=True, exist_ok=True)
config_file.write_text(json.dumps({
"plugin": ["some-plugin"],
"permission": {"bash": {"*": "ask"}},
"model": "openai/gpt-4o",
}))
config_file.write_text(
json.dumps(
{
"plugin": ["some-plugin"],
"permission": {"bash": {"*": "ask"}},
"model": "openai/gpt-4o",
}
)
)
inject_opencode_provider_config(port=8787)
@ -375,9 +378,7 @@ def test_append_headroom_plugin_preserves_configured_tuple_entry() -> None:
}
assert append_headroom_plugin(config) is False
assert config["plugin"] == [
[HEADROOM_OPENCODE_PLUGIN, {"proxyUrl": "http://127.0.0.1:8787"}]
]
assert config["plugin"] == [[HEADROOM_OPENCODE_PLUGIN, {"proxyUrl": "http://127.0.0.1:8787"}]]
def test_inject_provider_config_no_crash_on_unwriteable_dir(
@ -386,6 +387,7 @@ def test_inject_provider_config_no_crash_on_unwriteable_dir(
"""inject_opencode_provider_config raises click.ClickException on OSError."""
import click as click_mod
monkeypatch.setenv("HOME", "/nonexistent/path/that/cannot/be/created")
try:
inject_opencode_provider_config(port=8787)

View file

@ -57,6 +57,7 @@ def test_apply_provider_scope_creates_config(
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
assert config_file.exists()
import json
config = json.loads(config_file.read_text())
assert config["provider"]["headroom"]["options"]["baseURL"] == "http://127.0.0.1:8787/v1"
@ -86,6 +87,7 @@ def test_revert_provider_scope_restores_file(
config_file.write_text('{"model": "openai/gpt-4o"}')
from headroom.install.models import ManagedMutation
mutation = ManagedMutation(
target="opencode",
kind="json-block",
@ -102,6 +104,7 @@ def test_revert_provider_scope_noop_when_file_missing(
) -> None:
"""revert_provider_scope is a safe no-op when the config file is gone."""
from headroom.install.models import ManagedMutation
mutation = ManagedMutation(
target="opencode",
kind="json-block",