"""Deterministic retrieval policy tests, separate from actual embedding/Qdrant recovery.""" from contextlib import nullcontext from datetime import UTC, datetime from types import SimpleNamespace import pytest from tht.memory.models import Card, MemoryNotFound from tht.memory.retrieval import RecallScope, expand_and_rank def card(identity, **values): return Card(id=identity, workspace_id="sales", family="domain_clarification", subject=identity, scope="Sales", origin="manual", revision="r", indexed=True, created_at=datetime.now(UTC), updated_at=datetime.now(UTC), **values) class Archive: def __init__(self, *cards): self.cards = {c.id: c for c in cards} self.reads = [] def get(self, identity): self.reads.append(identity) if identity not in self.cards: raise MemoryNotFound(identity) return self.cards[identity] def operation(self): return nullcontext(self) def hit(identity, **metadata): return SimpleNamespace(ref=identity, metadata={"memory_revision": "r", "memory_format": 2, **metadata}) def link(identity): return {"target_id": identity, "meaning": "Requires the grain clarification"} def rank(repo, hits, **kwargs): return expand_and_rank(repo, hits, scope=kwargs.pop("scope", RecallScope()), family="domain_clarification", excluded=kwargs.pop("excluded", set()), top=100, **kwargs) def test_link_only_candidates_cycles_duplicates_depth_and_current_content(): repo = Archive(card("a", links=[link("b")]), card("b", detail="Current correction", links=[link("a"), link("c")]), card("c", links=[link("d")]), card("d")) results = rank(repo, [hit("a"), hit("a")]) assert [r.card.id for r in results] == ["a", "b", "c"] assert results[1].card.detail == "Current correction" assert results[2].path == ("a", "b", "c") assert len(repo.reads) == len(set(repo.reads)) == 3 assert rank(repo, [hit("a")]) == results # duplicate seeds do not boost a score def test_direct_and_graph_candidates_are_reranked_together_without_cycle_boost(): repo = Archive(card("a", links=[link("c")]), card("b"), card("c", links=[link("a")])) results = rank(repo, [hit("a"), hit("b"), hit("c")]) assert [r.card.id for r in results] == ["a", "c", "b"] assert len(results) == 3 @pytest.mark.parametrize("invalid", ["missing", "pending", "excluded", "wrong_scope", "family"]) def test_ineligible_link_targets_cannot_be_returned_or_used_as_bridges(invalid): target = card("b", links=[link("c")]) if invalid == "pending": target.indexed = False if invalid == "wrong_scope": target.scope = "Purchases" if invalid == "family": target.family = "sql_rule" repo = Archive(card("a", links=[link("b")]), card("c"), *([] if invalid == "missing" else [target])) results = rank(repo, [hit("a")], scope=RecallScope(scope="Sales"), excluded={"b"} if invalid == "excluded" else set()) assert [r.card.id for r in results] == ["a"] @pytest.mark.parametrize("metadata", [{"memory_revision": "old"}, {"memory_format": 1}]) def test_stale_seeds_do_not_expand(metadata): repo = Archive(card("a", links=[link("b")]), card("b")) assert rank(repo, [hit("a", **metadata)]) == [] def test_physical_scope_matches_one_dependency_and_includes_workspace_and_parent_rules(): scope = RecallScope(database="sales", schema_name="public", table="orders", column="id") assert scope.matches(card("global")) assert scope.matches(card("database", dependencies=[{"database": "sales"}])) assert scope.matches(card("table", dependencies=[{"database": "sales", "schema_name": "public", "table": "orders"}])) assert not scope.matches(card("split", dependencies=[ {"database": "sales", "schema_name": "public", "table": "orders", "column": "amount"}, {"database": "purchases", "schema_name": "public", "table": "orders", "column": "id"}, ])) assert not scope.matches(card("schema", dependencies=[ {"database": "sales", "schema_name": "audit", "table": "orders", "column": "id"}, ])) def test_scope_and_concepts_are_explicit_and_combined(): scope = RecallScope(scope="Sales", concepts=["orders", "grain"]) assert scope.matches(card("a", concepts=["orders", "grain"])) assert not scope.matches(card("b", concepts=["orders"])) with pytest.raises(ValueError): RecallScope(column="id") def test_fanout_and_total_visits_are_bounded(): cards = [card(f"c{i:04}", links=[link(f"c{j:04}") for j in range(i+1, i+101)]) for i in range(500)] repo = Archive(*cards) results = rank(repo, [hit("c0000")]) assert "c0100" not in {r.card.id for r in results} assert len(repo.reads) <= 200 repo.reads.clear() rank(repo, [hit(c.id) for c in cards[:100]]) assert len(repo.reads) <= 200