dc00a4f8f5
Wave 5-1 mypy adoption (D-058 relaxed-first): mypy 2.3.1, [tool.mypy] check_untyped_defs + no_implicit_optional + warn_unused_ignores + warn_redundant_casts + warn_unreachable, no overrides (every dep ships py.typed), files = ai_service + scripts/sandbox-agent.py + tests (not excluded). 215 errors → 0: ~140 fixed at source incl. genuine bugs found by the gate (LLMProvider protocol async-generator shape, SessionStore protocol sync/async mismatch, structured_reply genericity, get_args migrations, sandbox-agent Optional guard honesty, proctor test arg-order, dead ExecResult kwarg, 16 sqlmodel col() sites); 34 live suppressions scoped-coded + reasoned. Original 27 type:ignore dispositioned: 12 dead removed, 15 scoped (warn_unused_ignores self-eliminates the rest). Wired: scripts/typecheck.sh + package.json + turbo task + root ai:typecheck — pnpm typecheck now 5 tsc + mypy. Wave 5-2 recharts 2.15.4 → 3.10.1 (D-049/D-059): one breaking class (Tooltip formatter ValueType typing); 2 one-line inference fixes on the 2 chart files; no rollback needed (well inside the G-106 timebox). Deprecated direct dep resolved; uuid@9 gone from the tree. Build 248/248 with chart SSR markers verified in exported employer + dashboard pages. Wave 5-3 deprecated-transitive audit (G-109): 4 lockfile entries remain, all dev-only, none actionable within NFR budget — glob@7.2.3 + inflight + rimraf@3 via Storybook docgen chain (flat-cache@3); glob@10.5.0 via c8→test-exclude (D-052-binding coverage tool — swapping it is out of budget). Python: no EOL packages; zero-warning regime under filterwarnings=error is the machine-checked proof. Counts: 670 py / 44 web / 64 cli, zero warnings, G-104 allowlist still exactly 2. ---ci--- phase: 5 milestone: v0.7 status: execute ---/ci---
76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""MockProvider tests — determinism, JSON mode, failure modes."""
|
|
|
|
import pytest
|
|
|
|
from ai_service.llm.mock import MockProvider
|
|
from ai_service.llm.types import Message
|
|
|
|
MSGS = [Message(role="user", content="hello there")]
|
|
|
|
|
|
async def test_stream_is_deterministic():
|
|
p1, p2 = MockProvider(), MockProvider()
|
|
out1 = [t async for t in p1.stream_chat(MSGS, model="m")]
|
|
out2 = [t async for t in p2.stream_chat(MSGS, model="m")]
|
|
assert "".join(out1) == "".join(out2)
|
|
assert out1 == out2
|
|
|
|
|
|
async def test_stream_content_differs_for_different_input():
|
|
p = MockProvider()
|
|
a = "".join([t async for t in p.stream_chat(MSGS, model="m")])
|
|
b = "".join(
|
|
[t async for t in p.stream_chat([Message(role="user", content="other")], model="m")]
|
|
)
|
|
assert a != b
|
|
|
|
|
|
async def test_json_object_response_format():
|
|
import json
|
|
|
|
p = MockProvider()
|
|
out = "".join(
|
|
[
|
|
t
|
|
async for t in p.stream_chat(
|
|
MSGS, model="m", response_format={"type": "json_object"}
|
|
)
|
|
]
|
|
)
|
|
assert json.loads(out) == {"summary": "mock structured reply", "confidence": 0.87}
|
|
|
|
|
|
async def test_fail_before_first_token():
|
|
p = MockProvider()
|
|
p.fail_before_first_token = True
|
|
with pytest.raises(RuntimeError):
|
|
async for _ in p.stream_chat(MSGS, model="m"):
|
|
pass
|
|
|
|
|
|
async def test_fail_mid_stream():
|
|
p = MockProvider()
|
|
p.fail_mid_stream_at_index = 2
|
|
tokens = []
|
|
with pytest.raises(RuntimeError):
|
|
async for t in p.stream_chat(MSGS, model="m"):
|
|
tokens.append(t)
|
|
assert len(tokens) == 2
|
|
|
|
|
|
async def test_cancellation_records_abort():
|
|
p = MockProvider()
|
|
gen = p.stream_chat(MSGS, model="m")
|
|
await gen.__anext__()
|
|
# P5 scoped: async generators expose aclose(); the declared protocol
|
|
# return type (AsyncIterator) doesn't carry it in mypy's view.
|
|
await gen.aclose() # type: ignore[attr-defined]
|
|
assert p.abort_recorded is True
|
|
|
|
|
|
async def test_chat_returns_full_reply():
|
|
p = MockProvider()
|
|
reply = await p.chat(MSGS, model="m")
|
|
streamed = "".join([t async for t in p.stream_chat(MSGS, model="m")])
|
|
assert reply == streamed
|