Files
cowork-local/tests/integration/test_routing_surfaces.py
T
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
2026-08-31 05:15:13 +00:00

255 lines
9.0 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)
* ``presentation/folder/ai_edit_model_resolver.py::AiEditModelResolver.apply_routing`` (AI-Edit)
On this branch the Manual-mode confirm dialog (``ui/routing_toggle.py::
confirm_switch``) still reads its decision straight off the engine's own
``core/routing/models.py::SwitchDecision`` - ``RoutingOutcome.decision`` passes
it through unwrapped rather than translating it into an application-layer
type, so there is no separate field contract to pin here.
"""
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,
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 _FakeDecisionPort:
"""A :class:`RoutingDecisionPort` that always proposes the same switch and
records the surface (and task type) it was asked to evaluate."""
def __init__(self, provider="anthropic", model="claude-sonnet-4-6",
gain: float = 0.4) -> None:
self.provider = provider
self.model = model
self.gain = gain
self.surfaces: List[str] = []
def evaluate(self, request, mode):
from cowork_local.application.model_routing import RouteEvaluation
self.surfaces.append(request.surface)
return RouteEvaluation(
task_type=request.task_type or "coding",
should_switch=True,
target_provider=self.provider,
target_model=self.model,
score_gain=self.gain,
reason="better fit",
)
class _FixedModeResolver:
"""A :class:`ModeResolver` that reports the same mode for every surface."""
def __init__(self, mode: str) -> None:
self._mode = mode
def mode_for(self, surface: str) -> str:
return self._mode
def _install(ctx: AppContext, mode: str) -> _FakeDecisionPort:
"""Wire a fake decision port into the context and force ``mode`` on every
surface.
Every surface reaches the service through
``build_routing_application_service(ctx)``, which memoises its instance on
``ctx._routing_app_service`` (see ``core_routing_adapter.py``) — pre-seeding
that exact attribute is what makes the surfaces under test see this fake
instead of building a real one against ``ctx.routing()``.
"""
router = _FakeDecisionPort()
service = RoutingApplicationService(router, mode_resolver=_FixedModeResolver(mode))
ctx._routing_app_service = 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):
"""Manual mode's confirm dialog is ``ui/routing_toggle.py::confirm_switch``,
imported locally inside ``_apply_routing`` at call time — patching the
source module's attribute is what a local import actually re-reads."""
from cowork_local.ui.cowork_tab import CoworkTab
_install(ctx, "manual")
tab = CoworkTab(ctx)
asked: List[Any] = []
def fake_confirm(parent, decision, timeout):
asked.append(decision)
return True
monkeypatch.setattr("cowork_local.ui.routing_toggle.confirm_switch", fake_confirm)
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("cowork_local.ui.routing_toggle.confirm_switch",
lambda parent, decision, timeout: 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):
"""R08-T12: the routing call this test pins moved from
``ui/folder_tab.py::FolderTab._ai_apply_routing`` to
``presentation/folder/ai_edit_model_resolver.py::AiEditModelResolver.
apply_routing`` - same RoutingApplicationService call, same surface key,
now independently testable without the whole FolderTab widget tree."""
from cowork_local.presentation.folder.folder_tab import FolderTab
router = _install(ctx, "auto")
tab = FolderTab(ctx)
tab.ai_panel.resolver.apply_routing("rename this variable")
assert router.surfaces == ["ai_edit"]
assert (tab.ai_panel.resolver.routed_provider, tab.ai_panel.resolver.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.presentation.folder.folder_tab import FolderTab
seen: List[Any] = []
class _Recorder(_FakeDecisionPort):
def evaluate(self, request, mode):
seen.append(request.task_type)
return super().evaluate(request, mode)
ctx._routing_app_service = RoutingApplicationService(
_Recorder(), mode_resolver=_FixedModeResolver("auto"))
tab = FolderTab(ctx)
tab.ai_panel.resolver.apply_routing("rename this variable")
assert seen == [TaskType.CODING]