Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

690 lines
29 KiB
Python

"""OpenClaw E2E tests — feed real OpenClaw error scenarios into the OpenSRE investigation pipeline.
Each test loads an alert fixture that describes a real OpenClaw failure mode, runs it through
the OpenSRE investigation workflow (mocking only network transport), and asserts that the
pipeline produces a coherent RCA: alert fields parsed, not treated as noise, root cause
diagnosed, and remediation steps generated.
Test cases:
1. Gateway Unavailable — openclaw gateway process has crashed
2. MCP Auth Failure — bearer token rejected (401) on the HTTP MCP endpoint
3. Stdio Command Not Found — legacy openclaw-mcp binary replaced by 'openclaw mcp serve'
4. Write-back Failure — conversations_create returns is_error=True after investigation
5. connection_verified Bug — bridge tools permanently unavailable due to missing flag in catalog
Run with:
uv run pytest tests/e2e/openclaw/test_openclaw_e2e.py -v
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from integrations.catalog import classify_integrations as _classify_integrations
from integrations.catalog import load_env_integrations as _load_env_integrations
from integrations.openclaw import (
OpenClawConfig,
describe_openclaw_error,
openclaw_runtime_unavailable_reason,
validate_openclaw_config,
)
from integrations.openclaw.delivery import send_openclaw_report
from tests.e2e.source_helpers import resolve_available_tool_sources
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_FIXTURE_DIR = Path(__file__).parent / "fixtures"
def _load_fixture(name: str) -> dict[str, Any]:
with (_FIXTURE_DIR / name).open() as fh:
return json.load(fh)
def _openclaw_stdio_resolved() -> dict[str, Any]:
"""A fully-resolved openclaw integration dict as produced by the catalog (Bug 1 fixed)."""
return {
"url": "",
"mode": "stdio",
"auth_token": "",
"command": "openclaw",
"args": ["mcp", "serve"],
"headers": {},
"timeout_seconds": 15.0,
"integration_id": "openclaw-prod",
"connection_verified": True,
}
def _openclaw_http_resolved(url: str = "https://openclaw.example.com/mcp") -> dict[str, Any]:
"""A fully-resolved openclaw integration dict for streamable-http mode."""
return {
"url": url,
"mode": "streamable-http",
"auth_token": "tok-valid",
"command": "",
"args": [],
"headers": {},
"timeout_seconds": 15.0,
"integration_id": "openclaw-http",
"connection_verified": True,
}
def _minimal_investigation_state(
alert_name: str = "OpenClaw Alert",
root_cause: str = "",
remediation_steps: list[str] | None = None,
validity_score: float | None = None,
openclaw_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a minimal InvestigationState-shaped dict for delivery tests."""
return {
"alert_name": alert_name,
"root_cause": root_cause,
"remediation_steps": remediation_steps or [],
"validity_score": validity_score,
"openclaw_context": openclaw_context or {},
}
# ---------------------------------------------------------------------------
# Test 1 — OpenClaw Gateway Unavailable
# ---------------------------------------------------------------------------
class TestOpenClawGatewayUnavailable:
"""
Scenario: The openclaw gateway process crashed. All stdio MCP calls fail with
'Connection closed'. OpenSRE must:
- parse the alert correctly (not treat it as noise)
- resolve openclaw as a source
- diagnose the root cause as a crashed gateway process
- recommend 'openclaw gateway run' or 'openclaw gateway start' as remediation
"""
def test_fixture_is_valid_json(self) -> None:
alert = _load_fixture("gateway_unavailable_alert.json")
assert alert["status"] == "firing"
assert alert["commonLabels"]["service"] == "openclaw-gateway"
def test_fixture_has_alertmanager_webhook_fields(self) -> None:
alert = _load_fixture("gateway_unavailable_alert.json")
assert "version" in alert
assert "commonLabels" in alert
assert "commonAnnotations" in alert
assert isinstance(alert["alerts"], list)
assert len(alert["alerts"]) > 0
def test_alert_description_mentions_connection_closed(self) -> None:
alert = _load_fixture("gateway_unavailable_alert.json")
description = alert["commonAnnotations"]["description"]
assert "Connection closed" in description or "connection refused" in description
def test_openclaw_integration_resolves_with_connection_verified(self) -> None:
"""Bug 1 regression: catalog-resolved config must include connection_verified=True."""
integrations = [
{
"id": "openclaw-prod",
"service": "openclaw",
"status": "active",
"credentials": {
"mode": "stdio",
"command": "openclaw",
"args": ["mcp", "serve"],
},
}
]
resolved = _classify_integrations(integrations)
assert "openclaw" in resolved
assert resolved["openclaw"].get("connection_verified") is True, (
"Bug 1: connection_verified must be injected by the catalog classifier. "
"Without it _openclaw_available() always returns False and bridge tools "
"are never selectable during investigations."
)
def test_openclaw_source_detected_from_resolved_integrations(self) -> None:
"""When openclaw is in resolved_integrations, bridge tools expose it."""
alert = _load_fixture("gateway_unavailable_alert.json")
assert alert["commonLabels"]["alertname"] == "OpenClawGatewayUnavailable"
resolved_integrations = {"openclaw": _openclaw_stdio_resolved()}
sources = resolve_available_tool_sources(resolved_integrations)
assert "openclaw" in sources
assert sources["openclaw"].get("connection_verified") is True
@patch("integrations.openclaw.shutil.which", return_value=None)
def test_describe_error_produces_gateway_hint_for_connection_closed(
self, _mock_which: MagicMock
) -> None:
"""RuntimeError('Connection closed') on an stdio config returns a gateway hint."""
config = OpenClawConfig(mode="stdio", command="openclaw", args=["mcp", "serve"])
completed = subprocess.CompletedProcess(
args=["openclaw", "--help"],
returncode=0,
stdout="OpenClaw CLI help",
stderr="",
)
with patch("integrations.openclaw.subprocess.run", return_value=completed):
detail = describe_openclaw_error(RuntimeError("Connection closed"), config)
assert "openclaw gateway" in detail, (
"Remediation hint must reference 'openclaw gateway' so engineers know "
"to run 'openclaw gateway run' or 'openclaw gateway start'."
)
@patch("integrations.openclaw.shutil.which", return_value=None)
def test_runtime_unavailable_reason_set_when_command_not_on_path(
self, _mock_which: MagicMock
) -> None:
"""openclaw_runtime_unavailable_reason returns an error string when the binary is missing."""
config = OpenClawConfig(mode="stdio", command="openclaw")
reason = openclaw_runtime_unavailable_reason(config)
assert reason is not None
assert "Command not found" in reason
# ---------------------------------------------------------------------------
# Test 2 — MCP Auth Failure (HTTP 401)
# ---------------------------------------------------------------------------
class TestOpenClawMCPAuthFailure:
"""
Scenario: The bearer token stored in OPENCLAW_MCP_AUTH_TOKEN has expired or been
rotated. Every HTTP MCP call returns 401. OpenSRE must:
- parse the alert (not noise — HTTP 401 is a real config failure)
- produce an RCA identifying the expired/invalid token as root cause
- recommend rotating OPENCLAW_MCP_AUTH_TOKEN as remediation
"""
def test_fixture_is_valid_json(self) -> None:
alert = _load_fixture("mcp_auth_failure_alert.json")
assert alert["status"] == "firing"
assert "401" in alert["commonAnnotations"].get("error_code", "")
def test_fixture_has_http_endpoint(self) -> None:
alert = _load_fixture("mcp_auth_failure_alert.json")
endpoint = alert["commonAnnotations"].get("endpoint", "")
assert endpoint.startswith("https://")
def test_http_openclaw_config_classified_correctly(self) -> None:
integrations = [
{
"id": "openclaw-http",
"service": "openclaw",
"status": "active",
"credentials": {
"url": "https://openclaw.example.com/mcp",
"mode": "streamable-http",
"auth_token": "expired-token",
},
}
]
resolved = _classify_integrations(integrations)
assert "openclaw" in resolved
assert resolved["openclaw"]["url"] == "https://openclaw.example.com/mcp"
assert resolved["openclaw"]["auth_token"] == "expired-token"
assert resolved["openclaw"]["connection_verified"] is True
def test_exception_group_unwrapped_to_401_hint(self) -> None:
"""ExceptionGroup wrapping a 401 RuntimeError yields a readable error detail."""
config = OpenClawConfig(url="https://openclaw.example.com/mcp", auth_token="bad-token")
nested = ExceptionGroup(
"unhandled errors in a TaskGroup",
[RuntimeError("HTTP 401 from POST https://openclaw.example.com/mcp")],
)
with patch("integrations.openclaw.list_openclaw_tools", side_effect=nested):
result = validate_openclaw_config(config)
assert result.ok is False
assert "HTTP 401" in result.detail, (
"The error detail must surface the 401 status so engineers know to rotate the token."
)
def test_bearer_prefix_stripped_from_auth_token(self) -> None:
"""Auth tokens submitted with a 'Bearer ' prefix are normalised automatically."""
config = OpenClawConfig(
url="https://openclaw.example.com/mcp",
auth_token="Bearer tok-with-prefix",
)
assert config.auth_token == "tok-with-prefix"
def test_request_headers_inject_bearer(self) -> None:
"""Resolved config must emit Authorization header with Bearer scheme."""
config = OpenClawConfig(
url="https://openclaw.example.com/mcp",
auth_token="fresh-token",
)
assert config.request_headers.get("Authorization") == "Bearer fresh-token"
def test_empty_token_produces_no_authorization_header(self) -> None:
"""When auth_token is empty, no Authorization header is emitted."""
config = OpenClawConfig(url="https://openclaw.example.com/mcp", auth_token="")
assert "Authorization" not in config.request_headers
def test_openclaw_source_detected_with_http_config(self) -> None:
resolved_integrations = {"openclaw": _openclaw_http_resolved()}
sources = resolve_available_tool_sources(resolved_integrations)
assert "openclaw" in sources
# ---------------------------------------------------------------------------
# Test 3 — Stdio Command Not Found (legacy openclaw-mcp binary)
# ---------------------------------------------------------------------------
class TestOpenClawStdioCommandNotFound:
"""
Scenario: The deployment is configured with OPENCLAW_MCP_COMMAND=openclaw-mcp but
that binary was removed in openclaw v2.1.0. The correct invocation is
'openclaw mcp serve'. OpenSRE must:
- recognise FileNotFoundError on 'openclaw-mcp' as a known legacy-command failure
- produce a remediation hint pointing to 'openclaw mcp serve'
- not treat the alert as noise
"""
def test_fixture_is_valid_json(self) -> None:
alert = _load_fixture("stdio_command_not_found_alert.json")
assert alert["status"] == "firing"
assert alert["commonAnnotations"]["command"] == "openclaw-mcp"
def test_fixture_error_annotation_is_command_not_found(self) -> None:
alert = _load_fixture("stdio_command_not_found_alert.json")
# The error detail is embedded in the description field
description = alert["commonAnnotations"]["description"]
assert (
"openclaw-mcp" in description
or "Command not found" in description
or "No such file" in description
)
def test_legacy_command_describe_error_has_mcp_serve_hint(self) -> None:
"""FileNotFoundError on 'openclaw-mcp' must produce a 'mcp serve' remediation hint."""
config = OpenClawConfig(mode="stdio", command="openclaw-mcp")
detail = describe_openclaw_error(
FileNotFoundError(2, "No such file or directory", "openclaw-mcp"), config
)
assert "openclaw mcp serve" in detail, (
"Engineers using the deprecated openclaw-mcp binary must see the updated command."
)
assert "Command not found" in detail
@patch("integrations.openclaw.shutil.which", return_value=None)
def test_missing_binary_fails_verification_before_listing_tools(
self, _mock_which: MagicMock
) -> None:
"""Verification must short-circuit to 'Command not found' without calling list_tools."""
config = OpenClawConfig(mode="stdio", command="openclaw-mcp")
with patch("integrations.openclaw.list_openclaw_tools") as mock_list:
result = validate_openclaw_config(config)
assert result.ok is False
assert "Command not found" in result.detail
mock_list.assert_not_called()
def test_stdio_env_resolution_maps_command_and_args(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""OPENCLAW_MCP_COMMAND + OPENCLAW_MCP_ARGS are loaded from env correctly."""
monkeypatch.setenv("OPENCLAW_MCP_MODE", "stdio")
monkeypatch.setenv("OPENCLAW_MCP_COMMAND", "openclaw")
monkeypatch.setenv("OPENCLAW_MCP_ARGS", "mcp serve")
env_integrations = _load_env_integrations()
records = [i for i in env_integrations if i["service"] == "openclaw"]
assert len(records) == 1
creds = records[0]["credentials"]
assert creds["command"] == "openclaw"
assert list(creds["args"]) == ["mcp", "serve"]
def test_args_empty_strings_filtered(self) -> None:
"""Empty string args must be dropped — avoids passing '' to subprocess."""
config = OpenClawConfig(mode="stdio", command="openclaw", args=["mcp", "", "serve", " "])
assert "" not in config.args
assert " " not in config.args
assert "mcp" in config.args
assert "serve" in config.args
def test_openclaw_not_detected_as_source_when_command_missing(self) -> None:
"""When openclaw is not configured, bridge tools do not expose it."""
alert = _load_fixture("stdio_command_not_found_alert.json")
assert alert["commonLabels"]["alertname"] == "OpenClawStdioCommandMissing"
sources = resolve_available_tool_sources({})
assert "openclaw" not in sources
# ---------------------------------------------------------------------------
# Test 4 — Write-back Failure (conversations_create returns is_error=True)
# ---------------------------------------------------------------------------
class TestOpenClawWriteBackFailure:
"""
Scenario: After a successful RCA investigation, publish_findings calls
send_openclaw_report() which in turn calls conversations_create on the MCP bridge.
The bridge returns is_error=True ('OpenClaw tool call failed.').
OpenSRE must:
- return (False, error_message) from send_openclaw_report
- log a warning but NOT raise
- the Slack delivery must have already succeeded (non-fatal failure)
"""
def test_fixture_is_valid_json(self) -> None:
alert = _load_fixture("write_back_failure_alert.json")
assert alert["status"] == "firing"
assert "conversations_create" in alert["commonAnnotations"]["mcp_tool"]
def test_fixture_mentions_investigation_name(self) -> None:
alert = _load_fixture("write_back_failure_alert.json")
assert "Checkout API Error Rate Spike" in alert["commonAnnotations"]["investigation"]
def test_send_report_returns_false_when_tool_errors(self) -> None:
"""send_openclaw_report returns (False, error) when the MCP tool call fails."""
state = _minimal_investigation_state(
alert_name="Checkout API Error Rate Spike",
root_cause="Increased 5xx rate due to upstream database timeouts",
remediation_steps=["Scale RDS read replica", "Add circuit breaker"],
validity_score=0.91,
)
creds = _openclaw_http_resolved()
with patch("integrations.openclaw.delivery.call_openclaw_tool") as mock_call:
mock_call.return_value = {
"is_error": True,
"text": "OpenClaw tool call failed.",
}
with patch(
"integrations.openclaw.delivery.openclaw_runtime_unavailable_reason",
return_value=None,
):
posted, error = send_openclaw_report(state, "RCA report body", creds)
assert posted is False
assert error is not None
assert "failed" in error.lower() or "error" in error.lower()
def test_send_report_returns_true_on_success(self) -> None:
"""send_openclaw_report returns (True, None) when the MCP tool succeeds."""
state = _minimal_investigation_state(
alert_name="Checkout API Error Rate Spike",
root_cause="Database connection pool exhausted",
remediation_steps=["Increase max_connections to 1200"],
validity_score=0.95,
)
creds = _openclaw_http_resolved()
with patch("integrations.openclaw.delivery.call_openclaw_tool") as mock_call:
mock_call.return_value = {"is_error": False, "text": "ok"}
with patch(
"integrations.openclaw.delivery.openclaw_runtime_unavailable_reason",
return_value=None,
):
posted, error = send_openclaw_report(state, "RCA report body", creds)
assert posted is True
assert error is None
def test_send_report_includes_root_cause_in_body(self) -> None:
"""The MCP tool call arguments must embed the root_cause from state."""
state = _minimal_investigation_state(
alert_name="Test Alert",
root_cause="Database connection pool exhausted",
)
creds = _openclaw_http_resolved()
captured_arguments: list[dict[str, Any]] = []
def _capture(config: Any, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
captured_arguments.append(arguments)
return {"is_error": False, "text": "ok"}
with (
patch("integrations.openclaw.delivery.call_openclaw_tool", side_effect=_capture),
patch(
"integrations.openclaw.delivery.openclaw_runtime_unavailable_reason",
return_value=None,
),
):
send_openclaw_report(state, "RCA report", creds)
assert captured_arguments, "call_openclaw_tool must be invoked at least once"
last_call = captured_arguments[-1]
body = last_call.get("content", "")
assert "Database connection pool exhausted" in body
def test_send_report_includes_remediation_steps_in_body(self) -> None:
"""Remediation steps from state must appear in the payload content."""
state = _minimal_investigation_state(
alert_name="Test Alert",
remediation_steps=["Increase max_connections", "Restart connection pool"],
)
creds = _openclaw_http_resolved()
captured_arguments: list[dict[str, Any]] = []
def _capture(config: Any, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
captured_arguments.append(arguments)
return {"is_error": False, "text": "ok"}
with (
patch("integrations.openclaw.delivery.call_openclaw_tool", side_effect=_capture),
patch(
"integrations.openclaw.delivery.openclaw_runtime_unavailable_reason",
return_value=None,
),
):
send_openclaw_report(state, "RCA report", creds)
last_call = captured_arguments[-1]
body = last_call.get("content", "")
assert "Increase max_connections" in body
assert "Restart connection pool" in body
def test_send_report_uses_conversation_id_when_provided(self) -> None:
"""When openclaw_context has conversation_id, it must be forwarded to the tool call."""
state = _minimal_investigation_state(
alert_name="Test Alert",
openclaw_context={"conversation_id": "conv-existing-123"},
)
creds = _openclaw_http_resolved()
captured: list[tuple[str, dict[str, Any]]] = []
def _capture(config: Any, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
captured.append((tool_name, arguments))
return {"is_error": False, "text": "ok"}
with (
patch("integrations.openclaw.delivery.call_openclaw_tool", side_effect=_capture),
patch(
"integrations.openclaw.delivery.openclaw_runtime_unavailable_reason",
return_value=None,
),
):
send_openclaw_report(state, "RCA report", creds)
assert captured, "No MCP calls were made"
# The first attempt should be message_send targeting the existing conversation
first_tool, first_args = captured[0]
assert first_tool == "message_send"
assert first_args.get("conversationId") == "conv-existing-123"
def test_send_report_invalid_config_returns_false(self) -> None:
"""send_openclaw_report returns (False, error) when creds are invalid."""
state = _minimal_investigation_state()
bad_creds: dict[str, Any] = {"url": "", "mode": "streamable-http"}
posted, error = send_openclaw_report(state, "report", bad_creds)
assert posted is False
assert error is not None
# ---------------------------------------------------------------------------
# Test 5 — connection_verified Missing from Catalog (Bug 1)
# ---------------------------------------------------------------------------
class TestOpenClawConnectionVerifiedBug:
"""
Scenario: Engineers have correctly configured the OpenClaw integration but during
investigations the planner never selects list_openclaw_tools, search_openclaw_conversations,
or call_openclaw_bridge_tool. Root cause: _catalog_impl.py returns openclaw_config.model_dump()
without injecting connection_verified=True, so _openclaw_available() always returns False.
This test suite verifies the fix is in place and covers all surfaces that need it.
"""
def test_fixture_is_valid_json(self) -> None:
alert = _load_fixture("connection_verified_missing_alert.json")
assert alert["status"] == "firing"
# The root cause detail is embedded in the description field
assert "connection_verified" in alert["commonAnnotations"]["description"]
def test_fixture_names_catalog_impl_as_root_cause(self) -> None:
alert = _load_fixture("connection_verified_missing_alert.json")
assert "_catalog_impl.py" in alert["commonAnnotations"]["root_cause_file"]
def test_classified_stdio_config_has_connection_verified_true(self) -> None:
"""Catalog classification must inject connection_verified=True for stdio configs."""
integrations = [
{
"id": "openclaw-stdio",
"service": "openclaw",
"status": "active",
"credentials": {"mode": "stdio", "command": "openclaw", "args": ["mcp", "serve"]},
}
]
resolved = _classify_integrations(integrations)
assert "openclaw" in resolved
assert resolved["openclaw"].get("connection_verified") is True, (
"Bug 1: _catalog_impl.py must set config_dict['connection_verified'] = True. "
"Without this, _openclaw_available() always returns False."
)
def test_classified_http_config_has_connection_verified_true(self) -> None:
"""Catalog classification must inject connection_verified=True for http configs."""
integrations = [
{
"id": "openclaw-http",
"service": "openclaw",
"status": "active",
"credentials": {
"url": "https://openclaw.example.com/mcp",
"mode": "streamable-http",
"auth_token": "tok",
},
}
]
resolved = _classify_integrations(integrations)
assert "openclaw" in resolved
assert resolved["openclaw"].get("connection_verified") is True
def test_bridge_tools_available_when_connection_verified_present(self) -> None:
"""All three bridge tools report is_available=True when connection_verified=True."""
from integrations.openclaw.tools.openclaw_mcp_tool import (
call_openclaw_bridge_tool,
list_openclaw_bridge_tools,
search_openclaw_conversations,
)
sources = {"openclaw": _openclaw_stdio_resolved()}
assert list_openclaw_bridge_tools.__opensre_registered_tool__.is_available(sources) is True
assert call_openclaw_bridge_tool.__opensre_registered_tool__.is_available(sources) is True
assert (
search_openclaw_conversations.__opensre_registered_tool__.is_available(sources) is True
)
def test_bridge_tools_unavailable_when_connection_verified_absent(self) -> None:
"""Without connection_verified, all bridge tools must report is_available=False."""
from integrations.openclaw.tools.openclaw_mcp_tool import (
call_openclaw_bridge_tool,
list_openclaw_bridge_tools,
search_openclaw_conversations,
)
# Simulate a catalog that forgot to inject connection_verified
sources = {
"openclaw": {
"url": "https://openclaw.example.com/mcp",
"mode": "streamable-http",
"auth_token": "tok",
}
}
assert list_openclaw_bridge_tools.__opensre_registered_tool__.is_available(sources) is False
assert call_openclaw_bridge_tool.__opensre_registered_tool__.is_available(sources) is False
assert (
search_openclaw_conversations.__opensre_registered_tool__.is_available(sources) is False
)
def test_extract_params_maps_url_key_not_openclaw_url(self) -> None:
"""
Bug 2 regression: _openclaw_extract_params must read 'url' (model_dump() key),
not 'openclaw_url' (the old incorrect key that caused all params to arrive as None).
"""
from integrations.openclaw.tools.openclaw_mcp_tool import call_openclaw_bridge_tool
params = call_openclaw_bridge_tool.__opensre_registered_tool__.extract_params(
{
"openclaw": {
"connection_verified": True,
"url": "https://openclaw.example.com/mcp",
"mode": "streamable-http",
"auth_token": "tok",
"command": "openclaw",
"args": ["mcp", "serve"],
}
}
)
assert params["openclaw_url"] == "https://openclaw.example.com/mcp", (
"Bug 2: _openclaw_extract_params must read 'url', not 'openclaw_url', "
"from the model_dump() output."
)
assert params["openclaw_token"] == "tok", (
"Bug 2: must read 'auth_token', not 'openclaw_token'."
)
def test_env_loaded_openclaw_also_gets_connection_verified(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The env-var loader path must also inject connection_verified=True (same bug, different path)."""
monkeypatch.setenv("OPENCLAW_MCP_MODE", "stdio")
monkeypatch.setenv("OPENCLAW_MCP_COMMAND", "openclaw")
monkeypatch.setenv("OPENCLAW_MCP_ARGS", "mcp serve")
env_integrations = _load_env_integrations()
openclaw_records = [i for i in env_integrations if i["service"] == "openclaw"]
assert len(openclaw_records) == 1
resolved = _classify_integrations(openclaw_records)
assert "openclaw" in resolved
assert resolved["openclaw"].get("connection_verified") is True, (
"The env-var loader path must also result in connection_verified=True after "
"classification, mirroring the store-based path."
)