mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986)
## Description Fixes #2492, #2028, and #2827. Claude daemon workers consume project settings rather than reliably inheriting wrapper environment state, while the Claude VS Code webview cannot render deferred-tool response blocks. Separately, recent Copilot Chat versions use the whole CAPI override for generation; the legacy proxy override alone only sends model discovery through Headroom. This PR carries both integrations through to the actual consumers instead of only changing their launch-time surface configuration. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update - [ ] Build / CI ## Changes Made - Persist the resolved Claude ENABLE_TOOL_SEARCH value into project settings for daemon workers and restore it transactionally after wrap exits. - Use compatibility-safe Foundry and Claude VS Code defaults while preserving explicit user choices. - Configure both Copilot overrideProxyUrl and overrideCapiUrl in the reversible managed VS Code settings block. - Route Copilot unprefixed POST /chat/completions and HTTP /responses requests through the real compression handlers. - Keep /responses out of the Codex WebSocket aliases because Copilot and Codex use different WebSocket wire protocols. - Extend wrap E2E assertions for both the Claude webview mode and Copilot CAPI routing. ## Testing - [x] 127 combined Claude, Copilot, route-integration, and MCP dependency-contract tests pass. - [x] Ruff check passes on all changed Python files. - [x] Ruff format check passes. - [x] Python compilation and git diff --check pass. ## Runtime Safety Standalone Claude CLI defaults remain unchanged. Explicit Claude tool-search values retain precedence, and project settings are restored through the existing cleanup path. Copilot model/session helper endpoints continue through generic passthrough, while only validated HTTP generation paths receive explicit compression routes. Existing Codex WebSocket behavior is unchanged. ## Review Readiness - [x] Current main and MCP v1 compatibility retained - [x] Worker-facing Claude persistence covered - [x] Reversible Copilot and Claude settings behavior covered - [x] Copilot generation routes covered at registration and proxy integration layers - [x] Ready for review
This commit is contained in:
parent
eafdf11a2c
commit
1aa701adaa
17 changed files with 162 additions and 13 deletions
|
|
@ -16,7 +16,8 @@ from headroom.cli import init as init_cli
|
|||
from headroom.providers.claude import install as claude_install
|
||||
|
||||
|
||||
def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path) -> None:
|
||||
def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("CLAUDE_CODE_USE_FOUNDRY", raising=False)
|
||||
settings = tmp_path / "settings.json"
|
||||
init_cli._ensure_claude_hooks(settings, profile="init-user", port=8787)
|
||||
|
||||
|
|
@ -25,6 +26,16 @@ def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path) -> None:
|
|||
assert env["ENABLE_TOOL_SEARCH"] == "true"
|
||||
|
||||
|
||||
def test_ensure_claude_hooks_disables_tool_search_for_foundry(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("CLAUDE_CODE_USE_FOUNDRY", "1")
|
||||
settings = tmp_path / "settings.json"
|
||||
|
||||
init_cli._ensure_claude_hooks(settings, profile="init-user", port=8787)
|
||||
|
||||
env = json.loads(settings.read_text(encoding="utf-8"))["env"]
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "false"
|
||||
|
||||
|
||||
def test_ensure_claude_hooks_respects_user_tool_search_value(tmp_path: Path) -> None:
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,29 @@ def test_write_preserves_other_env_keys(tmp_path: Path) -> None:
|
|||
assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
|
||||
|
||||
|
||||
def test_tool_search_write_and_restore_reaches_daemon_worker_settings(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(
|
||||
json.dumps({"env": {"ENABLE_TOOL_SEARCH": "true", "KEEP": "1"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
previous = wrap_cli._write_claude_wrap_tool_search("false", settings_path=path)
|
||||
|
||||
assert previous == "true"
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["env"] == {
|
||||
"ENABLE_TOOL_SEARCH": "false",
|
||||
"KEEP": "1",
|
||||
}
|
||||
|
||||
wrap_cli._restore_claude_wrap_tool_search(previous, settings_path=path)
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["env"] == {
|
||||
"ENABLE_TOOL_SEARCH": "true",
|
||||
"KEEP": "1",
|
||||
}
|
||||
|
||||
|
||||
def test_write_returns_none_when_key_absent(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
prev = wrap_cli._write_claude_wrap_base_url("http://127.0.0.1:8787", settings_path=path)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,13 @@ def _invoke_wrap_claude(
|
|||
|
||||
monkeypatch.setattr(wrap_mod, "_write_claude_wrap_base_url", fake_write_base_url)
|
||||
monkeypatch.setattr(wrap_mod, "_restore_claude_wrap_base_url", lambda *_args, **_kwargs: None)
|
||||
|
||||
def fake_write_tool_search(value: str, **kwargs: object) -> None:
|
||||
captured["write_tool_search_value"] = value
|
||||
captured["write_tool_search_kwargs"] = kwargs
|
||||
|
||||
monkeypatch.setattr(wrap_mod, "_write_claude_wrap_tool_search", fake_write_tool_search)
|
||||
monkeypatch.setattr(wrap_mod, "_restore_claude_wrap_tool_search", lambda *_a, **_k: None)
|
||||
monkeypatch.setattr(wrap_mod, "_print_telemetry_notice", lambda: None)
|
||||
|
||||
def fake_ensure_proxy(*args: object, **kwargs: object) -> tuple[None, int]:
|
||||
|
|
@ -209,6 +216,24 @@ def test_wrap_claude_tool_search_banner_line_still_accurate_when_active(
|
|||
assert "on-demand tool loading kept on" in output
|
||||
assert "keeps it on for this session" in output
|
||||
assert "DISABLED per your setting" not in output
|
||||
assert _captured["write_tool_search_value"] == "true"
|
||||
|
||||
|
||||
def test_wrap_claude_foundry_persists_disabled_tool_search_for_workers(
|
||||
runner: CliRunner, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
captured, output = _invoke_wrap_claude(
|
||||
runner,
|
||||
monkeypatch,
|
||||
env={
|
||||
"CLAUDE_CODE_USE_FOUNDRY": "1",
|
||||
"ANTHROPIC_FOUNDRY_BASE_URL": "https://tenant.services.ai.azure.com/anthropic",
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["child_env"]["ENABLE_TOOL_SEARCH"] == "false"
|
||||
assert captured["write_tool_search_value"] == "false"
|
||||
assert "on-demand tool loading DISABLED" in output
|
||||
|
||||
|
||||
def test_wrap_claude_vertex_passes_custom_base_url_to_proxy_before_child_redirect(
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def test_wrap_vscode_no_configure_prints_transparent_settings(tmp_path: Path) ->
|
|||
assert result.exit_code == 0, result.output
|
||||
assert not path.exists()
|
||||
assert "overrideProxyUrl" in result.output
|
||||
assert "overrideCapiUrl" in result.output
|
||||
assert "overrideAuthType" in result.output
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ def test_wrap_vscode_claude_configures_actual_port(tmp_path: Path) -> None:
|
|||
assert result.exit_code == 0, result.output
|
||||
env = json.loads(path.read_text(encoding="utf-8"))["env"]
|
||||
assert env["ANTHROPIC_BASE_URL"].startswith("http://127.0.0.1:9999/p/")
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "true"
|
||||
assert env["ENABLE_TOOL_SEARCH"] == "false"
|
||||
assert "Reload VS Code" in result.output
|
||||
assert captured["agent_type"] == "claude"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue