432 lines
15 KiB
Python
432 lines
15 KiB
Python
"""Unit tests for opencode-native provider-config synthesis."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import stat
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from omnigent.opencode_native_provider import (
|
|
DEFAULT_DATABRICKS_GATEWAY_MODEL,
|
|
OpenCodeGatewayResolution,
|
|
_gateway_endpoint_for_model,
|
|
_strip_jsonc_comments,
|
|
_strip_trailing_commas,
|
|
build_opencode_model_default_config,
|
|
build_opencode_omnigent_mcp_server,
|
|
build_opencode_provider_config,
|
|
maybe_merge_user_provider_config,
|
|
resolve_databricks_gateway,
|
|
write_opencode_provider_config,
|
|
)
|
|
|
|
|
|
def test_build_omnigent_mcp_server_points_serve_mcp_at_bridge_dir() -> None:
|
|
block = build_opencode_omnigent_mcp_server(Path("/tmp/bridge-xyz"))
|
|
assert set(block) == {"omnigent"}
|
|
entry = block["omnigent"]
|
|
assert entry["type"] == "local"
|
|
assert entry["enabled"] is True
|
|
cmd = entry["command"]
|
|
# Launches the SHARED serve-mcp relay, pointed at THIS bridge dir.
|
|
assert cmd[-3:] == ["serve-mcp", "--bridge-dir", "/tmp/bridge-xyz"]
|
|
assert "omnigent.claude_native_bridge" in cmd
|
|
assert entry.get("environment", {}).get("PYTHONUNBUFFERED") == "1"
|
|
|
|
|
|
def test_build_omnigent_mcp_server_honors_python_executable() -> None:
|
|
block = build_opencode_omnigent_mcp_server(Path("/tmp/b"), python_executable="/custom/python")
|
|
assert block["omnigent"]["command"][0] == "/custom/python"
|
|
|
|
|
|
def test_build_model_default_config_pins_model_without_provider_block() -> None:
|
|
cfg = build_opencode_model_default_config("anthropic/claude-sonnet-4-5")
|
|
assert cfg == {
|
|
"$schema": "https://opencode.ai/config.json",
|
|
"model": "anthropic/claude-sonnet-4-5",
|
|
}
|
|
# No provider block: opencode resolves the provider from the model prefix.
|
|
assert "provider" not in cfg
|
|
|
|
|
|
def test_model_default_config_round_trips_through_writer(tmp_path: Path) -> None:
|
|
path = write_opencode_provider_config(
|
|
tmp_path, build_opencode_model_default_config("openai/gpt-5.5")
|
|
)
|
|
written = json.loads(path.read_text(encoding="utf-8"))
|
|
assert written["model"] == "openai/gpt-5.5"
|
|
|
|
|
|
def test_qualified_model_joins_provider_and_endpoint() -> None:
|
|
res = OpenCodeGatewayResolution(
|
|
base_url="https://ws/serving-endpoints",
|
|
api_key="tok",
|
|
model_id="databricks-claude-sonnet-4-6",
|
|
provider_id="databricks-gateway",
|
|
)
|
|
assert res.qualified_model == "databricks-gateway/databricks-claude-sonnet-4-6"
|
|
|
|
|
|
def test_build_provider_config_shape() -> None:
|
|
res = OpenCodeGatewayResolution(
|
|
base_url="https://ws/serving-endpoints",
|
|
api_key="sekret",
|
|
model_id="databricks-claude-sonnet-4-6",
|
|
)
|
|
cfg = build_opencode_provider_config(res)
|
|
block = cfg["provider"]["databricks-gateway"]
|
|
assert block["npm"] == "@ai-sdk/openai-compatible"
|
|
assert block["options"] == {"baseURL": "https://ws/serving-endpoints", "apiKey": "sekret"}
|
|
assert "databricks-claude-sonnet-4-6" in block["models"]
|
|
assert cfg["$schema"].endswith("config.json")
|
|
|
|
|
|
def test_write_provider_config_is_0600_and_valid_json(tmp_path: Path) -> None:
|
|
res = OpenCodeGatewayResolution(
|
|
base_url="https://ws/serving-endpoints", api_key="tok", model_id="databricks-x"
|
|
)
|
|
path = write_opencode_provider_config(tmp_path, build_opencode_provider_config(res))
|
|
assert path == tmp_path / "opencode" / "opencode.json"
|
|
# Token-bearing config must not be world/group readable.
|
|
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
|
parsed = json.loads(path.read_text())
|
|
assert parsed["provider"]["databricks-gateway"]["options"]["apiKey"] == "tok"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_id,expected",
|
|
[
|
|
("databricks-claude-sonnet-4-6", "databricks-claude-sonnet-4-6"),
|
|
("databricks/databricks-gpt-5-5", "databricks-gpt-5-5"),
|
|
("claude-opus-4", None), # not a gateway endpoint name
|
|
("anthropic/claude-opus-4", None),
|
|
(None, None),
|
|
],
|
|
)
|
|
def test_gateway_endpoint_normalization(model_id: str | None, expected: str | None) -> None:
|
|
assert _gateway_endpoint_for_model(model_id) == expected
|
|
|
|
|
|
def test_resolve_gateway_none_without_profile() -> None:
|
|
assert resolve_databricks_gateway(None) is None
|
|
assert resolve_databricks_gateway("") is None
|
|
|
|
|
|
def test_resolve_gateway_none_when_sdk_absent(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
# Simulate databricks-sdk not installed: the import inside the function raises.
|
|
monkeypatch.setitem(sys.modules, "databricks.sdk.core", None)
|
|
assert resolve_databricks_gateway("oss") is None
|
|
|
|
|
|
def _install_fake_sdk(monkeypatch: pytest.MonkeyPatch, *, host: str, token: str | None) -> None:
|
|
fake = types.ModuleType("databricks.sdk.core")
|
|
|
|
class _Config:
|
|
def __init__(self, *, profile: str) -> None:
|
|
self.profile = profile
|
|
self.host = host
|
|
|
|
def authenticate(self) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
|
|
fake.Config = _Config # type: ignore[attr-defined]
|
|
# Ensure parent packages resolve for the dotted import.
|
|
monkeypatch.setitem(sys.modules, "databricks", types.ModuleType("databricks"))
|
|
monkeypatch.setitem(sys.modules, "databricks.sdk", types.ModuleType("databricks.sdk"))
|
|
monkeypatch.setitem(sys.modules, "databricks.sdk.core", fake)
|
|
|
|
|
|
def test_resolve_gateway_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_install_fake_sdk(monkeypatch, host="https://ws.cloud.databricks.com/", token="abc123")
|
|
res = resolve_databricks_gateway("oss", model_id="databricks-gpt-5-5")
|
|
assert res is not None
|
|
assert res.base_url == "https://ws.cloud.databricks.com/serving-endpoints"
|
|
assert res.api_key == "abc123"
|
|
assert res.model_id == "databricks-gpt-5-5"
|
|
assert res.qualified_model == "databricks-gateway/databricks-gpt-5-5"
|
|
|
|
|
|
def test_resolve_gateway_defaults_non_gateway_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_install_fake_sdk(monkeypatch, host="https://ws.databricks.com", token="t")
|
|
res = resolve_databricks_gateway("oss", model_id="claude-opus-4")
|
|
assert res is not None
|
|
assert res.model_id == DEFAULT_DATABRICKS_GATEWAY_MODEL
|
|
|
|
|
|
def test_resolve_gateway_none_when_no_token(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_install_fake_sdk(monkeypatch, host="https://ws.databricks.com", token=None)
|
|
assert resolve_databricks_gateway("oss") is None
|
|
|
|
|
|
def test_build_mcp_block_stdio_and_http() -> None:
|
|
from types import SimpleNamespace as N
|
|
|
|
from omnigent.opencode_native_provider import build_opencode_mcp_block
|
|
|
|
servers = [
|
|
N(
|
|
name="gh",
|
|
transport="stdio",
|
|
command="npx",
|
|
args=["-y", "server-github"],
|
|
env={"GITHUB_TOKEN": "x"},
|
|
url=None,
|
|
headers={},
|
|
databricks_profile=None,
|
|
),
|
|
N(
|
|
name="remote",
|
|
transport="http",
|
|
url="https://mcp.example/sse",
|
|
headers={"X-Key": "k"},
|
|
databricks_profile=None,
|
|
command=None,
|
|
args=[],
|
|
env={},
|
|
),
|
|
# Unrepresentable (stdio without a command) → skipped.
|
|
N(name="bad", transport="stdio", command=None, args=[], env={}, url=None, headers={}),
|
|
]
|
|
block = build_opencode_mcp_block(servers)
|
|
assert set(block) == {"gh", "remote"}
|
|
assert block["gh"] == {
|
|
"type": "local",
|
|
"command": ["npx", "-y", "server-github"],
|
|
"enabled": True,
|
|
"environment": {"GITHUB_TOKEN": "x"},
|
|
}
|
|
assert block["remote"] == {
|
|
"type": "remote",
|
|
"url": "https://mcp.example/sse",
|
|
"enabled": True,
|
|
"headers": {"X-Key": "k"},
|
|
}
|
|
|
|
|
|
def test_build_mcp_block_http_databricks_injects_bearer(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from types import SimpleNamespace as N
|
|
|
|
import omnigent.opencode_native_provider as prov
|
|
|
|
monkeypatch.setattr(prov, "_databricks_bearer_token", lambda _p: "tok123")
|
|
servers = [
|
|
N(
|
|
name="dbx",
|
|
transport="http",
|
|
url="https://ws/mcp",
|
|
headers={},
|
|
databricks_profile="oss",
|
|
command=None,
|
|
args=[],
|
|
env={},
|
|
)
|
|
]
|
|
block = prov.build_opencode_mcp_block(servers)
|
|
assert block["dbx"]["headers"] == {"Authorization": "Bearer tok123"}
|
|
|
|
|
|
def test_strip_jsonc_comments_removes_line_and_block_comments() -> None:
|
|
raw = """{
|
|
// line comment
|
|
"key": "value", /* block comment */
|
|
"nested": /* another */ "val"
|
|
}"""
|
|
cleaned = _strip_jsonc_comments(raw)
|
|
assert "//" not in cleaned
|
|
assert "/*" not in cleaned
|
|
assert "*/" not in cleaned
|
|
import json
|
|
|
|
parsed = json.loads(cleaned)
|
|
assert parsed == {"key": "value", "nested": "val"}
|
|
|
|
|
|
def test_strip_jsonc_comments_preserves_valid_json() -> None:
|
|
raw = '{"key": "value", "nested": {"a": 1}}'
|
|
assert _strip_jsonc_comments(raw) == raw
|
|
|
|
|
|
def test_strip_jsonc_comments_does_not_corrupt_urls() -> None:
|
|
"""URLs containing // must not have the // stripped."""
|
|
raw = '{"baseURL": "https://my-gateway/v1"}'
|
|
cleaned = _strip_jsonc_comments(raw)
|
|
import json
|
|
|
|
parsed = json.loads(cleaned)
|
|
assert parsed["baseURL"] == "https://my-gateway/v1"
|
|
|
|
|
|
def test_merge_user_provider_config_noop_without_user_config(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""No user config file → config returned unchanged."""
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "nonexistent"))
|
|
|
|
config = {"model": "anthropic/claude-sonnet-4-5"}
|
|
result = maybe_merge_user_provider_config(config)
|
|
assert result == config
|
|
|
|
|
|
def test_merge_user_provider_config_adds_user_providers(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""User's provider definitions are merged into the synthesized config."""
|
|
cfg_dir = tmp_path / "cfg" / "opencode"
|
|
cfg_dir.mkdir(parents=True)
|
|
(cfg_dir / "opencode.jsonc").write_text(
|
|
'{"provider": {"my-openai": {"npm": "@ai-sdk/openai-compatible", '
|
|
'"options": {"baseURL": "https://my-gateway/v1", "apiKey": "sk-"}, '
|
|
'"models": {"gpt-4": {"name": "gpt-4"}}}}}',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
|
|
config: dict[str, object] = {}
|
|
result = maybe_merge_user_provider_config(config)
|
|
|
|
assert "provider" in result
|
|
providers = result["provider"]
|
|
assert isinstance(providers, dict)
|
|
assert "my-openai" in providers
|
|
assert providers["my-openai"]["options"]["baseURL"] == "https://my-gateway/v1"
|
|
# Synthesized $schema should have been added.
|
|
assert "$schema" in result
|
|
|
|
|
|
def test_merge_user_provider_config_does_not_clobber_synthesized_providers(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""A user provider with the same key as a synthesized one is NOT overwritten."""
|
|
cfg_dir = tmp_path / "cfg" / "opencode"
|
|
cfg_dir.mkdir(parents=True)
|
|
(cfg_dir / "opencode.jsonc").write_text(
|
|
'{"provider": {"databricks-gateway": {"options": {"baseURL": "http://evil"}}}}',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
|
|
config = {
|
|
"provider": {
|
|
"databricks-gateway": {
|
|
"npm": "@ai-sdk/openai-compatible",
|
|
"options": {
|
|
"baseURL": "https://real-databricks/serving-endpoints",
|
|
"apiKey": "tok",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
result = maybe_merge_user_provider_config(config)
|
|
assert (
|
|
result["provider"]["databricks-gateway"]["options"]["baseURL"]
|
|
== "https://real-databricks/serving-endpoints"
|
|
)
|
|
|
|
|
|
def test_merge_user_provider_config_merges_alongside_synthesized_providers(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""User providers appear alongside the synthesized ones when keys differ."""
|
|
cfg_dir = tmp_path / "cfg" / "opencode"
|
|
cfg_dir.mkdir(parents=True)
|
|
(cfg_dir / "opencode.jsonc").write_text(
|
|
'{"provider": {"my-openai": {"options": {"baseURL": "http://my-gw/v1"}}}}',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
|
|
config = {
|
|
"provider": {
|
|
"databricks-gateway": {
|
|
"options": {"baseURL": "https://dbx/serving-endpoints", "apiKey": "tok"},
|
|
}
|
|
}
|
|
}
|
|
result = maybe_merge_user_provider_config(config)
|
|
providers = result["provider"]
|
|
assert "databricks-gateway" in providers
|
|
assert "my-openai" in providers
|
|
assert providers["my-openai"]["options"]["baseURL"] == "http://my-gw/v1"
|
|
|
|
|
|
def test_merge_user_provider_config_handles_jsonc_comments(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""The user's JSONC file with comments is parsed correctly."""
|
|
cfg_dir = tmp_path / "cfg" / "opencode"
|
|
cfg_dir.mkdir(parents=True)
|
|
(cfg_dir / "opencode.jsonc").write_text(
|
|
"{\n"
|
|
" // my custom provider\n"
|
|
' "provider": {\n'
|
|
' "my-openai": {\n'
|
|
' "options": {"baseURL": "https://my-gw/v1"}\n'
|
|
" }\n"
|
|
" }\n"
|
|
"}\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
|
|
result = maybe_merge_user_provider_config({})
|
|
assert result["provider"]["my-openai"]["options"]["baseURL"] == "https://my-gw/v1"
|
|
|
|
|
|
def test_strip_trailing_commas_object() -> None:
|
|
raw = '{"a": 1, "b": 2,}'
|
|
assert _strip_trailing_commas(raw) == '{"a": 1, "b": 2}'
|
|
|
|
|
|
def test_strip_trailing_commas_array() -> None:
|
|
raw = "[1, 2, 3,]"
|
|
assert _strip_trailing_commas(raw) == "[1, 2, 3]"
|
|
|
|
|
|
def test_strip_trailing_commas_nested() -> None:
|
|
raw = '{"a": [1, 2,], "b": {"c": 3,}}'
|
|
assert _strip_trailing_commas(raw) == '{"a": [1, 2], "b": {"c": 3}}'
|
|
|
|
|
|
def test_strip_trailing_commas_noop_without_trailing_commas() -> None:
|
|
raw = '{"a": 1, "b": [1, 2]}'
|
|
assert _strip_trailing_commas(raw) == raw
|
|
|
|
|
|
def test_strip_trailing_commas_preserves_commas_inside_strings() -> None:
|
|
"""Commas followed by } or ] inside string literals must NOT be stripped."""
|
|
raw = '{"note": "a, }", "list": "b, ]"}'
|
|
assert _strip_trailing_commas(raw) == raw
|
|
|
|
|
|
def test_strip_trailing_commas_nested_with_string_values() -> None:
|
|
"""Trailing commas outside strings stripped; commas inside strings preserved."""
|
|
raw = '{"a": "x, }", "b": [1, 2,],}'
|
|
expected = '{"a": "x, }", "b": [1, 2]}'
|
|
assert _strip_trailing_commas(raw) == expected
|
|
|
|
|
|
def test_merge_user_provider_config_handles_jsonc_trailing_commas(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""Trailing commas in JSONC are handled (they're valid in JSONC but not JSON)."""
|
|
cfg_dir = tmp_path / "cfg" / "opencode"
|
|
cfg_dir.mkdir(parents=True)
|
|
(cfg_dir / "opencode.jsonc").write_text(
|
|
"{\n"
|
|
' "provider": {\n'
|
|
' "my-openai": {\n'
|
|
' "options": {"baseURL": "https://my-gw/v1",},\n' # trailing comma
|
|
" },\n" # trailing comma
|
|
" },\n" # trailing comma
|
|
"}\n",
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
|
|
|
result = maybe_merge_user_provider_config({})
|
|
assert result["provider"]["my-openai"]["options"]["baseURL"] == "https://my-gw/v1"
|