mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Both template tests listed the filters they hand to jinja by name, which made the FilterModule a second source: adding a filter to it left these two behind, and the templates that used it failed to compile with "No filter named …" in test-unit and test-integration at once. The list is derived from the module now, so a filter reaches the tests the moment the role can use it. The two entries that are not filters at all, to_json and bool, stay spelled out because jinja needs them and the module does not ship them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
3.2 KiB
Python
94 lines
3.2 KiB
Python
"""OpenClaw refuses to boot on a model entry that misses a key.
|
|
|
|
The gateway validates its whole configuration before it serves anything, and
|
|
a rejected file is not a degraded start but a crash loop: OpenClaw retries,
|
|
trips its own restart breaker after four unclean boots, and the container
|
|
exits 128 while the deploy sits in the health check until it gives up.
|
|
|
|
The provider block reached the repository without ever being rendered in a
|
|
test - every case here passed OPENCLAW_LITELLM_ENABLED=False, so the block was
|
|
skipped and its shape was never checked against anything.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import unittest
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
OPENCLAW = PROJECT_ROOT / "roles/web-app-openclaw/templates"
|
|
FILTERS = PROJECT_ROOT / "plugins/filter/mcp/authorization.py"
|
|
|
|
MODEL = "qwen2.5:0.5b"
|
|
|
|
CONTEXT = {
|
|
"OPENCLAW_URL": "https://claw.example.org",
|
|
"OPENCLAW_LITELLM_ENABLED": True,
|
|
"OPENCLAW_LITELLM_PROVIDER": "litellm",
|
|
"OPENCLAW_LITELLM_MODEL": MODEL,
|
|
"OPENCLAW_LITELLM_BASE_URL": "http://litellm:4000/v1",
|
|
"OPENCLAW_LITELLM_API_KEY": "sk-" + "x" * 20,
|
|
"OPENCLAW_MCP_SERVERS": [],
|
|
}
|
|
|
|
|
|
def _filters() -> dict:
|
|
spec = importlib.util.spec_from_file_location("mcp_authorization", FILTERS)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
filters = dict(module.FilterModule().filters())
|
|
filters.update({"to_json": json.dumps, "bool": bool})
|
|
return filters
|
|
|
|
|
|
def _render(**overrides) -> dict:
|
|
"""Return the gateway config as OpenClaw would parse it.
|
|
|
|
Args:
|
|
overrides: context values replacing the defaults.
|
|
"""
|
|
env = Environment(loader=FileSystemLoader(str(OPENCLAW)), autoescape=False) # noqa: S701 - renders configuration, not markup
|
|
env.filters.update(_filters())
|
|
return json.loads(
|
|
env.get_template("openclaw.json.j2").render(**{**CONTEXT, **overrides})
|
|
)
|
|
|
|
|
|
class TestOpenClawModelProvider(unittest.TestCase):
|
|
def _entry(self, **overrides) -> dict:
|
|
"""Return the provider's single model entry.
|
|
|
|
Args:
|
|
overrides: context values replacing the defaults.
|
|
"""
|
|
return _render(**overrides)["models"]["providers"]["litellm"]["models"][0]
|
|
|
|
def test_the_model_entry_carries_both_keys_the_gateway_validates(self) -> None:
|
|
self.assertEqual({"id": MODEL, "name": MODEL}, self._entry())
|
|
|
|
def test_the_name_matches_the_id(self) -> None:
|
|
"""Which key reaches the API is the upstream's choice, not ours."""
|
|
entry = self._entry()
|
|
self.assertEqual(entry["id"], entry["name"])
|
|
|
|
def test_the_primary_agent_model_names_the_same_provider(self) -> None:
|
|
rendered = _render()
|
|
self.assertEqual(
|
|
f"litellm/{MODEL}",
|
|
rendered["agents"]["defaults"]["model"]["primary"],
|
|
)
|
|
self.assertIn("litellm", rendered["models"]["providers"])
|
|
|
|
def test_the_block_disappears_when_litellm_is_off(self) -> None:
|
|
rendered = _render(OPENCLAW_LITELLM_ENABLED=False)
|
|
self.assertNotIn("models", rendered)
|
|
self.assertNotIn("agents", rendered)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|