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>
100 lines
4.1 KiB
Python
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"]
|