Files
2026-07-13 13:12:33 +08:00

315 lines
8.7 KiB
Python

from __future__ import annotations
import asyncio
import time
from collections.abc import AsyncIterator
from typing import Any
import pytest
from opensquilla.engine.pipeline import TurnContext
from opensquilla.engine.runtime import TurnRunner
from opensquilla.engine.steps import squilla_router as squilla_router_step
from opensquilla.gateway.config import GatewayConfig, SquillaRouterConfig
from opensquilla.provider import ChatConfig, EnsembleProvider, Message
from opensquilla.provider.selector import ProviderConfig
class _Provider:
provider_name = "fake"
def chat(
self,
messages: list[Message],
tools: list[Any] | None = None,
config: ChatConfig | None = None,
) -> AsyncIterator[Any]:
raise AssertionError("pipeline test should not start provider chat")
async def list_models(self) -> list[Any]:
return []
class _FakeSelector:
def __init__(self) -> None:
self._cfg = ProviderConfig(
provider="openrouter",
model="base-model",
api_key="sk-test",
base_url="https://openrouter.ai/api",
)
@property
def current_config(self) -> ProviderConfig:
return self._cfg
def override_model(self, model: str) -> None:
self._cfg = ProviderConfig(
provider=self._cfg.provider,
model=model,
api_key=self._cfg.api_key,
base_url=self._cfg.base_url,
proxy=self._cfg.proxy,
provider_routing=self._cfg.provider_routing,
)
def resolve(self) -> _Provider:
return _Provider()
class _SlowHistoryStrategy:
async def classify(
self,
message: str,
valid_tiers: list[str],
routing_history: list[dict] | None = None,
**kwargs: object,
) -> tuple[str, float, str, dict]:
time.sleep(0.08)
return (
"c2",
0.95,
"v4_phase3",
{
"route_class": "R2",
"thinking_mode": "T2",
"prompt_policy": "P1",
},
)
class _MutatingHistoryStrategy:
async def classify(
self,
message: str,
valid_tiers: list[str],
routing_history: list[dict] | None = None,
**kwargs: object,
) -> tuple[str, float, str, dict]:
time.sleep(0.08)
assert routing_history
routing_history[0]["final_tier"] = "poisoned"
return (
"c2",
0.95,
"v4_phase3",
{
"route_class": "R2",
"thinking_mode": "T2",
"prompt_policy": "P1",
},
)
def _config_with_router_timeout() -> GatewayConfig:
return GatewayConfig(
squilla_router=SquillaRouterConfig(routing_timeout_seconds=0.01)
)
@pytest.mark.asyncio
async def test_run_pipeline_wraps_provider_when_llm_ensemble_enabled() -> None:
config = GatewayConfig(
squilla_router=SquillaRouterConfig(enabled=False),
llm_ensemble={"enabled": True},
)
runner = TurnRunner(provider_selector=None, config=config)
selector = _FakeSelector()
turn, provider = await runner._run_pipeline(
"hello",
"agent:main:test",
_Provider(),
selector,
[],
"system prompt",
[],
)
assert isinstance(provider, EnsembleProvider)
assert turn.metadata["ensemble_enabled"] is True
assert turn.metadata["routed_model_before_ensemble"]
@pytest.mark.asyncio
async def test_squilla_router_timeout_fails_open(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def slow_router(ctx: TurnContext) -> TurnContext:
await asyncio.sleep(1.0)
ctx.model = "should-not-route"
return ctx
slow_router.__name__ = "apply_squilla_router"
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", slow_router)
runner = TurnRunner(
provider_selector=None,
config=_config_with_router_timeout(),
)
provider = _Provider()
turn, resolved_provider = await asyncio.wait_for(
runner._run_pipeline(
"hello",
"agent:main:test",
provider,
None,
[],
"system prompt",
[],
),
timeout=0.25,
)
assert resolved_provider is provider
assert turn.model != "should-not-route"
router_record = next(
record
for record in turn.metadata["pipeline_steps"]
if record.step_name == "apply_squilla_router"
)
assert router_record.applied is False
assert "timed out" in (router_record.fallback_reason or "")
@pytest.mark.asyncio
async def test_squilla_router_timeout_does_not_late_append_history(
monkeypatch: pytest.MonkeyPatch,
) -> None:
session_key = "agent:main:test-router-timeout-history"
squilla_router_step._history_store.clear()
monkeypatch.setattr(
squilla_router_step,
"_get_strategy",
lambda _config: _SlowHistoryStrategy(),
)
runner = TurnRunner(
provider_selector=None,
config=_config_with_router_timeout(),
)
provider = _Provider()
turn, resolved_provider = await asyncio.wait_for(
runner._run_pipeline(
"hello",
session_key,
provider,
None,
[],
"system prompt",
[],
),
timeout=0.25,
)
await asyncio.sleep(0.1)
assert resolved_provider is provider
assert squilla_router_step._history_store.get(session_key) is None
router_record = next(
record
for record in turn.metadata["pipeline_steps"]
if record.step_name == "apply_squilla_router"
)
assert router_record.applied is False
assert "timed out" in (router_record.fallback_reason or "")
@pytest.mark.asyncio
async def test_squilla_router_timeout_does_not_late_mutate_history_entries(
monkeypatch: pytest.MonkeyPatch,
) -> None:
session_key = "agent:main:test-router-timeout-history-mutation"
original_history = [
{
"turn_index": 0,
"_ts": time.monotonic(),
"text": "previous",
"final_tier": "c1",
}
]
squilla_router_step._history_store.clear()
squilla_router_step._history_store.set(session_key, original_history)
monkeypatch.setattr(
squilla_router_step,
"_get_strategy",
lambda _config: _MutatingHistoryStrategy(),
)
runner = TurnRunner(
provider_selector=None,
config=_config_with_router_timeout(),
)
provider = _Provider()
turn, resolved_provider = await asyncio.wait_for(
runner._run_pipeline(
"hello",
session_key,
provider,
None,
[],
"system prompt",
[],
),
timeout=0.25,
)
await asyncio.sleep(0.1)
assert resolved_provider is provider
stored_history = squilla_router_step._history_store.get(session_key)
assert stored_history == original_history
assert stored_history is not None
assert stored_history[0]["final_tier"] == "c1"
router_record = next(
record
for record in turn.metadata["pipeline_steps"]
if record.step_name == "apply_squilla_router"
)
assert router_record.applied is False
assert "timed out" in (router_record.fallback_reason or "")
@pytest.mark.asyncio
async def test_squilla_router_timeout_fails_open_for_blocking_router(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def blocking_router(ctx: TurnContext) -> TurnContext:
time.sleep(0.08)
ctx.model = "should-not-route"
return ctx
blocking_router.__name__ = "apply_squilla_router"
monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", blocking_router)
runner = TurnRunner(
provider_selector=None,
config=_config_with_router_timeout(),
)
provider = _Provider()
started = time.monotonic()
turn, resolved_provider = await asyncio.wait_for(
runner._run_pipeline(
"hello",
"agent:main:test",
provider,
None,
[],
"system prompt",
[],
),
timeout=0.25,
)
elapsed = time.monotonic() - started
assert elapsed < 0.075
assert resolved_provider is provider
assert turn.model != "should-not-route"
await asyncio.sleep(0.1)
assert turn.model != "should-not-route"
router_record = next(
record
for record in turn.metadata["pipeline_steps"]
if record.step_name == "apply_squilla_router"
)
assert router_record.applied is False
assert "timed out" in (router_record.fallback_reason or "")