from __future__ import annotations import importlib.util import tempfile import unittest import unittest.mock from unittest import mock from pathlib import Path SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "xkb_memory_service.py" spec = importlib.util.spec_from_file_location("xkb_memory_service", SCRIPT) assert spec and spec.loader module = importlib.util.module_from_spec(spec) import sys sys.modules[spec.name] = module spec.loader.exec_module(module) Store = module.Store _wiki_patch = None def setUpModule() -> None: """讓測試不要碰到真實的知識庫。 recall 加入 wiki 搜尋之後,任何呼叫 knowledge_recall 的測試都會去載入 使用者真實的語意索引(本機 1,872 筆),再對 Gemini 打一次 embedding。 那讓測試變慢、要花錢、依賴網路,而且結果隨著知識庫內容改變—— 在這台機器上甚至會 segfault。 預設回空,個別測試要驗 wiki 行為時再自己 patch。 """ global _wiki_patch _wiki_patch = mock.patch.object(module.KnowledgeCatalog, "_wiki_search", return_value=[]) _wiki_patch.start() def tearDownModule() -> None: if _wiki_patch is not None: _wiki_patch.stop() class XKBMemoryServiceTests(unittest.TestCase): def setUp(self) -> None: self.tmp = tempfile.TemporaryDirectory() self.store = Store(Path(self.tmp.name) / "memory.sqlite") self.store.catalog.index_file = Path(self.tmp.name) / "search_index.json" self.store.catalog.cards_dir = Path(self.tmp.name) / "cards" self.store.catalog.wiki_topics_dir = Path(self.tmp.name) / "wiki" / "topics" def tearDown(self) -> None: self.tmp.cleanup() def test_session_turn_complete_recall_and_idempotency(self) -> None: session = self.store.open_session( { "source": "fixture", "agent_id": "fixture", "namespace": "private", "session_key": "session-1", } ) resumed = self.store.open_session( { "source": "fixture", "agent_id": "fixture", "namespace": "private", "session_key": "session-1", } ) self.assertFalse(session["resumed"]) self.assertTrue(resumed["resumed"]) turn = self.store.start_turn( { "session_id": session["session_id"], "turn_id": "turn-1", "query": "shared memory query", } ) self.assertFalse(turn["resumed"]) completed = self.store.complete_turn( "turn-1", { "session_id": session["session_id"], "query": "shared memory query", "answer": "shared memory answer", "status": "succeeded", "content": {"answer": "shared memory answer"}, }, ) duplicate = self.store.complete_turn( "turn-1", { "session_id": session["session_id"], "query": "shared memory query", "answer": "shared memory answer", "status": "succeeded", "content": {"answer": "shared memory answer"}, }, ) self.assertTrue(completed["stored"]) self.assertEqual(completed["retrieval"]["query"], "shared memory query") with self.store.connect() as db: persisted = db.execute("SELECT status, retrieval_json, trace_id FROM turns WHERE turn_id=?", ("turn-1",)).fetchone() self.assertEqual(persisted["status"], "succeeded") self.assertEqual(persisted["trace_id"], completed["trace_id"]) self.assertEqual(__import__("json").loads(persisted["retrieval_json"])["query"], "shared memory query") self.assertTrue(duplicate["deduplicated"]) recall = self.store.recall("shared memory") self.assertEqual(recall["count"], 1) self.assertIn("shared memory answer", recall["context"]) artifact = self.store.artifact(completed["trace_id"]) self.assertIsNotNone(artifact) self.assertEqual(artifact["retrieval"]["query"], "shared memory query") def test_input_validation_rejects_empty_query_bad_limits_and_status(self) -> None: with self.assertRaisesRegex(ValueError, "query is required"): self.store.knowledge_recall(" ") with self.assertRaisesRegex(ValueError, "limit must be an integer"): self.store.knowledge_recall("query", limit="10") with self.assertRaisesRegex(ValueError, "between 1 and 50"): self.store.knowledge_recall("query", limit=0) session = self.store.open_session({"source": "fixture", "session_key": "validation"}) with self.assertRaisesRegex(ValueError, "namespace is required"): self.store.open_session({"source": "fixture", "session_key": "bad", "namespace": ""}) self.store.start_turn({"session_id": session["session_id"], "turn_id": "validation-turn", "query": "query"}) with self.assertRaisesRegex(ValueError, "status must be"): self.store.complete_turn("validation-turn", {"query": "query", "answer": "answer", "status": "running"}) def test_cancelled_turn_is_not_stored(self) -> None: session = self.store.open_session({"source": "fixture", "session_key": "session-2"}) self.store.start_turn({"session_id": session["session_id"], "turn_id": "turn-2", "query": "cancel me"}) result = self.store.complete_turn("turn-2", {"status": "cancelled"}) self.assertFalse(result["stored"]) self.assertEqual(self.store.recall("cancel me")["count"], 0) def test_unified_recall_includes_external_knowledge_records(self) -> None: original = self.store.catalog class FixtureCatalog: def search(self, query, limit=10, namespace="private"): return { "records": [{ "record_type": "knowledge_card", "id": "fixture-card", "title": "Fixture knowledge", "summary": "shared knowledge evidence", "score": 3, }] } self.store.catalog = FixtureCatalog() try: result = self.store.knowledge_recall("shared knowledge") finally: self.store.catalog = original self.assertEqual(result["count"], 1) self.assertEqual(result["records"][0]["record_type"], "knowledge_card") self.assertIn("Fixture knowledge", result["context"]) def test_catalog_fails_closed_across_namespaces_and_exposes_metadata(self) -> None: catalog = module.KnowledgeCatalog() root = Path(self.tmp.name) catalog.cards_dir = root / "cards" catalog.wiki_topics_dir = root / "topics" catalog.index_file = root / "index.json" catalog.cards_dir.mkdir() catalog.wiki_topics_dir.mkdir() (catalog.cards_dir / "secret.md").write_text("---\nnamespace: team-a\nsensitivity: internal\n---\nsecret", encoding="utf-8") (catalog.index_file).write_text('{"items":[{"id":"secret","title":"Secret"}]}', encoding="utf-8") self.assertIsNone(catalog.card("secret", "team-b")) visible = catalog.card("secret", "team-a") self.assertEqual(visible["namespace"], "team-a") self.assertEqual(catalog.search("secret", namespace="team-b")["count"], 0) def test_semantic_backend_is_used_and_declared(self) -> None: original = module.xbrain_query def fixture_query(query, *, limit, no_expand, semantic, diagnostics=None): self.assertEqual(query, "semantic fixture") self.assertTrue(semantic) return [{ "slug": "fixture-semantic-card", "title": "Semantic fixture", "chunk_text": "vector evidence", "source_url": "https://example.test/source", "type": "knowledge-card", "score": 0.91, }] module.xbrain_query = fixture_query try: # 前提要寫出來。原本這個測試能過,只是因為 fixture 的 slug 剛好 # 不在這台機器的 cards_index 裡——換一台機器、或哪天真的建了同名 # 的卡片,斷言就會翻面。測試不該從磁碟繼承它的前提。 import xkb_relevance with unittest.mock.patch.object(xkb_relevance, "similarities", return_value=None): result = self.store.catalog.search("semantic fixture", 3) finally: module.xbrain_query = original self.assertEqual(result["retrieval_mode"], "xbrain_hybrid") self.assertEqual(result["records"][0]["retrieval"], "xbrain_hybrid") record = result["records"][0] # 一個鍵都對不到向量索引時,這裡不做任何比較,也就不該假裝比過: # 後端的分數原樣帶著,只加上「沒驗證過」的標籤。 # 跨層可比是合併點(knowledge_recall → xkb_score.rank)的事, # 不是這裡編一個位置出來——我在這個檔案上來回改了三輪才把它放對地方。 self.assertEqual(record["score"], 0.91) self.assertEqual(record.get("score_basis"), "unverified") # 沒被驗證過的記錄有自己的尺度。這件事以前是靠在 xkb_relevance 裡 # 改寫分數做到的,那等於捏造一個測量值;現在 score 保持是後端自己 # 算的數字,排序交給 xkb_score 的尺度表。 self.assertEqual(record.get("score_scale"), "unverified") self.assertNotIn("_unverified", record) def test_layers_are_ordered_by_a_comparable_score_not_raw_numbers(self) -> None: """合併之後要照跨層可比的分數排,不是照各層自己的原始數字。 三層的尺度互不相同:卡片是 gbrain 的 RRF、wiki 是餘弦、對話是關鍵字 比例(上限 0.65)。合併點原本直接照 score 排,等於拿公分比英吋。 這是外部審查連續六輪繞著打轉的那個缺陷——每一輪我都在 xkb_relevance 裡替驗不出相似度的項目找「壓到哪裡才對」,而不管壓到哪裡都會撞上另一層 的尺度:壓高壓過 wiki 的真餘弦,壓低掉到對話軌跡之下,而後者會讓索引 壞掉長得像「知識庫裡沒東西」。真正該修的是合併點。 測試要走真正的路徑。我第一版直接呼叫 xkb_score.rank(),於是把合併點 改回原始排序它照樣是綠的——測到的是那個函式,不是那條路。 """ def packet_for(card_score: float) -> list[str]: knowledge = { "records": [ {"title": "卡片", "score": card_score, "score_scale": "card_semantic", "record_type": "knowledge_chunk"}, {"title": "wiki 結論", "score": 0.68, "score_scale": "wiki_semantic", "record_type": "wiki_topic"}, ], "retrieval_mode": "xbrain_hybrid", "filtered_counts": {}, "semantic_retrieval_attempted": True, "semantic_backend": {"status": "used"}, "dropped_as_irrelevant": 0, } conversation = {"memories": [{"query": "對話軌跡", "answer": "", "score": 0.65}]} with mock.patch.object(type(self.store.catalog), "search", return_value=knowledge), \ mock.patch.object(type(self.store), "recall", return_value=conversation): packet = self.store.knowledge_recall("任意問題", 5, "private") return [r.get("title") or r.get("query") for r in packet["records"]] strong = packet_for(0.75) self.assertEqual(strong[0], "wiki 結論", "消化過的結論該排在相近強度的原始素材之前") self.assertLess(strong.index("卡片"), strong.index("對話軌跡"), "語意命中的卡片該排在關鍵字命中的對話軌跡之前") # 對話軌跡仍然在封包裡——不是被系統性地丟掉。RRF 之下它就是另一條腿, # 照名次貢獻,不再被雙重折扣(呼叫端 0.65 折扣 + 尺度表未折扣錨點) # 壓到門檻之下、一填滿 limit 就整段消失。 marginal = packet_for(0.56) self.assertIn("對話軌跡", marginal, "對話層被整段擠出封包") # 截斷防護:知識層填滿 limit 時,最強的對話軌跡仍要進封包(不是被 # marginal 卡片擠到 records[:limit] 之外)。這是舊測試那條順序斷言真正 # 要守的東西——RRF 之下順序斷言不再成立,但「不被整段截掉」還是要守。 many_cards = { "records": [ {"title": f"卡片{n}", "score": 0.56, "score_scale": "card_semantic", "record_type": "knowledge_chunk"} for n in range(8) ], "retrieval_mode": "xbrain_hybrid", "filtered_counts": {}, "semantic_retrieval_attempted": True, "semantic_backend": {"status": "used"}, "dropped_as_irrelevant": 0, } conversation = {"memories": [{"query": "強對話軌跡", "answer": "", "score": 0.65}]} with mock.patch.object(type(self.store.catalog), "search", return_value=many_cards), mock.patch.object(type(self.store), "recall", return_value=conversation): packet = self.store.knowledge_recall("任意", 5, "private") titles = [r.get("title") or r.get("query") for r in packet["records"]] self.assertIn("強對話軌跡", titles, "知識層填滿 limit 後對話軌跡被整段截掉") # scale mixing 結構上不可能:關鍵字尺度的分數(6~20)跟餘弦尺度的 # 分數(0~1)在 RRF 裡都只是「腿內第 N 名」,不會因為原始數字大就碾壓。 import xkb_score mixed = xkb_score.rank([ {"title": "關鍵字命中的書籤", "score": 15.0, "score_scale": "card_keyword"}, {"title": "餘弦命中的卡片", "score": 0.62, "score_scale": "card_semantic"}, ]) self.assertLess(abs(mixed[0]["unified_score"] - mixed[1]["unified_score"]), 0.001, "15 分的關鍵字書籤把 0.62 的餘弦卡片碾壓了——scale mixing 回來了") def test_each_producer_declares_a_scale_xkb_score_knows(self) -> None: """算出分數的地方要標對尺度,而且是 xkb_score 認得的那些。 source_type 是「這是什麼東西」(會回給 API 用戶),score_scale 是 「這個數字怎麼來的」。標錯或沒標,整層就會用錯錨點對齊,而且不會有 任何錯誤訊息——不認得的尺度會靜靜掉到 0.5 的 fallback。 我第一版這個測試是在原始碼文字裡找 `score_scale` 這個子字串,於是 它對「值是錯的」完全無感——寫這個測試的當下就有兩個標籤是錯的, 它照樣是綠的。改成檢查實際產出的值。 """ import xkb_score known = set(xkb_score.DEFAULT_ANCHORS) & set(xkb_score.DEFAULT_WEIGHTS) def fixture_query(query, *, limit, no_expand, semantic, diagnostics=None): return [{"slug": "s", "title": "t", "chunk_text": "c", "source_url": "", "type": "knowledge-card", "score": 0.8}] original = module.xbrain_query module.xbrain_query = fixture_query try: packet = self.store.knowledge_recall("尺度測試", 5, "private") finally: module.xbrain_query = original scales = {r.get("score_scale") for r in packet["records"]} self.assertTrue(scales, "一筆記錄都沒有,測不到尺度") self.assertNotIn(None, scales, "有記錄沒有標尺度") for scale in scales: self.assertIn(scale, known, f"{scale} 不在 xkb_score 的尺度表裡,會靜靜掉到 fallback") def test_every_record_template_declares_its_specific_scale(self) -> None: """每一個產生分數的樣板都要標對尺度——包括這個測試檔案 mock 掉的那些。 動態的那個測試只走得到 fixture 那一筆:setUpModule 把 _wiki_search 整個 換成回空陣列、store 也是空的,所以 wiki 與對話那兩個樣板沒有任何測試 看著。我刪掉的靜態測試本來守著它們,替換的版本反而涵蓋更少。 而靜態檢查要看**值**。我更早的版本只找 score_scale 這個欄位名, 於是四個標籤裡有兩個是錯的時候它照樣是綠的。 """ source = (Path(module.__file__).parent / "xkb_memory_service.py").read_text( encoding="utf-8") expected = { '"retrieval": "wiki_semantic"': "wiki_semantic", '"record_type": "conversation_trace"': "conversation", } # GBrain declares its scale at the adapter boundary; the service must # preserve it. Runtime coverage is in test_recall_backend_contract. self.assertIn('"score_scale": hit.get("score_scale", "card")', source) for marker, scale in expected.items(): idx = source.index(marker) window = source[max(0, idx - 700):idx + 300] self.assertIn(f'"score_scale": "{scale}"', window, f"{marker} 的樣板沒有標成 {scale}") def test_the_keyword_fallback_declares_a_keyword_scale(self) -> None: """關鍵字退路的分數是 0~1 的覆蓋率,不是 RRF 也不是餘弦。 這條路徑上的記錄一度被標成 card / wiki_semantic——用餘弦與 RRF 的錨點 去對齊覆蓋率,會把真實的先後順序翻過來,正是收斂想消滅的那件事, 只是搬到了退路上。上一版的測試連這兩個樣板都沒看到。 """ source = (Path(module.__file__).parent / "xkb_memory_service.py").read_text( encoding="utf-8") for marker in ('"retrieval": "keyword"',): idx = 0 found = 0 while True: idx = source.find(marker, idx + 1) if idx < 0: break found += 1 template = source[max(0, idx - 700):idx] self.assertRegex( template, r'"score_scale": "(card_keyword|wiki_keyword)"', "關鍵字退路的記錄要標關鍵字尺度") self.assertGreater(found, 0, "找不到關鍵字退路的記錄樣板") def test_recall_packet_explains_acl_namespace_filtering_and_backend(self) -> None: original = module.xbrain_query def fixture_query(query, *, limit, no_expand, semantic, diagnostics=None): return [ {"slug": "allowed", "title": "Allowed", "chunk_text": "visible", "namespace": "team-a"}, {"slug": "hidden", "title": "Hidden", "chunk_text": "secret", "namespace": "team-b"}, ] module.xbrain_query = fixture_query try: result = self.store.knowledge_recall("acl packet", namespace="team-a") finally: module.xbrain_query = original self.assertEqual(result["request_namespace"], "team-a") self.assertEqual(result["acl_policy"]["decision"], "allow_public_or_matching_namespace") self.assertEqual(result["filtered_counts"]["semantic"], 1) self.assertIn("records_filtered_by_acl", result["warnings"]) self.assertEqual(result["semantic_backend"]["status"], "used") # The breakdown must say which layer was filtered, otherwise "nothing # came back" and "the wiki layer was filtered out" look identical. self.assertEqual(result["filtered_counts"]["total"], 1) self.assertEqual(result["filtered_counts"]["by_layer"]["semantic"], 1) self.assertEqual(result["filtered_counts"]["by_layer"]["conversation"], 0) def test_acl_filter_counts_are_attributed_per_layer(self) -> None: """A quiet result must say which layer the ACL removed.""" module = importlib.import_module("xkb_memory_service") original = module.xbrain_query module.xbrain_query = None # force the keyword path, where layers are known try: result = self.store.knowledge_recall("anything", namespace="team-a") finally: module.xbrain_query = original counts = result["filtered_counts"] self.assertEqual(result["retrieval_mode"], "keyword_fallback") self.assertEqual(set(counts["by_layer"]), {"card", "wiki", "semantic", "conversation"}) self.assertEqual(counts["total"], sum(counts["by_layer"].values())) # No semantic backend ran, so nothing can be attributed to it. self.assertEqual(counts["by_layer"]["semantic"], 0) # Legacy channel keys stay consistent with the layer breakdown. self.assertEqual(counts["keyword"], counts["by_layer"]["card"] + counts["by_layer"]["wiki"]) def test_turn_start_persists_retrieval_packet_and_source_ids(self) -> None: original = self.store.knowledge_recall packet = { "schema": "xkb-knowledge-service.v1", "query": "turn query", "records": [{"id": "knowledge-1", "record_type": "knowledge_chunk", "score": 0.8}], "context": "historical evidence", "retrieval_mode": "xbrain_hybrid", "semantic_retrieval_attempted": True, "warnings": [], } self.store.knowledge_recall = lambda query, limit=10, namespace="private", conversation=None, **_: packet try: session = self.store.open_session({"source": "fixture", "session_key": "retrieval-session"}) started = self.store.start_turn({"session_id": session["session_id"], "turn_id": "retrieval-turn", "query": "turn query"}) resumed = self.store.start_turn({"session_id": session["session_id"], "turn_id": "retrieval-turn", "query": "turn query"}) finally: self.store.knowledge_recall = original self.assertEqual(started["source_memory_ids"], ["knowledge-1"]) self.assertEqual(started["retrieval"]["retrieval_mode"], "xbrain_hybrid") self.assertTrue(resumed["resumed"]) self.assertEqual(resumed["retrieval"]["query"], "turn query") def test_turn_start_rejects_idempotency_conflicts(self) -> None: first = self.store.open_session({"source": "fixture", "session_key": "identity-1", "namespace": "private"}) other = self.store.open_session({"source": "fixture", "session_key": "identity-2", "namespace": "private"}) self.store.start_turn({"session_id": first["session_id"], "turn_id": "identity-turn", "query": "original"}) with self.assertRaisesRegex(ValueError, "session_id"): self.store.start_turn({"session_id": other["session_id"], "turn_id": "identity-turn", "query": "original"}) with self.assertRaisesRegex(ValueError, "query"): self.store.start_turn({"session_id": first["session_id"], "turn_id": "identity-turn", "query": "changed"}) with self.assertRaisesRegex(ValueError, "namespace"): self.store.start_turn({"session_id": first["session_id"], "turn_id": "identity-turn", "query": "original", "namespace": "shared"}) def test_recall_is_scoped_to_session_namespace_and_start_requires_matching_namespace(self) -> None: private = self.store.open_session({"source": "fixture", "session_key": "private", "namespace": "private"}) shared = self.store.open_session({"source": "fixture", "session_key": "shared", "namespace": "shared"}) self.store.start_turn({"session_id": private["session_id"], "turn_id": "private-turn", "query": "same namespace secret"}) self.store.complete_turn("private-turn", {"query": "same namespace secret", "answer": "private answer"}) self.store.start_turn({"session_id": shared["session_id"], "turn_id": "shared-turn", "query": "same namespace secret"}) self.store.complete_turn("shared-turn", {"query": "same namespace secret", "answer": "shared answer"}) self.assertEqual([m["answer"] for m in self.store.recall("same namespace", namespace="private")["memories"]], ["private answer"]) self.assertEqual([m["answer"] for m in self.store.recall("same namespace", namespace="shared")["memories"]], ["shared answer"]) with self.assertRaisesRegex(ValueError, "namespace conflicts with session"): self.store.start_turn({"session_id": private["session_id"], "turn_id": "bad-namespace", "query": "q", "namespace": "shared"}) def test_turn_start_rejects_unknown_session(self) -> None: with self.assertRaisesRegex(ValueError, "unknown session_id"): self.store.start_turn({"session_id": "missing", "turn_id": "missing-turn", "query": "q"}) def test_pipeline_snapshot_is_read_only_and_lists_known_stages(self) -> None: snapshot = self.store.catalog.pipeline_snapshot(days=7) self.assertTrue(snapshot["read_only"]) self.assertEqual(snapshot["control_plane"], "observed_status_only") stages = {item["stage"] for item in snapshot["stages"]} self.assertEqual( stages, {"ingest", "card_generation", "index", "distill", "promotion", "publish"}, ) self.assertTrue(all(item["control"] == "read_only" for item in snapshot["stages"])) self.assertIn("summary", snapshot) def test_job_event_is_persisted_and_filterable_without_running_worker(self) -> None: created = self.store.record_job_event({ "job_id": "job-1", "stage": "index", "worker": "build_vector_index.py", "status": "running", "input_ref": "cards:batch-1", "metadata": {"count": 3}, }) self.assertTrue(created["stored"]) self.assertTrue(created["observed_only"]) completed = self.store.record_job_event({ "job_id": "job-1", "stage": "index", "worker": "build_vector_index.py", "status": "succeeded", "output_ref": "vector-index:batch-1", }) self.assertEqual(completed["status"], "succeeded") result = self.store.list_jobs(stage="index", status="succeeded") self.assertEqual(result["count"], 1) self.assertEqual(result["jobs"][0]["output_ref"], "vector-index:batch-1") self.assertFalse(self.store.list_jobs(stage="distill")["count"]) class RelevanceAndIntentTests(unittest.TestCase): """Injecting ten results into every turn is what makes an unrelated question cost as much as a real one.""" def setUp(self) -> None: self.module = module def test_acknowledgements_and_greetings_skip_retrieval_entirely(self) -> None: skip = self.module.Store._skip_reason self.assertEqual(skip("好"), "acknowledgement") self.assertEqual(skip("謝謝"), "acknowledgement") self.assertEqual(skip("早安"), "greeting") self.assertEqual(skip("hi"), "greeting") def test_real_questions_are_never_skipped(self) -> None: """Eight Chinese characters is a question, not chitchat.""" skip = self.module.Store._skip_reason self.assertEqual(skip("碳盤查的計算方式"), "") self.assertEqual(skip("XKB 召回"), "") self.assertEqual(skip("我們之前怎麼處理碳盤查的"), "") def test_vector_key_maps_slug_onto_index_key(self) -> None: import xkb_relevance self.assertEqual(xkb_relevance.vector_key("01-topic/12345"), "01-topic/12345.md") self.assertEqual(xkb_relevance.vector_key("01-topic/12345.md"), "01-topic/12345.md") self.assertEqual(xkb_relevance.vector_key("https://x.com/i/status/1"), "") self.assertEqual(xkb_relevance.vector_key(""), "") def test_rank_score_is_replaced_by_measured_similarity(self) -> None: """The backend's score says "ranked first", not "is relevant".""" catalog = self.module.KnowledgeCatalog() records = [{"id": "a/1", "score": 0.88}, {"id": "a/2", "score": 0.87}] with mock.patch("xkb_relevance.similarities", return_value={"a/1.md": 0.72, "a/2.md": 0.30}): kept, dropped = catalog._drop_irrelevant("q", records) self.assertEqual(dropped, 1) self.assertEqual([item["id"] for item in kept], ["a/1"]) self.assertEqual(kept[0]["score"], 0.72) self.assertEqual(kept[0]["rank_score"], 0.88) def test_unavailable_similarity_passes_records_through(self) -> None: """A missing index must not silently delete every result.""" catalog = self.module.KnowledgeCatalog() records = [{"id": "a/1", "score": 0.88}] with mock.patch("xkb_relevance.similarities", return_value=None): kept, dropped = catalog._drop_irrelevant("q", records) self.assertEqual((len(kept), dropped), (1, 0)) def test_warnings_separate_broken_backend_from_nothing_relevant(self) -> None: broken = self.module._recall_warnings( {"retrieval_mode": "keyword_fallback", "dropped_as_irrelevant": 0}, {}) self.assertIn("semantic_backend_unavailable_or_empty", broken[0]) nothing_relevant = self.module._recall_warnings( {"retrieval_mode": "keyword_fallback", "dropped_as_irrelevant": 10}, {}) self.assertIn("relevance floor", nothing_relevant[0]) self.assertNotIn("unavailable", nothing_relevant[0]) class KnowledgeRetirementTests(unittest.TestCase): """Wiki and cards only ever grew; nothing ever aged out.""" def setUp(self) -> None: self.tmp = tempfile.TemporaryDirectory() self.store = Store(Path(self.tmp.name) / "memory.sqlite") self.addCleanup(self.tmp.cleanup) def test_records_that_never_clear_the_floor_are_reported_as_cold(self) -> None: for _ in range(6): self.store.record_usage([("cold/1", 0.31, False), ("warm/1", 0.72, True)]) report = self.store.cold_knowledge(min_considered=5) self.assertEqual([r["record_id"] for r in report["records"]], ["cold/1"]) self.assertEqual(report["tracked_records"], 2) self.assertEqual(report["ever_useful"], 1) def test_useful_records_are_never_listed_however_often_retrieved(self) -> None: for _ in range(20): self.store.record_usage([("warm/1", 0.40, False)]) self.store.record_usage([("warm/1", 0.80, True)]) report = self.store.cold_knowledge(min_considered=5) self.assertEqual(report["records"], []) def test_counters_accumulate_and_keep_the_best_similarity(self) -> None: self.store.record_usage([("a/1", 0.30, False)]) self.store.record_usage([("a/1", 0.51, False)]) row = self.store.cold_knowledge(min_considered=1)["records"][0] self.assertEqual(row["considered_count"], 2) self.assertEqual(row["injected_count"], 0) self.assertAlmostEqual(row["best_similarity"], 0.51, places=4) self.assertIsNone(row["last_injected_at"]) def test_retirement_is_advisory_only(self) -> None: """Provenance is the point; nothing may be deleted automatically.""" self.store.record_usage([("cold/1", 0.10, False)] * 1) report = self.store.cold_knowledge(min_considered=1) self.assertFalse(report["automatic_retirement"]) self.assertTrue(report["read_only"]) def test_usage_accounting_never_breaks_recall(self) -> None: catalog = module.KnowledgeCatalog() catalog.usage_sink = lambda observations: (_ for _ in ()).throw(RuntimeError("db down")) with mock.patch("xkb_relevance.similarities", return_value={"a/1.md": 0.9}): kept, dropped = catalog._drop_irrelevant("q", [{"id": "a/1", "score": 0.88}]) self.assertEqual((len(kept), dropped), (1, 0)) class QueryEmbeddingCacheTests(unittest.TestCase): """The same query is embedded twice per recall: once by hybrid search, once by the relevance filter. The second call is a wasted round trip.""" def test_repeated_query_is_embedded_once(self) -> None: import continuity_recall as cr cr._QUERY_VECTORS.clear() calls = [] def fake(query): calls.append(query) return [0.1, 0.2] with mock.patch.object(cr, "_embed_query_uncached", side_effect=fake): first = cr._embed_query("same question") second = cr._embed_query("same question") cr._embed_query("different question") self.assertEqual(first, second) self.assertEqual(calls, ["same question", "different question"]) cr._QUERY_VECTORS.clear() def test_failure_is_cached_too_so_a_dead_provider_is_not_retried(self) -> None: import continuity_recall as cr cr._QUERY_VECTORS.clear() calls = [] with mock.patch.object(cr, "_embed_query_uncached", side_effect=lambda q: calls.append(q) or None): self.assertIsNone(cr._embed_query("q")) self.assertIsNone(cr._embed_query("q")) self.assertEqual(len(calls), 1) cr._QUERY_VECTORS.clear() def test_cache_is_bounded(self) -> None: import continuity_recall as cr cr._QUERY_VECTORS.clear() with mock.patch.object(cr, "_embed_query_uncached", return_value=[0.0]): for i in range(cr._QUERY_VECTOR_LIMIT + 5): cr._embed_query(f"q{i}") self.assertLessEqual(len(cr._QUERY_VECTORS), cr._QUERY_VECTOR_LIMIT) cr._QUERY_VECTORS.clear() if __name__ == "__main__": unittest.main()