mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
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:
parent
dee3500db6
commit
487aa71a3c
12 changed files with 132 additions and 103 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue