transcription/tests/test_qmd_cache.py
keboss-m eee8f4c8a4 Replace LightRAG with native Python RAG engine + add deploy tooling
- 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.
2026-06-10 14:24:01 +03:00

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()