Files
cowork-local/tests/integration/test_routing_surfaces.py
T
anhtnm1andClaude Opus 5 15e1d3eb65 test(R03/R04): cover the three code paths that were changed but never executed
Verification gap closed. The suite proved the new services correct in isolation,
but three paths I had modified had no test actually running them:

tests/integration/test_task_executor_flow.py (7 tests)
  The Schedule Task path after R04-T05. Pins that History is still re-saved from
  the LIVE message list mid-run (the reason begin_turn() exists - the pre-turn
  copy would have frozen progress at the first user message), that update_plan
  tracking still reports an unfinished checklist, and that a failed run still
  raises so execute_task writes error.txt.

tests/integration/test_routing_surfaces.py (11 tests)
  Real offscreen CoworkTab/Co4ETab/FolderTab calling the shared routing service:
  correct surface key per screen, Auto switches, Off does not consult the engine,
  Manual switches only on approval, a pinned Admin agent still wins, and AI-Edit
  still pins TaskType.CODING. Also pins the field contract ui/routing_toggle.py
  reads off RoutingDecision (from_model/to_model as provider/model keys) - a
  rename there would only fail inside a modal dialog.

Also updates docs/refactor/Refactoring_Checklist.md: the 16 completed R01/R03/R04
tasks, the Team Duy daily rows, and a status block recording the measured
numbers, the scope correction (team owns R01/R02/R04/R10), and what is still
outstanding.

Suite: 243 passed, 2 pre-existing failures (EPIC R02). Fast suite (unit +
contracts + characterization + routing): 218 passed in 1.16s.

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

257 lines
9.2 KiB
Python

