"""Shared pytest fixtures and builder helpers. WS-04 M1 (SL-3): the in-process ``session_store`` is a module-global ``dict`` with a hard capacity cap (``_MAX_SESSIONS``). Now that ``load_fixture_session`` / ``load_text_session`` / ``load_repo_session`` enforce that cap (previously only ``load_session`` did), tests that create sessions without tearing them down would accumulate across the whole run and eventually trip ``OverflowError("session_cap")`` in unrelated tests. This autouse fixture resets the session lifecycle state (records AND their paired locks) around every test so each test starts from an empty store — standard per-test isolation. Files with their own inline ``session_store``/``session_locks`` cleanup fixtures still work (clearing an already-empty dict is a no-op). TST-2: shared builder functions so individual test files can ``from conftest import`` them instead of duplicating local copies. Files may keep local overrides (different signatures, extra defaults) without touching these. """ from __future__ import annotations from collections.abc import Iterator import pytest from loosecanvas.contracts import Claim, Edge, Node, SceneState, SourceSnapshot from loosecanvas.graph_repository import LooseGraphRepository from loosecanvas.turn_logic import session_locks, session_store @pytest.fixture(autouse=True) def _reset_session_store() -> Iterator[None]: session_store.clear() session_locks.clear() yield session_store.clear() session_locks.clear() # ── Shared builder helpers ──────────────────────────────────────────────────── # These are importable functions (not fixtures) so test files can call them # directly: ``from conftest import _node, _edge``. # Signatures are the widest common denominator across the existing test files; # individual files may keep narrower local overrides. def _node( nid: str, label: str | None = None, *, kind: str = "concept", summary: str = "", ) -> Node: """Build a minimal ``Node``. Default ``kind="concept"`` matches most tests.""" return Node(id=nid, kind=kind, label=label or nid, summary=summary) # type: ignore[arg-type] def _edge( eid: str, src: str, tgt: str, *, kind: str = "relates_to", ) -> Edge: """Build a minimal ``Edge``. Default ``kind="relates_to"`` matches most tests.""" return Edge(id=eid, source=src, target=tgt, kind=kind) # type: ignore[arg-type] def _graph( *node_ids: str, edges: list[Edge] | None = None, ) -> LooseGraphRepository: """Build a ``LooseGraphRepository`` from bare node-id strings.""" repo = LooseGraphRepository() repo.upsert_nodes([_node(nid) for nid in node_ids]) if edges: repo.upsert_edges(edges) return repo def _scene( *, visible_nodes: list[str] | None = None, visible_edges: list[str] | None = None, fogged: list[str] | None = None, ) -> SceneState: """Build a minimal ``SceneState``.""" return SceneState( visible_node_ids=visible_nodes or [], visible_edge_ids=visible_edges or [], fogged_node_ids=fogged or [], ) def _claim( cid: str, target: str, *, origin: str = "model_inferred", claim_type: str = "label", support_state: str = "unverified", review_state: str = "pending", text: str = "test", summary: str = "", ) -> Claim: """Build a minimal ``Claim``. The ``summary`` kwarg is accepted for compatibility with ``test_context_packer`` / ``test_t3_03`` callers that pass ``summary=``. It is silently ignored here because ``Claim`` has no such field; those callers should instead set it on the associated ``Node``. """ return Claim( id=cid, claim_type=claim_type, # type: ignore[arg-type] target_id=target, text=text, origin=origin, # type: ignore[arg-type] support_state=support_state, # type: ignore[arg-type] review_state=review_state, # type: ignore[arg-type] ) def _make_repo( nodes: list[Node] | None = None, edges: list[Edge] | None = None, claims: list[Claim] | None = None, ) -> LooseGraphRepository: """Build a ``LooseGraphRepository`` from typed lists.""" return LooseGraphRepository.from_extraction( nodes or [], edges or [], claims or [], SourceSnapshot(), )