Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
411 lines
15 KiB
Python
411 lines
15 KiB
Python
"""Plan 06-04 R4: cue-detection router tests.
|
||
|
||
Covers:
|
||
- Task 1: classifier function _classify_cue (8 unit tests + parameterized).
|
||
- Task 3: dispatch integration (5 tests appended after the wiring lands).
|
||
|
||
Per naming convention and SPEC R4 acceptance:
|
||
- 6 verbatim-positive cues (3 EN + 3 RU) covered.
|
||
- 6 concept-negative cues covered.
|
||
- triggered_pattern label surfaced for diagnostics (logged, not in response).
|
||
- case-insensitivity for EN word-marker.
|
||
- RU patterns anchored at start-of-string.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
|
||
# --------------------------------------------------------------------- Task 1
|
||
|
||
|
||
def test_module_exposes_compiled_trigger_lists():
|
||
"""EN_TRIGGERS and RU_TRIGGERS must be present and contain 4 entries each."""
|
||
from iai_mcp.cue_router import EN_TRIGGERS, RU_TRIGGERS
|
||
|
||
assert len(EN_TRIGGERS) == 4, f"EN_TRIGGERS must have 4 entries, got {len(EN_TRIGGERS)}"
|
||
assert len(RU_TRIGGERS) == 4, f"RU_TRIGGERS must have 4 entries, got {len(RU_TRIGGERS)}"
|
||
|
||
# Each entry is (label, compiled_pattern)
|
||
for label, pat in EN_TRIGGERS:
|
||
assert isinstance(label, str) and label, "EN trigger label must be non-empty str"
|
||
assert hasattr(pat, "search"), f"EN trigger pattern for {label!r} must be compiled regex"
|
||
for label, pat in RU_TRIGGERS:
|
||
assert isinstance(label, str) and label, "RU trigger label must be non-empty str"
|
||
assert hasattr(pat, "search"), f"RU trigger pattern for {label!r} must be compiled regex"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"cue",
|
||
[
|
||
"find the verbatim quote about migration",
|
||
"what did the user say on day 17?",
|
||
"show me the exact phrase about cleanup",
|
||
],
|
||
)
|
||
def test_classify_cue_en_verbatim_positives(cue):
|
||
"""3 EN verbatim-positive cues each return mode=verbatim."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
mode, pattern = _classify_cue(cue)
|
||
assert mode == "verbatim", f"cue {cue!r} should classify as verbatim, got {mode!r}"
|
||
assert pattern is not None, f"cue {cue!r} should report a triggered_pattern label"
|
||
|
||
|
||
def test_classify_cue_en_quoted_phrase():
|
||
"""EN positive: cue containing a "..." quoted phrase routes to verbatim."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
mode, pattern = _classify_cue('recall "lancedb pre-cleanup snapshot" verbatim')
|
||
assert mode == "verbatim"
|
||
# quoted-phrase OR word-marker may match first; both label types are valid.
|
||
assert pattern in ("quoted-phrase", "word-marker"), (
|
||
f"expected quoted-phrase or word-marker label, got {pattern!r}"
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"cue",
|
||
[
|
||
"найди дословно сообщение о схема-чистке",
|
||
"точная цитата про deg_norm",
|
||
"что я сказал в прошлой сессии о dedup",
|
||
],
|
||
)
|
||
def test_classify_cue_ru_verbatim_positives(cue):
|
||
"""3 RU starts-with cues each return mode=verbatim."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
mode, pattern = _classify_cue(cue)
|
||
assert mode == "verbatim", f"cue {cue!r} should classify as verbatim, got {mode!r}"
|
||
assert pattern is not None, f"cue {cue!r} should report a triggered_pattern label"
|
||
assert pattern.startswith("ru-start-"), (
|
||
f"expected ru-start-* label for cue {cue!r}, got {pattern!r}"
|
||
)
|
||
|
||
|
||
def test_classify_cue_ru_european_quote_marker():
|
||
"""EN positive (european-quote): cue with «...» routes to verbatim."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
mode, pattern = _classify_cue('recall the «schema_reinforced event payload» definition')
|
||
assert mode == "verbatim"
|
||
assert pattern == "european-quote"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"cue",
|
||
[
|
||
"tell me about schema dedup",
|
||
"how does the rank stage work",
|
||
"community structure of the live store",
|
||
"каков статус Phase 6",
|
||
"sleep daemon REM cycle behaviour",
|
||
"что нового в проекте",
|
||
],
|
||
)
|
||
def test_classify_cue_concept_negatives(cue):
|
||
"""6 concept-negative cues each return mode=concept and triggered_pattern=None."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
mode, pattern = _classify_cue(cue)
|
||
assert mode == "concept", f"cue {cue!r} should classify as concept, got {mode!r}"
|
||
assert pattern is None, f"cue {cue!r} should not have a triggered_pattern, got {pattern!r}"
|
||
|
||
|
||
def test_classify_cue_triggered_pattern_label_non_none_for_verbatim():
|
||
"""Every verbatim cue carries a non-None triggered_pattern; every concept cue carries None."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
verbatim_cues = [
|
||
"verbatim quote please",
|
||
"what I said on day 7",
|
||
'"quoted text"',
|
||
"найди дословно вот это",
|
||
]
|
||
for cue in verbatim_cues:
|
||
mode, pattern = _classify_cue(cue)
|
||
assert mode == "verbatim", f"{cue!r} -> mode {mode!r}"
|
||
assert pattern is not None, f"{cue!r} -> pattern None"
|
||
|
||
concept_cues = [
|
||
"what is the architecture",
|
||
"general project status",
|
||
"опиши структуру проекта",
|
||
]
|
||
for cue in concept_cues:
|
||
mode, pattern = _classify_cue(cue)
|
||
assert mode == "concept", f"{cue!r} -> mode {mode!r}"
|
||
assert pattern is None, f"{cue!r} -> pattern {pattern!r}"
|
||
|
||
|
||
def test_classify_cue_case_insensitive_en():
|
||
"""EN word-marker honours re.IGNORECASE: VERBATIM, EXACT, Quote all match."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
for cue in ("VERBATIM what did I say", "EXACT phrase", "Quote me on this"):
|
||
mode, _pat = _classify_cue(cue)
|
||
assert mode == "verbatim", f"case-insensitive match failed for {cue!r}"
|
||
|
||
|
||
def test_classify_cue_ru_patterns_anchored_at_start():
|
||
"""RU triggers require the cue to START with the phrase; mid-string match returns concept."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
# Mid-string occurrence -> concept (RU patterns anchored ^ at start).
|
||
mode_mid, pattern_mid = _classify_cue("remind me, найди дословно not in middle")
|
||
assert mode_mid == "concept", (
|
||
f"RU trigger should NOT match mid-string, got mode={mode_mid!r} pattern={pattern_mid!r}"
|
||
)
|
||
|
||
# Start-of-string occurrence -> verbatim.
|
||
mode_start, pattern_start = _classify_cue("найди дословно вот эту фразу")
|
||
assert mode_start == "verbatim"
|
||
assert pattern_start == "ru-start-найди-дословно"
|
||
|
||
|
||
def test_classify_cue_empty_string_returns_concept():
|
||
"""Empty / None-ish cue returns concept (defensive default)."""
|
||
from iai_mcp.cue_router import _classify_cue
|
||
|
||
mode, pattern = _classify_cue("")
|
||
assert mode == "concept"
|
||
assert pattern is None
|
||
|
||
|
||
# ============================================================================
|
||
# Task 3 — dispatch integration tests
|
||
# ============================================================================
|
||
|
||
# Reuses the _ControlledEmbedder pattern + helper builders so
|
||
# dispatch end-to-end tests can pin the embedder side-effect (advisor #5).
|
||
|
||
|
||
from datetime import datetime, timezone # noqa: E402 -- co-located fixtures
|
||
from uuid import uuid4 # noqa: E402
|
||
|
||
import numpy as np # noqa: E402
|
||
|
||
from iai_mcp.types import EMBED_DIM, MemoryRecord # noqa: E402
|
||
|
||
|
||
class _DispatchEmbedder:
|
||
"""Lightweight embedder for the dispatch tests — pins fixed cue vectors
|
||
so dispatch's embedder_for_store-loaded bge does not destroy the
|
||
hand-crafted geometry. Same trick as Plan 06-03's deviation #1.
|
||
"""
|
||
|
||
DIM = EMBED_DIM
|
||
|
||
def __init__(self) -> None:
|
||
self.fixed: dict[str, list[float]] = {}
|
||
|
||
def set_fixed(self, text: str, vec: list[float]) -> None:
|
||
self.fixed[text] = list(vec)
|
||
|
||
def embed(self, text: str) -> list[float]:
|
||
if text in self.fixed:
|
||
return list(self.fixed[text])
|
||
import hashlib
|
||
import random
|
||
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||
rng = random.Random(int(digest[:16], 16))
|
||
v = [rng.random() * 2 - 1 for _ in range(self.DIM)]
|
||
norm = sum(x * x for x in v) ** 0.5
|
||
return [x / norm for x in v] if norm > 0 else v
|
||
|
||
def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
||
return [self.embed(t) for t in texts]
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _isolated_keyring(monkeypatch: pytest.MonkeyPatch):
|
||
import keyring as _keyring
|
||
|
||
fake: dict[tuple[str, str], str] = {}
|
||
monkeypatch.setattr(_keyring, "get_password", lambda s, u: fake.get((s, u)))
|
||
monkeypatch.setattr(
|
||
_keyring, "set_password", lambda s, u, p: fake.__setitem__((s, u), p)
|
||
)
|
||
monkeypatch.setattr(
|
||
_keyring, "delete_password", lambda s, u: fake.pop((s, u), None)
|
||
)
|
||
yield fake
|
||
|
||
|
||
def _seed_populated_store(tmp_path):
|
||
"""Seed a store with one episodic record matching the test cues so
|
||
dispatch returns a non-empty hits list under either mode.
|
||
"""
|
||
from iai_mcp.store import MemoryStore
|
||
|
||
store = MemoryStore(path=tmp_path / "lancedb")
|
||
embedder = _DispatchEmbedder()
|
||
|
||
cue_text = "verbatim quote about migration snapshot"
|
||
cue_vec = embedder.embed(cue_text)
|
||
embedder.set_fixed(cue_text, cue_vec)
|
||
|
||
# One episodic record whose embedding matches the cue (cos=1.0).
|
||
now = datetime.now(timezone.utc)
|
||
rec = MemoryRecord(
|
||
id=uuid4(),
|
||
tier="episodic",
|
||
literal_surface="verbatim record about migration snapshot",
|
||
aaak_index="",
|
||
embedding=list(cue_vec),
|
||
community_id=None,
|
||
centrality=0.0,
|
||
detail_level=2,
|
||
pinned=False,
|
||
stability=0.0,
|
||
difficulty=0.0,
|
||
last_reviewed=None,
|
||
never_decay=False,
|
||
never_merge=False,
|
||
provenance=[],
|
||
created_at=now,
|
||
updated_at=now,
|
||
tags=[],
|
||
language="en",
|
||
)
|
||
store.insert(rec)
|
||
|
||
return store, embedder, cue_text, rec
|
||
|
||
|
||
def test_dispatch_routes_verbatim_cue_to_verbatim_mode(tmp_path, monkeypatch):
|
||
"""Dispatch with a populated store + verbatim cue: response.cue_mode == 'verbatim'."""
|
||
from iai_mcp import core
|
||
from iai_mcp import embed as _embed_mod
|
||
|
||
store, embedder, cue, rec = _seed_populated_store(tmp_path)
|
||
monkeypatch.setattr(_embed_mod, "embedder_for_store", lambda _store: embedder)
|
||
|
||
response = core.dispatch(
|
||
store, "memory_recall",
|
||
{"cue": cue, "session_id": "verb_cue", "cue_embedding": embedder.embed(cue)},
|
||
)
|
||
assert response["cue_mode"] == "verbatim", (
|
||
f"verbatim cue should classify to verbatim mode, got {response['cue_mode']!r}"
|
||
)
|
||
assert "patterns_observed" in response, "patterns_observed must be in response"
|
||
assert isinstance(response["patterns_observed"], list)
|
||
|
||
|
||
def test_dispatch_routes_concept_cue_to_concept_mode(tmp_path, monkeypatch):
|
||
"""Dispatch with a populated store + concept cue: response.cue_mode == 'concept'."""
|
||
from iai_mcp import core
|
||
from iai_mcp import embed as _embed_mod
|
||
|
||
store, embedder, _cue, _rec = _seed_populated_store(tmp_path)
|
||
monkeypatch.setattr(_embed_mod, "embedder_for_store", lambda _store: embedder)
|
||
|
||
concept_cue = "tell me about cleanup"
|
||
embedder.set_fixed(concept_cue, embedder.embed(concept_cue))
|
||
response = core.dispatch(
|
||
store, "memory_recall",
|
||
{"cue": concept_cue, "session_id": "concept_cue",
|
||
"cue_embedding": embedder.embed(concept_cue)},
|
||
)
|
||
assert response["cue_mode"] == "concept", (
|
||
f"concept cue should classify to concept mode, got {response['cue_mode']!r}"
|
||
)
|
||
assert "patterns_observed" in response
|
||
|
||
|
||
def test_dispatch_empty_store_fallback_honours_classified_mode(tmp_path):
|
||
"""records_count==0 path: retrieve.recall is exercised; cue_mode reflects the
|
||
classifier's verdict; hits[] empty (no records to return) but the field is
|
||
still episodic-only-shaped (verbatim filter wouldn't matter)."""
|
||
from iai_mcp import core
|
||
from iai_mcp.store import MemoryStore
|
||
|
||
store = MemoryStore(path=tmp_path / "lancedb") # empty
|
||
response = core.dispatch(
|
||
store, "memory_recall",
|
||
{"cue": "verbatim quote please", "session_id": "fallback",
|
||
"cue_embedding": [0.0] * EMBED_DIM},
|
||
)
|
||
assert response["cue_mode"] == "verbatim", (
|
||
f"verbatim cue should classify even on the fallback (empty-store) path, "
|
||
f"got {response['cue_mode']!r}"
|
||
)
|
||
|
||
|
||
def test_dispatch_passes_mode_kwarg_to_recall_for_response(tmp_path, monkeypatch):
|
||
"""Monkeypatch recall_for_response to capture kwargs; assert mode kwarg passed.
|
||
|
||
entry-point split: core.dispatch calls recall_for_response
|
||
(production answer-packing) instead of the deleted pipeline_recall.
|
||
The mode-plumbing acceptance criterion is preserved verbatim — the
|
||
cue-classifier output flows unchanged into the new entry point.
|
||
"""
|
||
from iai_mcp import core
|
||
from iai_mcp import pipeline as _pipeline_mod
|
||
from iai_mcp import embed as _embed_mod
|
||
from iai_mcp.types import RecallResponse
|
||
|
||
store, embedder, _cue, _rec = _seed_populated_store(tmp_path)
|
||
monkeypatch.setattr(_embed_mod, "embedder_for_store", lambda _store: embedder)
|
||
|
||
captured: dict = {}
|
||
|
||
def fake_recall_for_response(**kwargs):
|
||
captured.update(kwargs)
|
||
return RecallResponse(
|
||
hits=[], anti_hits=[], activation_trace=[], budget_used=0,
|
||
cue_mode=kwargs.get("mode", "concept"),
|
||
patterns_observed=[],
|
||
)
|
||
|
||
monkeypatch.setattr(_pipeline_mod, "recall_for_response", fake_recall_for_response)
|
||
|
||
verbatim_cue = "verbatim recall this exact quote"
|
||
response = core.dispatch(
|
||
store, "memory_recall",
|
||
{"cue": verbatim_cue, "session_id": "kwarg_capture",
|
||
"cue_embedding": embedder.embed(verbatim_cue)},
|
||
)
|
||
assert "mode" in captured, "dispatch must pass mode kwarg to recall_for_response"
|
||
assert captured["mode"] == "verbatim", (
|
||
f"verbatim cue should propagate as mode='verbatim' to recall_for_response, "
|
||
f"got mode={captured.get('mode')!r}"
|
||
)
|
||
assert response["cue_mode"] == "verbatim"
|
||
|
||
|
||
def test_dispatch_passes_mode_kwarg_to_retrieve_recall(tmp_path, monkeypatch):
|
||
"""Empty-store fallback path: monkeypatch retrieve.recall, assert mode passed."""
|
||
from iai_mcp import core
|
||
from iai_mcp import retrieve as _retrieve_mod
|
||
from iai_mcp.store import MemoryStore
|
||
from iai_mcp.types import RecallResponse
|
||
|
||
store = MemoryStore(path=tmp_path / "lancedb") # empty -> fallback path
|
||
|
||
captured: dict = {}
|
||
|
||
def fake_recall(**kwargs):
|
||
captured.update(kwargs)
|
||
return RecallResponse(
|
||
hits=[], anti_hits=[], activation_trace=[], budget_used=0,
|
||
cue_mode=kwargs.get("mode", "verbatim"),
|
||
patterns_observed=[],
|
||
)
|
||
|
||
monkeypatch.setattr(_retrieve_mod, "recall", fake_recall)
|
||
|
||
response = core.dispatch(
|
||
store, "memory_recall",
|
||
{"cue": "verbatim quote about something", "session_id": "fallback_kwarg",
|
||
"cue_embedding": [0.0] * EMBED_DIM},
|
||
)
|
||
assert "mode" in captured, (
|
||
"dispatch must pass mode kwarg to retrieve.recall on empty-store fallback"
|
||
)
|
||
assert captured["mode"] == "verbatim", (
|
||
f"verbatim cue should propagate as mode='verbatim' to retrieve.recall, "
|
||
f"got mode={captured.get('mode')!r}"
|
||
)
|
||
assert response["cue_mode"] == "verbatim"
|