Files
computer-playbook/tests/unit/python/roles/agents/test_openclaw_model_provider.py
T
kevinveenbirkenbachandClaude Opus 5 7c13e96c96 Merge branch 'main' into feature/store-i18n
Twelve files conflicted; most resolutions keep both sides rather than pick
one.

cli env.py and compose.py: both branches extend the same spots. The tools
override is appended before write_override, because that call filters the
mirror override to the compose files it is handed. compose.py keeps this
branch's CACHE_ROLE_SHELL_DIR, since the cache scripts moved into a role and
main's literal scripts/docker/cache path is their old home, and takes main's
mirrored init image alongside it.

utils/docker/image/discovery.py: this branch's version_variants expansion
plus main's whole runner-image block, which runner_mirror imports.

sys-svc-compose network create: main's side wins on substance and this
branch's on structure. main carries eda6249d7b, the fix for a probe that
matched an id prefix and let a sibling shadow the exact name; this branch had
refactored the task into create/overlay.yml and create/bridge.yml but brought
back the docker_network_info probe that fix replaced. The subfiles stay and
now probe through the exact-name lookup. main's half also read
SWARM_CLUSTER_AVAILABLE, which this branch removed in favour of
IS_SWARM_MODE, as the merge base confirms, so the gates follow this branch.
The lookup's import follows this branch's get_entity_name to entity_name
rename.

web-app-pixelfed: the custom entrypoints stay removed, since the new image
brings its own and the variables behind them no longer exist, and main's
host_alias=true comes along.

web-app-suitecrm: main's session.yaml and its config/packages path, because
only that version wires framework.session and only that directory is read by
Symfony.

web-app-odoo: main's onionOptions import is required by the test.use below
it; this branch's test-seaweedfs registration stays.

Four defects surfaced that no conflict marker shows.

The auto-merge of web-app-suitecrm/vars/main.yml duplicated
SUITECRM_SESSION_CONFIG_HOST, which ansible-lint rejects. Removing it exposed
the second: SUITECRM_SESSION_CONFIG_DOCKER still named config/session.yaml
while the mount had moved to config/packages, so the freshness comparison in
tasks/01_manager_ops.yml would have tested a path nothing writes.

svc-db-elasticsearch carried environment: on two include_tasks, which Ansible
accepts on include_role only. ELASTIC_PW now sits on the five tasks that read
it, which is also the tighter scope for a credential.

The openclaw templates render NETWORK_TRUSTED_PROXY_CIDRS while their test
renderer passes only its explicit kwargs, so seven tests raised Undefined.
That predates the merge: the template line is on this branch and not on main,
and both sides share the same test file.

test_runner_mirror neutralises four environment variables under clear=False
and missed three, so the gpu and tools overrides leaked in from the container
and the assertion on the layered list broke. It now neutralises all seven.

make quality-high passes both phases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 10:57:22 +02:00

97 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, select_autoescape
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 = {
"NETWORK_TRUSTED_PROXY_CIDRS": ["127.0.0.1/32", "::1/128", "10.0.0.0/8"],
"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=select_autoescape()
)
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()