Files
cowork-local/tests/fakes/fake_tool_executor.py
T
anhtnm1andClaude Opus 5 bbc09f628a feat(R01): architecture foundation, offline fakes and characterization net
EPIC R01 (Team Duy) - safety net before the parallel refactor starts.

R01-T01 docs/architecture/ADR-001-layered-architecture.md
  4-tier boundaries, allowed dependency directions, invariants I1-I6 and
  the strangler-fig migration strategy.
R01-T02 tests/fakes/{fake_provider,fake_tool_executor}.py
  Scripted, offline Provider and extra-tool executor doubles.
R01-T03 scripts/check_imports.py
  AST-based Clean Architecture Guard (CASAN Check 3). Also covers relative
  imports and function-local imports; ASCII-only output for cp932 consoles.
R01-T04 tests/characterization/test_run_cowork.py
  13 snapshot tests pinning run_cowork's current observable contract before
  EPIC R04 moves its orchestration into application/.
R01-T05 docs/architecture/dormant-code.md
  Import-graph scan: 43 unimported modules verified down to 6 genuinely
  dormant items (~1887 LOC); the rest run via subprocess/CLI entry points.

tests/conftest.py binds `cowork_local` to THIS checkout by absolute path -
previously sys.path discovery could import a sibling checkout and the suite
would silently test the wrong code.

Suite: 104 passed, 1.08s (2 pre-existing failures in test_config_security.py
remain - config.py still ships a hardcoded default password, EPIC R02/Team Nam).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:05:50 +09:00

100 lines
4.1 KiB
Python

"""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"]