merge: kéo Delta epic-R04 (gồm cả R01 và R03) vào gamma/refactor

Nam chốt: không chờ Delta merge vào main, lấy sớm để va chạm nhỏ và sửa
ngay, thay vì dồn một cục lúc cả hai cùng lên main.

R04 chứa trọn R01 và R03 nên một lần merge là đủ cả ba: 96 file, +8260
dòng. Xung đột chỉ 5 file, đều là __init__.py add/add — hai team cùng
dựng khung thư mục nên đụng docstring. Giữ docstring của Gamma (nói rõ
ràng buộc "không import PySide6"), giữ mọi phần code của Delta.

Riêng tests/fakes/__init__.py: bỏ hai dòng import háo hức của Delta
(fake_provider, fake_tool_executor). fake_provider dùng
`from providers.base import ...` — import tuyệt đối, chỉ chạy được khi
cwd là gốc repo — nên nó làm đứt bài test "dùng fake mà không nạp config
thật". Không ai import ở cấp package; test của Delta gọi thẳng module
nên bỏ đi không ảnh hưởng họ. Đã ghi lý do vào docstring của gói.

Delta cũng xoá preview-desktop và "requirements (cloud copy).txt".

430 test xanh sau merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-25 10:20:36 +09:00
co-authored by Claude Opus 5
97 changed files with 8275 additions and 235 deletions
+14 -1
View File
@@ -1 +1,14 @@
"""Test double dùng chung cho cả 3 team — không phụ thuộc Qt."""
"""Test double dùng chung cho cả 3 team — không phụ thuộc Qt.
Gói này cố ý **không** import sẵn fake nào. Import ở đây là import háo hức:
chạm vào bất kỳ fake nào là kéo theo mọi phụ thuộc của nó, nên chỉ cần một
fake lỡ import module cần sys.path đặc biệt là cả gói hỏng trong môi trường
cô lập. Đã xảy ra thật khi merge Delta: `fake_provider` dùng
`from providers.base import ...` (import tuyệt đối) làm đứt bài kiểm
"dùng fake mà không nạp config thật".
Import thẳng module cần dùng:
from cowork_local.tests.fakes.fake_config import FakeConfigRepository
from cowork_local.tests.fakes.fake_provider import FakeProvider
"""
+113
View File
@@ -0,0 +1,113 @@
"""Fake LLM Provider for offline unit, contract, and characterization testing.
Provides deterministic responses, stream simulation, tool-call dispatching,
and fault injection without requiring any external network access or API keys.
"""
from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional
from providers.base import CancelFn, Provider, ProviderError, TextCallback, ToolSpec
class FakeProvider(Provider):
"""Deterministic test double mimicking real LLM Providers (OpenAI, Anthropic, Ollama)."""
name = "fake"
supports_vision = True
def __init__(self, conf: Optional[Dict[str, Any]] = None) -> None:
# Initialize base provider with default configuration if none provided
super().__init__(conf or {"model": "fake-model-v1"})
# History of all message batches sent across all chat calls
self.call_history: List[List[Dict[str, Any]]] = []
# Queue of programmed assistant responses to return sequentially
self.response_queue: List[Dict[str, Any]] = []
# Queue of exceptions to raise on corresponding calls
self.error_queue: List[Exception] = []
# Default text returned when response queue is empty
self.default_text: str = "Fake model response."
# Total number of chat invocations
self.call_count: int = 0
# Recorded tool specs passed into each turn
self.last_tools: Optional[List[ToolSpec]] = None
def queue_response(
self,
content: str = "",
tool_calls: Optional[List[Dict[str, Any]]] = None,
reasoning: Optional[str] = None,
chunks: Optional[List[str]] = None,
) -> FakeProvider:
"""Enqueue a pre-configured response structure for upcoming chat turns."""
self.response_queue.append({
"content": content,
"tool_calls": tool_calls or [],
"reasoning": reasoning,
"chunks": chunks or ([content] if content else []),
})
return self
def queue_error(self, exc: Exception) -> FakeProvider:
"""Enqueue an exception to simulate network/API errors on the next turn."""
self.error_queue.append(exc)
return self
def chat(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[ToolSpec]] = None,
on_text: Optional[TextCallback] = None,
cancel: Optional[CancelFn] = None,
on_reasoning: Optional[TextCallback] = None,
) -> Dict[str, Any]:
"""Simulate single LLM turn with full streaming and tool-call support."""
self.call_count += 1
self.call_history.append([dict(m) for m in messages])
self.last_tools = tools
# 1. Check for injected errors
if self.error_queue:
raise self.error_queue.pop(0)
# 2. Check early cancellation before processing
if cancel and cancel():
raise ProviderError("Execution aborted by user cancel signal before response generation.")
# 3. Retrieve queued response or construct default response
if self.response_queue:
resp_spec = self.response_queue.pop(0)
content = resp_spec.get("content", "")
tool_calls = resp_spec.get("tool_calls", [])
reasoning = resp_spec.get("reasoning")
chunks = resp_spec.get("chunks", [content] if content else [])
else:
content = self.default_text
tool_calls = []
reasoning = None
chunks = [content]
# 4. Stream reasoning chunks if provided
if reasoning and on_reasoning:
on_reasoning(reasoning)
# 5. Stream text chunks, checking cancellation between fragments
for chunk in chunks:
if cancel and cancel():
raise ProviderError("Execution cancelled during text chunk streaming.")
if on_text and chunk:
on_text(chunk)
# 6. Return canonical assistant message payload
assistant_msg: Dict[str, Any] = {
"role": "assistant",
"content": content,
}
if tool_calls:
assistant_msg["tool_calls"] = tool_calls
return assistant_msg
def list_models(self) -> List[str]:
"""Return available mock models for settings and validation tests."""
return ["fake-model-v1", "fake-reasoner-pro", "fake-vision-plus"]
+71
View File
@@ -0,0 +1,71 @@
"""Fake Tool Executor for isolated, offline agent tool-call verification.
Allows tests to verify tool invocation arguments, mock tool return values,
and simulate failures/delays without performing unsafe host disk or OS operations.
"""
from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional
class FakeToolExecutor:
"""Mock execution engine for agent tool-call dispatching."""
def __init__(self) -> None:
# History of all executed tool invocations: List of {"name": str, "args": dict, "result": dict}
self.call_log: List[Dict[str, Any]] = []
# Custom handlers registered per tool name
self.handlers: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {}
# Pre-programmed fixed responses keyed by tool name
self.mock_responses: Dict[str, Dict[str, Any]] = {}
# Default response when no specific handler or response is found
self.default_result: Dict[str, Any] = {"ok": True, "output": "Fake tool executed successfully."}
def register_handler(
self,
tool_name: str,
handler: Callable[[Dict[str, Any]], Dict[str, Any]],
) -> FakeToolExecutor:
"""Register a dynamic handler function for a specific tool name."""
self.handlers[tool_name] = handler
return self
def set_mock_response(
self,
tool_name: str,
result: Dict[str, Any],
) -> FakeToolExecutor:
"""Set a static return payload for a specific tool name."""
self.mock_responses[tool_name] = result
return self
def execute(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a tool call using registered mocks and record invocation details."""
# 1. Resolve result from handler, preset response, or default fallback
if tool_name in self.handlers:
result = self.handlers[tool_name](arguments)
elif tool_name in self.mock_responses:
result = self.mock_responses[tool_name]
else:
result = dict(self.default_result)
result["tool"] = tool_name
result["received_args"] = arguments
# 2. Record execution trace for post-test assertions
self.call_log.append({
"name": tool_name,
"args": dict(arguments),
"result": dict(result),
})
return result
def get_calls_for(self, tool_name: str) -> List[Dict[str, Any]]:
"""Retrieve all recorded calls for a given tool name."""
return [call for call in self.call_log if call["name"] == tool_name]
def reset(self) -> None:
"""Clear recorded logs and registered mock responses."""
self.call_log.clear()
self.handlers.clear()
self.mock_responses.clear()
+141
View File
@@ -0,0 +1,141 @@
"""Offline test doubles for the R04 turn runtime seams.
Sits beside ``fake_provider.py``/``fake_tool_executor.py`` (R01-T02) and plays
the same role one level up: those fake a *provider*, these fake the ports
``ConversationApplicationService`` is driven through
(``application/conversations/turn_runtime.py``).
Deliberately dumb — they record what they were asked and return canned answers.
A failing test then points at the service under test rather than at a mock
framework's configuration.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
from cowork_local.domain.agents.agent_event import ToolPreview
from cowork_local.domain.agents.conversation_execution_request import (
ConversationExecutionRequest,
)
class FakeSpec:
"""An advertised tool. The service only ever reads ``.name`` off a spec."""
def __init__(self, name: str) -> None:
self.name = name
class FakeReply:
"""One programmed provider answer."""
def __init__(self, content: str = "", tool_calls=None, chunks=None, reasoning: str = ""):
self.content = content
self.tool_calls = tool_calls or []
# Default to streaming the whole content as a single chunk, which is what
# a non-streaming gateway effectively does.
self.chunks = chunks if chunks is not None else ([content] if content else [])
self.reasoning = reasoning
class FakeModelCall:
""":class:`ModelCallPort` returning programmed replies in order.
A programmed entry may be an exception instead of a reply, which is how a
test simulates the gateway dying mid-turn.
"""
def __init__(self, replies: List[Any]) -> None:
self.replies = list(replies)
self.calls: List[Dict[str, Any]] = []
def call(self, messages, tools, on_text=None, on_reasoning=None, cancel=None):
# Snapshot the messages: the service keeps mutating its own list, so
# storing it by reference would make every recorded call look identical.
self.calls.append({"messages": [dict(m) for m in messages],
"tool_names": [getattr(t, "name", "") for t in tools]})
reply = self.replies.pop(0) if self.replies else FakeReply(content="(default)")
if isinstance(reply, BaseException):
raise reply
if reply.reasoning and on_reasoning:
on_reasoning(reply.reasoning)
for chunk in reply.chunks:
if on_text and chunk:
on_text(chunk)
assistant: Dict[str, Any] = {"role": "assistant", "content": reply.content}
if reply.tool_calls:
assistant["tool_calls"] = reply.tool_calls
return assistant
class FakeToolRuntime:
""":class:`ToolRuntimePort` over an imaginary output folder."""
def __init__(self, specs=("save_file", "run_command", "update_plan"),
results: Optional[Dict[str, Dict[str, Any]]] = None,
removed: Tuple[str, ...] = (), added: Tuple[str, ...] = ()) -> None:
self._specs = [FakeSpec(n) for n in specs]
self._results = results or {}
self._removed, self._added = removed, added
self.executed: List[Tuple[str, Dict[str, Any]]] = []
self.finalize_calls: List[Dict[str, Any]] = []
# When set, every executed tool streams this string through ``on_output``.
self.emit_output: Optional[str] = None
def specs(self, allowed_tools=None):
if allowed_tools is None:
return list(self._specs)
return [s for s in self._specs if s.name in allowed_tools]
def preview(self, name, args):
return ToolPreview(kind="info", title=name, text=str(args))
def execute(self, name, args, on_output=None, cancel=None):
self.executed.append((name, dict(args)))
if self.emit_output and on_output:
on_output(self.emit_output)
return dict(self._results.get(name, {"ok": True, "output": f"{name} ok"}))
def snapshot(self):
return "before"
def finalize(self, before, cancelled=False):
self.finalize_calls.append({"before": before, "cancelled": cancelled})
return list(self._removed), list(self._added)
# --------------------------------------------------------------------------- #
# Small helpers shared by the turn tests.
# --------------------------------------------------------------------------- #
def make_request(**overrides) -> ConversationExecutionRequest:
"""A minimal valid request; each test overrides only what it exercises."""
base: Dict[str, Any] = {"turn_id": "t1", "session_id": "s1", "prompt": "do it"}
base.update(overrides)
return ConversationExecutionRequest(**base)
def run_turn(service, request=None, cancel=None):
"""Execute a turn and return ``(result, events)``."""
events: List[Any] = []
result = service.execute(request or make_request(), events.append, cancel=cancel)
return result, events
def events_of_type(events, cls):
"""Every emitted event of one type, in order."""
return [e for e in events if isinstance(e, cls)]
def tool_turn(tool_name: str = "save_file", args=None, **tool_kwargs):
"""A turn that calls one tool and then answers — ``(model, tools)``."""
calls = [{"id": "c1", "name": tool_name, "arguments": args or {"filename": "a.md"}}]
model = FakeModelCall([FakeReply(content="working", tool_calls=calls),
FakeReply(content="done")])
return model, FakeToolRuntime(**tool_kwargs)
__all__ = [
"FakeSpec", "FakeReply", "FakeModelCall", "FakeToolRuntime",
"make_request", "run_turn", "events_of_type", "tool_turn",
]