"""T1.1 网关核心单测:run / stream / 记账 / 路由 / 错误。""" from __future__ import annotations import uuid import pytest from fakes import FakeAdapter, FakeLedger, fake_route from ww_llm_gateway.gateway import Gateway from ww_llm_gateway.routing import resolve_route from ww_llm_gateway.types import LlmRequest, Scope from ww_shared import AppError, ErrorCode async def test_run_returns_text_usage_and_served_by(req: LlmRequest) -> None: adapter = FakeAdapter(text="第一章正文") ledger = FakeLedger() gw = Gateway({"deepseek": adapter}, ledger, resolver=fake_route) resp = await gw.run(req) assert resp.text == "第一章正文" assert resp.served_by.provider == "deepseek" assert resp.served_by.fell_back is False assert resp.usage.input_tokens == 100 assert resp.usage.output_tokens == 50 assert adapter.complete_calls == 1 async def test_run_writes_exactly_one_ledger_record(req: LlmRequest) -> None: ledger = FakeLedger() gw = Gateway({"deepseek": FakeAdapter()}, ledger, resolver=fake_route) await gw.run(req) assert len(ledger.records) == 1 assert ledger.records[0].provider == "deepseek" assert ledger.records[0].model == "deepseek-chat" async def test_run_computes_cost_from_pricing(req: LlmRequest) -> None: ledger = FakeLedger() gw = Gateway({"deepseek": FakeAdapter()}, ledger, resolver=fake_route) resp = await gw.run(req) # ceil(100/1e6*27 + 50/1e6*110) == 1 cent assert resp.usage.cost_minor == 1 assert resp.usage.currency == "USD" async def test_stream_yields_deltas_and_records_once(req: LlmRequest) -> None: ledger = FakeLedger() gw = Gateway({"deepseek": FakeAdapter(deltas=["a", "b", "c"])}, ledger, resolver=fake_route) collected = [d.text async for d in gw.stream(req)] assert "".join(collected) == "abc" assert len(ledger.records) == 1 assert ledger.records[0].output_tokens == 50 async def test_unknown_provider_raises_llm_unavailable(req: LlmRequest) -> None: gw = Gateway({}, FakeLedger(), resolver=fake_route) with pytest.raises(AppError) as exc: await gw.run(req) assert exc.value.code == ErrorCode.LLM_UNAVAILABLE def test_resolve_route_parses_tier_defaults() -> None: route = resolve_route("writer") assert route.provider == "deepseek" assert route.model == "deepseek-chat" def test_agent_passes_only_tier_never_model() -> None: # 不变量 ②:LlmRequest 无 model 字段,只有 tier req = LlmRequest(tier="analyst", input="x", scope=Scope(user_id=uuid.UUID(int=9))) assert "model" not in LlmRequest.model_fields assert req.tier == "analyst"