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
193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
"""FixtureOpenClawBackend for synthetic investigation tests.
|
|
|
|
Intercepts all calls to integrations.openclaw.call_openclaw_tool and
|
|
integrations.openclaw.list_openclaw_tools, serving fixture data without
|
|
any live OpenClaw process.
|
|
|
|
Usage in run_suite.py
|
|
---------------------
|
|
backend = FixtureOpenClawBackend(scenario)
|
|
with backend.patch():
|
|
state = run_investigation(..., resolved_integrations={
|
|
"openclaw": {
|
|
"connection_verified": True,
|
|
"mode": "stdio",
|
|
"command": "openclaw",
|
|
"args": ["mcp", "serve"],
|
|
"url": "",
|
|
"auth_token": "",
|
|
}
|
|
})
|
|
|
|
The backend intercepts calls at the integration layer so the full tool call
|
|
path (is_available check → extract_params → _resolve_config →
|
|
openclaw_runtime_unavailable_reason bypass → invoke_openclaw_mcp_tool) is
|
|
exercised, except the final network/subprocess hop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from typing import TYPE_CHECKING, Any
|
|
from unittest.mock import patch
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Generator
|
|
|
|
from tests.synthetic.openclaw.scenario_loader import OpenClawScenario
|
|
|
|
|
|
class FixtureOpenClawBackend:
|
|
"""Serves scenario fixture data in place of live OpenClaw MCP calls."""
|
|
|
|
def __init__(self, scenario: OpenClawScenario) -> None:
|
|
self._scenario = scenario
|
|
|
|
# ------------------------------------------------------------------
|
|
# Core dispatch
|
|
# ------------------------------------------------------------------
|
|
|
|
def call_tool(
|
|
self,
|
|
_config: Any,
|
|
tool_name: str,
|
|
arguments: dict[str, object] | None = None,
|
|
) -> dict[str, object]:
|
|
"""Intercept call_openclaw_tool and return fixture data."""
|
|
handler = self._tool_handlers().get(tool_name)
|
|
if handler is None:
|
|
return {
|
|
"is_error": True,
|
|
"text": f"FixtureOpenClawBackend: unknown tool '{tool_name}'",
|
|
"tool": tool_name,
|
|
"arguments": arguments or {},
|
|
"content": [],
|
|
"structured_content": None,
|
|
}
|
|
return handler(arguments or {}) # type: ignore[no-any-return]
|
|
|
|
def list_tools(self, _config: Any) -> list[dict[str, object]]:
|
|
"""Intercept list_openclaw_tools and return fixture tool descriptors."""
|
|
return self._scenario.fixture_tools
|
|
|
|
# ------------------------------------------------------------------
|
|
# Patch context manager
|
|
# ------------------------------------------------------------------
|
|
|
|
@contextmanager
|
|
def patch(self) -> Generator[None]:
|
|
"""Patch MCP transport functions and the runtime-availability check.
|
|
|
|
Patches both locations where openclaw_runtime_unavailable_reason
|
|
is imported by name:
|
|
- integrations.openclaw (canonical source)
|
|
- integrations.openclaw.tools.openclaw_mcp_tool (tool layer local alias)
|
|
|
|
Also patches the tool call and list functions at both the integration
|
|
module and the tool module's local aliases.
|
|
"""
|
|
with (
|
|
patch(
|
|
"integrations.openclaw.call_openclaw_tool",
|
|
side_effect=self.call_tool,
|
|
),
|
|
patch(
|
|
"integrations.openclaw.tools.openclaw_mcp_tool.invoke_openclaw_mcp_tool",
|
|
side_effect=self.call_tool,
|
|
),
|
|
patch(
|
|
"integrations.openclaw.list_openclaw_tools",
|
|
side_effect=self.list_tools,
|
|
),
|
|
patch(
|
|
"integrations.openclaw.tools.openclaw_mcp_tool.list_openclaw_mcp_tools",
|
|
side_effect=self.list_tools,
|
|
),
|
|
patch(
|
|
"integrations.openclaw.openclaw_runtime_unavailable_reason",
|
|
return_value=None,
|
|
),
|
|
patch(
|
|
"integrations.openclaw.tools.openclaw_mcp_tool.openclaw_runtime_unavailable_reason",
|
|
return_value=None,
|
|
),
|
|
):
|
|
yield
|
|
|
|
# ------------------------------------------------------------------
|
|
# Tool handlers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _tool_handlers(self) -> dict[str, Any]:
|
|
return {
|
|
"conversations_list": self._handle_conversations_list,
|
|
"conversations_get": self._handle_conversations_get,
|
|
"conversations_create": self._handle_conversations_create,
|
|
"message_send": self._handle_message_send,
|
|
}
|
|
|
|
def _handle_conversations_list(self, arguments: dict[str, object]) -> dict[str, object]:
|
|
conversations = self._scenario.fixture_conversations
|
|
search = str(arguments.get("search", "")).lower().strip()
|
|
if search:
|
|
filtered = [
|
|
c
|
|
for c in conversations
|
|
if search in str(c.get("title", "")).lower()
|
|
or search in str(c.get("lastMessage", "")).lower()
|
|
]
|
|
# Fall back to all conversations when search term yields no matches
|
|
# (mirrors real OpenClaw returning recent conversations on broad queries)
|
|
conversations = filtered if filtered else conversations
|
|
limit = int(str(arguments.get("limit", 10)))
|
|
conversations = conversations[:limit]
|
|
return {
|
|
"is_error": False,
|
|
"tool": "conversations_list",
|
|
"arguments": arguments,
|
|
"text": f"Found {len(conversations)} conversation(s).",
|
|
"content": [{"type": "text", "text": f"Found {len(conversations)} conversation(s)."}],
|
|
"structured_content": {"conversations": conversations},
|
|
}
|
|
|
|
def _handle_conversations_get(self, arguments: dict[str, object]) -> dict[str, object]:
|
|
conversation_id = str(arguments.get("conversationId", ""))
|
|
for conv in self._scenario.fixture_conversations:
|
|
if conv.get("id") == conversation_id:
|
|
return {
|
|
"is_error": False,
|
|
"tool": "conversations_get",
|
|
"arguments": arguments,
|
|
"text": conv.get("lastMessage", ""),
|
|
"content": [{"type": "text", "text": str(conv.get("lastMessage", ""))}],
|
|
"structured_content": conv,
|
|
}
|
|
return {
|
|
"is_error": True,
|
|
"tool": "conversations_get",
|
|
"arguments": arguments,
|
|
"text": f"Conversation '{conversation_id}' not found.",
|
|
"content": [],
|
|
"structured_content": None,
|
|
}
|
|
|
|
def _handle_conversations_create(self, arguments: dict[str, object]) -> dict[str, object]:
|
|
return {
|
|
"is_error": False,
|
|
"tool": "conversations_create",
|
|
"arguments": arguments,
|
|
"text": "Conversation created successfully.",
|
|
"content": [{"type": "text", "text": "Conversation created successfully."}],
|
|
"structured_content": {"id": "fixture-conv-new", "title": arguments.get("title", "")},
|
|
}
|
|
|
|
def _handle_message_send(self, arguments: dict[str, object]) -> dict[str, object]:
|
|
return {
|
|
"is_error": False,
|
|
"tool": "message_send",
|
|
"arguments": arguments,
|
|
"text": "Message sent.",
|
|
"content": [{"type": "text", "text": "Message sent."}],
|
|
"structured_content": None,
|
|
}
|