"""FakeToolExecutor - offline stand-in for the extra-tool executor (R01-T02). ``core.chat_agent.run_cowork`` routes any tool call whose name appears in ``extra_tools`` to ``extra_executor(name, args)`` and expects back:: {"ok": bool, "output": str} In production that callable reaches MCP servers, Microsoft 365 connectors and subprocesses. This double answers from a table instead, so the agent loop's tool branch is testable with no processes, no sockets and no credentials - and every invocation is recorded for assertions about what the agent actually asked for. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional, Union from cowork_local.providers.base import ToolSpec # A scripted answer is either the literal result dict, or a callable computing it # from the arguments (for tools whose output must depend on the input). ToolResult = Dict[str, Any] ScriptedResult = Union[ToolResult, Callable[[Dict[str, Any]], ToolResult]] @dataclass(frozen=True) class ToolInvocation: """One recorded ``extra_executor(name, args)`` call.""" name: str args: Dict[str, Any] @dataclass class FakeToolExecutor: """Callable test double for ``run_cowork(extra_executor=...)``. Args: results: tool name -> scripted result (dict, or callable taking args). default: what to answer for a tool with no scripted result. ``None`` (the default) answers with ``ok=False`` and an explicit message rather than raising - the production executor also reports unknown tools as a failed tool result, and matching that keeps the agent loop on its real code path instead of an exception path it would never take in production. """ results: Dict[str, ScriptedResult] = field(default_factory=dict) default: Optional[ScriptedResult] = None calls: List[ToolInvocation] = field(default_factory=list) def __call__(self, name: str, args: Dict[str, Any]) -> ToolResult: """Record the invocation and return its scripted result.""" self.calls.append(ToolInvocation(name=name, args=dict(args or {}))) scripted = self.results.get(name, self.default) if scripted is None: return {"ok": False, "output": f"No fake result scripted for tool '{name}'."} # A callable lets one entry serve many different arguments (e.g. echo the # path it was asked to read) without scripting every combination. resolved = scripted(dict(args or {})) if callable(scripted) else dict(scripted) resolved.setdefault("ok", True) resolved.setdefault("output", "") return resolved # -- introspection helpers used by tests ---------------------------- # @property def call_names(self) -> List[str]: """Tool names in call order - the usual thing a test asserts on.""" return [c.name for c in self.calls] def called(self, name: str) -> bool: """True when ``name`` was invoked at least once.""" return any(c.name == name for c in self.calls) def args_for(self, name: str) -> List[Dict[str, Any]]: """Every argument dict this tool was called with, in order.""" return [c.args for c in self.calls if c.name == name] def specs(self) -> List[ToolSpec]: """``ToolSpec`` entries for the scripted tools, ready to pass as ``run_cowork(extra_tools=...)``. The agent loop dispatches to ``extra_executor`` only for names present in ``extra_tools``; generating the specs from the same table removes the chance of a test scripting a result the loop can never reach. """ return [ ToolSpec( name=name, description=f"Fake tool '{name}' (test double).", # Permissive schema on purpose: these specs exist to register the # name with the agent loop, not to validate arguments. parameters={"type": "object", "properties": {}, "additionalProperties": True}, ) for name in self.results ] __all__ = ["FakeToolExecutor", "ToolInvocation"]