"""Integration test for the AppContext routing wiring (R03-T04 / R03-T05). The three chat surfaces now call ``ctx.routing_application()`` instead of each carrying their own copy of the routing algorithm. The unit tests cover the policy; this file covers the WIRING, which unit tests with a fake router cannot see: * the service is built and memoised on the context * it reads the per-workspace mode through ``project_routing_mode`` * the legacy ``core.routing.RoutingService`` is what sits underneath it * ``fallback`` survives a round trip through the per-workspace mode store Still Qt-free: ``AppContext`` itself imports no widgets, and the config is written into a tmp dir so nothing touches ``~/.cowork_local``. """ from __future__ import annotations from pathlib import Path import pytest from cowork_local.application.model_routing import ( RoutingApplicationService, RoutingMode, ) from cowork_local.config import AppConfig from cowork_local.state import AppContext @pytest.fixture def ctx(tmp_path: Path) -> AppContext: """An AppContext backed by a throwaway config file.""" return AppContext(AppConfig.load(tmp_path / "config.json")) def test_routing_application_is_built_and_memoised(ctx): """One instance per app: the pending-switch registry underneath it must be shared by every surface, so a second call has to return the same object.""" first = ctx.routing_application() assert isinstance(first, RoutingApplicationService) assert ctx.routing_application() is first def test_the_legacy_engine_sits_underneath_the_new_service(): """Strangler-fig check (ADR-001 section 4): the scoring engine is reused, not reimplemented. If this ever stops holding, the assessment scores the scheduler probes would no longer be the ones routing decisions use.""" from cowork_local.core.routing.service import RoutingService config = AppConfig.load(Path("does-not-exist.json")) context = AppContext(config) service = context.routing_application() assert isinstance(service._router, RoutingService) assert service._router is context.routing() def test_mode_is_read_through_the_per_workspace_lookup(ctx, monkeypatch): seen = [] def fake_mode(surface: str) -> str: seen.append(surface) return "off" monkeypatch.setattr(ctx, "project_routing_mode", fake_mode) # Built after the patch so the service captures the patched reader. service = RoutingApplicationService(ctx.routing(), mode_reader=ctx.project_routing_mode) decision = service.route_turn("co4e", "hello", "openai_compat", "gpt-4o-mini") assert seen == ["co4e"] assert decision.switched is False def test_routing_off_by_default_leaves_the_selected_model_alone(ctx): """Default config has routing off on every surface, so a fresh install must never move a turn to another model.""" decision = ctx.routing_application().route_turn( "cowork", "write a function", "openai_compat", "gpt-4o-mini") assert decision.mode is RoutingMode.OFF assert decision.switched is False assert decision.target() == ("openai_compat", "gpt-4o-mini") @pytest.mark.parametrize("mode", ["off", "auto", "manual", "fallback"]) def test_every_mode_survives_a_round_trip_through_the_config(ctx, mode): """``fallback`` is new (R03-T03); the per-surface store used to whitelist only three values and would have silently downgraded it to "off".""" ctx.set_project_routing_mode("cowork", mode) assert ctx.project_routing_mode("cowork") == mode def test_an_unknown_mode_still_falls_back_to_off(ctx): ctx.set_project_routing_mode("cowork", "turbo") assert ctx.project_routing_mode("cowork") == "off" def test_a_real_route_call_never_raises_without_any_assessments(ctx): """The store is empty on a fresh install. Routing must degrade to "keep the current model" rather than raise into the middle of the first message.""" ctx.set_project_routing_mode("cowork", "auto") decision = ctx.routing_application().route_turn( "cowork", "hello there", "openai_compat", "gpt-4o-mini") assert decision.switched is False assert decision.target() == ("openai_compat", "gpt-4o-mini")