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---
89 lines
2.8 KiB
Python
89 lines
2.8 KiB
Python
"""SessionStore tests — create/append/window/LRU/agent scoping (D-019)."""
|
|
|
|
import pytest
|
|
|
|
from ai_service.agents.session import InMemorySessionStore
|
|
from ai_service.llm.types import Message
|
|
|
|
|
|
def msg(n: int) -> Message:
|
|
return Message(role="user", content=f"m{n}")
|
|
|
|
|
|
async def test_create_and_get():
|
|
store = InMemorySessionStore()
|
|
session = await store.create("s1", agent="coach")
|
|
assert session.agent == "coach"
|
|
got = await store.get("s1")
|
|
assert got is not None
|
|
assert got.session_id == "s1"
|
|
assert await store.get("missing") is None
|
|
|
|
|
|
async def test_append_and_history_window():
|
|
store = InMemorySessionStore()
|
|
await store.create("s1", agent="coach")
|
|
for i in range(30):
|
|
await store.append("s1", msg(i))
|
|
window = await store.history_window("s1", max_messages=20)
|
|
assert len(window) == 20
|
|
assert window[0].content == "m10" # last 20 of m0..m29
|
|
assert window[-1].content == "m29"
|
|
|
|
|
|
async def test_default_window_uses_20():
|
|
store = InMemorySessionStore()
|
|
await store.create("s1", agent="coach")
|
|
for i in range(25):
|
|
await store.append("s1", msg(i))
|
|
window = await store.history_window("s1")
|
|
assert len(window) == 20
|
|
assert window[0].content == "m5"
|
|
|
|
|
|
async def test_lru_eviction_at_cap():
|
|
store = InMemorySessionStore(window=20, max_sessions=3)
|
|
for i in range(3):
|
|
await store.create(f"s{i}", agent="coach")
|
|
# touch s0 so s1 becomes least-recently-used
|
|
await store.get("s0")
|
|
await store.create("s3", agent="coach") # evicts s1
|
|
assert await store.get("s1") is None
|
|
assert await store.get("s0") is not None
|
|
assert await store.get("s2") is not None
|
|
assert await store.get("s3") is not None
|
|
|
|
|
|
async def test_lru_eviction_at_default_500_cap():
|
|
store = InMemorySessionStore() # defaults: window=20, max_sessions=500
|
|
for i in range(500):
|
|
await store.create(f"s{i}", agent="coach")
|
|
await store.get("s0") # touch the oldest → s1 becomes least-recently-used
|
|
await store.create("s500", agent="coach") # evicts s1
|
|
assert await store.get("s1") is None
|
|
assert await store.get("s0") is not None
|
|
assert await store.get("s499") is not None
|
|
assert await store.get("s500") is not None
|
|
|
|
|
|
async def test_append_unknown_session_raises():
|
|
store = InMemorySessionStore()
|
|
with pytest.raises(KeyError):
|
|
await store.append("nope", msg(0))
|
|
|
|
|
|
async def test_delete():
|
|
store = InMemorySessionStore()
|
|
await store.create("s1", agent="coach")
|
|
await store.delete("s1")
|
|
assert await store.get("s1") is None
|
|
|
|
|
|
async def test_sessions_are_agent_scoped():
|
|
store = InMemorySessionStore()
|
|
a = await store.create("coach-session", agent="coach")
|
|
b = await store.create("tutor-session", agent="tutor")
|
|
assert a.agent == "coach"
|
|
assert b.agent == "tutor"
|
|
assert a.session_id != b.session_id
|