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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user