Files
nextcraft/apps/ai-service/tests/agents/test_session.py
T
CIAgent dc00a4f8f5 chore(P05): type & dependency hardening — mypy + recharts 3 (REQ-7-005)
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---
2026-09-27 10:08:07 +00:00

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