b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
"""Tests for FactRetriever FTS5 query sanitization.
|
|
|
|
These tests cover the fix where raw natural-language queries passed to
|
|
FTS5 MATCH were AND-joined by default, dropping recall to zero on any
|
|
multi-word prose query. The sanitizer drops stopwords and OR-joins the
|
|
remaining content tokens as phrase literals.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("numpy") # retrieval module imports numpy indirectly
|
|
|
|
from plugins.memory.holographic.retrieval import FactRetriever
|
|
from plugins.memory.holographic.store import MemoryStore
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _sanitize_fts_query — unit tests (no DB required)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize(
|
|
"query,expected_tokens",
|
|
[
|
|
# stopwords dropped
|
|
("what happened with the deployment rollback", {"happened", "deployment", "rollback"}),
|
|
# single content word passes through
|
|
("compaction", {"compaction"}),
|
|
# all stopwords → falls back to raw
|
|
("the and of", None), # None = sentinel for fallback-to-raw
|
|
# empty string → empty output
|
|
("", ""),
|
|
# FTS5 operator characters stripped
|
|
("context: length-probe", {"context", "lengthprobe"}),
|
|
# trailing punctuation stripped by tokenizer
|
|
("hello, world!", {"hello", "world"}),
|
|
],
|
|
)
|
|
def test_sanitize_fts_query_extracts_content_tokens(query, expected_tokens):
|
|
result = FactRetriever._sanitize_fts_query(query)
|
|
|
|
if expected_tokens == "":
|
|
assert result == ""
|
|
return
|
|
|
|
if expected_tokens is None:
|
|
# Pathological case: all stopwords — should fall back to raw query
|
|
assert result == query
|
|
return
|
|
|
|
# OR-joined phrase literals: `"tok1" OR "tok2" OR ...`
|
|
# Extract the tokens between quotes, order-independent.
|
|
import re
|
|
matches = re.findall(r'"([^"]+)"', result)
|
|
assert set(matches) == expected_tokens, f"got {result!r}"
|
|
|
|
|
|
def test_sanitize_fts_query_never_crashes_on_fts5_specials():
|
|
"""Queries with FTS5 operator characters must not produce malformed SQL."""
|
|
problematic = [
|
|
'test " query',
|
|
"test * query",
|
|
"test (a OR b) query",
|
|
"test^2 query",
|
|
"test:colon query",
|
|
"test-hyphen query",
|
|
"a" * 1000, # long query
|
|
]
|
|
for q in problematic:
|
|
result = FactRetriever._sanitize_fts_query(q)
|
|
# We just need it to return a string without raising
|
|
assert isinstance(result, str)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration test — actually run _fts_candidates against an in-memory DB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture
|
|
def retriever_with_facts(tmp_path):
|
|
"""MemoryStore seeded with a few facts for retrieval tests."""
|
|
db_path = tmp_path / "test_facts.db"
|
|
store = MemoryStore(str(db_path))
|
|
store.add_fact(
|
|
content="The Thursday deployment rollback failed because of stale migration state.",
|
|
category="project",
|
|
)
|
|
store.add_fact(
|
|
content="Compaction settings tuned to 0.85 threshold.",
|
|
category="tool",
|
|
)
|
|
store.add_fact(
|
|
content="Venice.ai advertises availableContextTokens inside model_spec.",
|
|
category="tool",
|
|
)
|
|
retriever = FactRetriever(store=store)
|
|
yield retriever
|
|
store.close()
|
|
|
|
|
|
def test_prefetch_recovers_prose_query(retriever_with_facts):
|
|
"""A natural-language query should now match the relevant fact.
|
|
|
|
Before the sanitizer fix, 'what happened with the deployment rollback'
|
|
returned zero hits because FTS5 required every token to co-occur.
|
|
"""
|
|
results = retriever_with_facts.search(
|
|
"what happened with the deployment rollback"
|
|
)
|
|
assert len(results) >= 1
|
|
# The top hit should be the deployment rollback fact
|
|
assert "deployment rollback" in results[0]["content"].lower()
|
|
|
|
|
|
def test_prefetch_single_keyword_still_works(retriever_with_facts):
|
|
"""Single-term queries (pre-fix working case) remain working."""
|
|
results = retriever_with_facts.search("compaction")
|
|
assert len(results) >= 1
|
|
assert "Compaction" in results[0]["content"] or "compaction" in results[0]["content"].lower()
|
|
|
|
|
|
def test_prefetch_stopword_only_query_empty(retriever_with_facts):
|
|
"""Pure stopword queries return zero results but don't crash."""
|
|
# Pass to _sanitize_fts_query directly first so we know what happens
|
|
assert FactRetriever._sanitize_fts_query("the and of") == "the and of"
|
|
# search() handles the likely-zero-hit case gracefully
|
|
results = retriever_with_facts.search("the and of")
|
|
# Either zero results or it errored-gracefully to [] — both are fine
|
|
assert isinstance(results, list)
|