- New: src/rag/engine/ — in-process hybrid search (FTS5 BM25 + sqlite-vec + LLM rerank) - New: src/rag/qmd/ — compatibility layer (qmd_query, qmd_chat, qmd_chat_stream, qmd_index_*) - New: src/ingest/stub_writer.py — .md stubs for binary files (videos, archives) - New: scripts/deploy.sh + scripts/pull_models.sh + Makefile + .env.example - Removed: LightRAG, sentence-transformers embedding via separate package, rag_standalone/ - Removed: @nousresearch/qmd npm dep (package not published); Node.js from Dockerfile - Updated: tests/ (46 passed), docker-compose, .dockerignore, config.yaml, README Engine: in-process Python (no daemon, no npm), sentence-transformers 384-dim, RRF fusion (k=60), BM25 + vector with numpy fallback. WebSocket API unchanged. Deploy: 'git clone' + 'make init' + 'make pull-models MODELS_SOURCE=...' + 'make up'. Models (5.83 GB) live outside git; pulled via rsync from dev host.
70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
"""Tests for qmd search-result cache (TTL + mtime invalidation)."""
|
|
|
|
import asyncio
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from src.rag.qmd.cache import SearchCache
|
|
|
|
|
|
class SearchCacheTestCase(unittest.IsolatedAsyncioTestCase):
|
|
async def test_get_set_roundtrip(self):
|
|
cache = SearchCache(ttl_seconds=60)
|
|
args = ("merakom", "2026", "bm25", True)
|
|
self.assertIsNone(cache.get("hello", args))
|
|
cache.set("hello", args, "ctx-blob")
|
|
self.assertEqual(cache.get("hello", args), "ctx-blob")
|
|
|
|
async def test_question_normalization(self):
|
|
cache = SearchCache(ttl_seconds=60)
|
|
args = ("merakom", "2026", "bm25", True)
|
|
cache.set("Hello World", args, "ctx")
|
|
self.assertEqual(cache.get(" hello world ", args), "ctx")
|
|
|
|
async def test_different_args_yield_different_keys(self):
|
|
cache = SearchCache(ttl_seconds=60)
|
|
cache.set("q", ("o", "p1", "bm25", True), "ctx1")
|
|
cache.set("q", ("o", "p2", "bm25", True), "ctx2")
|
|
self.assertEqual(cache.get("q", ("o", "p1", "bm25", True)), "ctx1")
|
|
self.assertEqual(cache.get("q", ("o", "p2", "bm25", True)), "ctx2")
|
|
|
|
async def test_set_skips_empty_value(self):
|
|
cache = SearchCache(ttl_seconds=60)
|
|
cache.set("q", ("o", "p", "bm25", True), "")
|
|
self.assertIsNone(cache.get("q", ("o", "p", "bm25", True)))
|
|
|
|
async def test_ttl_expiry(self):
|
|
cache = SearchCache(ttl_seconds=0)
|
|
args = ("o", "p", "bm25", True)
|
|
with patch("src.rag.qmd.cache._index_mtime", return_value=0.0):
|
|
cache.set("q", args, "ctx")
|
|
await asyncio.sleep(0.05)
|
|
self.assertIsNone(cache.get("q", args))
|
|
|
|
async def test_mtime_invalidation(self):
|
|
cache = SearchCache(ttl_seconds=60)
|
|
args = ("o", "p", "bm25", True)
|
|
with patch("src.rag.qmd.cache._index_mtime", return_value=10.0):
|
|
cache.set("q", args, "ctx")
|
|
with patch("src.rag.qmd.cache._index_mtime", return_value=20.0):
|
|
self.assertIsNone(cache.get("q", args))
|
|
|
|
async def test_clear(self):
|
|
cache = SearchCache(ttl_seconds=60)
|
|
cache.set("q", ("o", "p", "bm25", True), "ctx")
|
|
cache.clear()
|
|
self.assertIsNone(cache.get("q", ("o", "p", "bm25", True)))
|
|
|
|
async def test_stats(self):
|
|
cache = SearchCache(ttl_seconds=42)
|
|
stats = cache.stats()
|
|
self.assertEqual(stats["entries"], 0)
|
|
self.assertEqual(stats["ttl_seconds"], 42)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|