"""The three chat surfaces really route through the shared service (R03-T04/T05).
The unit suite proves ``RoutingApplicationService`` decides correctly against a
fake router. This file proves the three widgets that used to own a private copy
of that algorithm now call it, on real (offscreen) widgets:
* ``ui/chat_panel.py::_apply_routing`` (Cowork)
* ``ui/co4e_tab.py::_apply_co4e_routing`` (Co4E)
* ``ui/folder_tab.py::_ai_apply_routing`` (AI-Edit)
It also pins the Manual-mode handshake, including the field contract the
existing confirm dialog reads off the decision - the one place where the new
``RoutingDecision`` has to look like the legacy ``SwitchDecision`` it replaced.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, List, Optional, Tuple
import pytest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from cowork_local.application.model_routing import ( # noqa: E402
RoutingApplicationService,
RoutingDecision,
RoutingMode,
)
from cowork_local.config import AppConfig # noqa: E402
from cowork_local.state import AppContext # noqa: E402
pytest.importorskip("PySide6", reason="Qt is required for the integration suite")
@pytest.fixture(scope="module")
def qt_app():
from PySide6.QtWidgets import QApplication
return QApplication.instance() or QApplication([])
@pytest.fixture
def ctx(qt_app, tmp_path: Path) -> AppContext:
return AppContext(AppConfig.load(tmp_path / "config.json"))
class _FakeRouteResult:
"""Shaped like ``core.routing.service.RouteResult``."""
def __init__(self, provider: str, model: str, gain: float = 0.4,
task: str = "coding") -> None:
self.should_switch = True
self._target = (provider, model)
self.task_type = type("_T", (), {"value": task})()
self.decision = type("_D", (), {"score_gain": gain, "reason": "better fit"})()
def target(self) -> Optional[Tuple[str, str]]:
return self._target
class _FakeRouter:
"""Minimal RoutingPort: always proposes the same switch, records the surface."""
def __init__(self, provider="anthropic", model="claude-sonnet-4-6") -> None:
self.result = _FakeRouteResult(provider, model)
self.surfaces: List[str] = []
def route(self, surface, prompt, current_provider, current_model, **kwargs):
self.surfaces.append(surface)
return self.result
def _install(ctx: AppContext, mode: str) -> _FakeRouter:
"""Wire a fake router into the context and force ``mode`` on every surface."""
router = _FakeRouter()
service = RoutingApplicationService(router, mode_reader=lambda _surface: mode)
ctx._routing_application = service # already-built instance; accessor returns it
return router
# --------------------------------------------------------------------------- #
# Cowork chat
# --------------------------------------------------------------------------- #
def test_cowork_applies_an_auto_switch_to_the_next_turn(ctx):
from cowork_local.ui.cowork_tab import CoworkTab
router = _install(ctx, "auto")
tab = CoworkTab(ctx)
turn: dict = {"bubbles": []}
tab._apply_routing("write a function", turn)
assert router.surfaces == [tab.kind]
# build_provider() honours these for THIS turn only.
assert (tab._routed_provider, tab._routed_model) == ("anthropic", "claude-sonnet-4-6")
assert turn["bubbles"], "the user must be told the model was switched"
def test_cowork_leaves_the_model_alone_when_routing_is_off(ctx):
from cowork_local.ui.cowork_tab import CoworkTab
router = _install(ctx, "off")
tab = CoworkTab(ctx)
turn: dict = {"bubbles": []}
tab._apply_routing("write a function", turn)
assert router.surfaces == []
assert (tab._routed_provider, tab._routed_model) == (None, None)
assert turn["bubbles"] == []
def test_cowork_manual_mode_switches_only_after_the_dialog_approves(ctx, monkeypatch):
from cowork_local.ui import chat_panel as chat_panel_module
from cowork_local.ui.cowork_tab import CoworkTab
_install(ctx, "manual")
tab = CoworkTab(ctx)
asked: List[Any] = []
monkeypatch.setattr(tab, "_confirm_routing_switch",
lambda decision: asked.append(decision) or True)
turn: dict = {"bubbles": []}
tab._apply_routing("write a function", turn)
assert len(asked) == 1
assert (tab._routed_provider, tab._routed_model) == ("anthropic", "claude-sonnet-4-6")
def test_cowork_manual_mode_keeps_the_model_when_the_dialog_is_declined(ctx, monkeypatch):
from cowork_local.ui.cowork_tab import CoworkTab
_install(ctx, "manual")
tab = CoworkTab(ctx)
monkeypatch.setattr(tab, "_confirm_routing_switch", lambda _decision: False)
turn: dict = {"bubbles": []}
tab._apply_routing("write a function", turn)
assert (tab._routed_provider, tab._routed_model) == (None, None)
assert turn["bubbles"] == []
def test_a_pinned_admin_agent_still_wins_over_routing(ctx):
"""An explicitly chosen Admin agent pins its own provider/model; routing must
not override a deliberate user choice."""
from cowork_local.ui.cowork_tab import CoworkTab
router = _install(ctx, "auto")
tab = CoworkTab(ctx)
tab._admin_agent = object()
turn: dict = {"bubbles": []}
tab._apply_routing("write a function", turn)
assert router.surfaces == []
assert (tab._routed_provider, tab._routed_model) == (None, None)
# --------------------------------------------------------------------------- #
# Co4E
# --------------------------------------------------------------------------- #
def test_co4e_routes_on_its_own_surface_key_and_returns_the_model(ctx):
from cowork_local.ui.co4e_tab import Co4ETab
router = _install(ctx, "auto")
tab = Co4ETab(ctx)
model = tab._apply_co4e_routing("build me a flow")
assert router.surfaces == ["co4e"]
assert model == "claude-sonnet-4-6"
assert tab._co4e_routed_provider == "anthropic"
def test_co4e_returns_an_empty_model_when_routing_is_off(ctx):
"""'' means "use the provider default" - the contract _run_chat_turn expects."""
from cowork_local.ui.co4e_tab import Co4ETab
_install(ctx, "off")
tab = Co4ETab(ctx)
assert tab._apply_co4e_routing("build me a flow") == ""
assert tab._co4e_routed_provider is None
# --------------------------------------------------------------------------- #
# AI-Edit
# --------------------------------------------------------------------------- #
def test_ai_edit_routes_on_its_own_surface_key(ctx):
from cowork_local.ui.folder_tab import FolderTab
router = _install(ctx, "auto")
tab = FolderTab(ctx)
tab._ai_apply_routing("rename this variable")
assert router.surfaces == ["ai_edit"]
assert (tab._ai_routed_provider, tab._ai_routed_model) == (
"anthropic", "claude-sonnet-4-6")
def test_ai_edit_pins_the_coding_task_type(ctx):
"""An edit instruction is never a QA question, so AI-Edit skips
classification entirely - the constraint has to survive the move into the
shared service or it is silently dropped."""
from cowork_local.core.routing.models import TaskType
from cowork_local.ui.folder_tab import FolderTab
seen: List[Any] = []
class _Recorder(_FakeRouter):
def route(self, surface, prompt, current_provider, current_model, **kwargs):
seen.append(kwargs.get("task_type"))
return super().route(surface, prompt, current_provider, current_model, **kwargs)
ctx._routing_application = RoutingApplicationService(
_Recorder(), mode_reader=lambda _s: "auto")
tab = FolderTab(ctx)
tab._ai_apply_routing("rename this variable")
assert seen == [TaskType.CODING]
# --------------------------------------------------------------------------- #
# The confirm dialog's field contract
# --------------------------------------------------------------------------- #
def test_the_decision_exposes_exactly_what_the_confirm_dialog_reads():
"""``ui/routing_toggle.py::confirm_switch`` is not migrated until EPIC R08,
so it still reads ``from_model``/``to_model`` as ``provider/model`` candidate
keys and splits them. A rename here would blow up inside a modal dialog -
the one place a failure is hardest to see in a test run."""
from cowork_local.core.routing.models import split_key
decision = RoutingDecision(
mode=RoutingMode.MANUAL, provider="anthropic", model="claude-sonnet-4-6",
switched=True, task_type="coding", score_gain=0.31, reason="better fit",
previous_provider="openai_compat", previous_model="gpt-4o-mini",
)
assert split_key(decision.from_model)[1] == "gpt-4o-mini"
assert split_key(decision.to_model)[1] == "claude-sonnet-4-6"
assert decision.task_type == "coding"
assert f"{decision.score_gain:.2f}" == "0.31"
assert decision.reason == "better fit"
def test_a_first_turn_with_no_current_model_yields_an_empty_from_model():
"""split_key() is only called when from_model is truthy, so an unset current
model must produce "" rather than a bare "provider/"."""
decision = RoutingDecision(mode=RoutingMode.AUTO, provider="anthropic",
model="claude", switched=True)
assert decision.from_model == ""