fix: restore main runtime sources and cache consistency
This commit is contained in:
197
tests/test_shared_regressions.py
Normal file
197
tests/test_shared_regressions.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""Regression tests for branch-shared RAG behavior."""
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from core.cache import RAGCacheManager
|
||||
from core.intent_analyzer import IntentAnalyzer
|
||||
from exam_pkg.generator import QuestionGenerator, analyze_document_for_exam
|
||||
from exam_pkg.grader import grade_fill_blank
|
||||
from knowledge.sync import ChangeType, DocumentChange, KnowledgeSyncService
|
||||
|
||||
|
||||
class CacheInvalidationTests(unittest.TestCase):
|
||||
def test_kb_version_change_clears_rerank_scores(self):
|
||||
cache = RAGCacheManager()
|
||||
cache.set_rerank_scores("问题", ["doc-1"], [0.91])
|
||||
self.assertEqual({"doc-1": 0.91}, cache.get_rerank_scores("问题", ["doc-1"]))
|
||||
|
||||
cache.increment_kb_version("public")
|
||||
|
||||
self.assertIsNone(cache.get_rerank_scores("问题", ["doc-1"]))
|
||||
|
||||
|
||||
class IntentCacheTests(unittest.TestCase):
|
||||
def test_unknown_semantic_cache_type_is_not_used_as_intent(self):
|
||||
class FakeEmbeddingModel:
|
||||
def encode(self, _text):
|
||||
return [1.0, 0.0]
|
||||
|
||||
class FakeCache:
|
||||
def __init__(self):
|
||||
self.saved = None
|
||||
|
||||
def get(self, _embedding):
|
||||
return {
|
||||
"cache_type": "future_cache_type",
|
||||
"rewritten_query": "错误的缓存结果",
|
||||
}
|
||||
|
||||
def set(self, _embedding, value):
|
||||
self.saved = value
|
||||
|
||||
def get_stats(self):
|
||||
return {}
|
||||
|
||||
analyzer = IntentAnalyzer(model="mimo-v2.5")
|
||||
analyzer._embedding_model = FakeEmbeddingModel()
|
||||
analyzer._cache = FakeCache()
|
||||
analyzer._client = object()
|
||||
|
||||
llm_result = json.dumps(
|
||||
{
|
||||
"rewritten_query": "来自 LLM 的新结果",
|
||||
"use_context": False,
|
||||
"need_retrieval": True,
|
||||
"reason": "新问题",
|
||||
"sub_queries": ["来自 LLM 的新结果"],
|
||||
"intent": "factual",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
with patch("core.llm_utils.call_llm", return_value=llm_result):
|
||||
result = analyzer.analyze("当前问题", [])
|
||||
|
||||
self.assertEqual("来自 LLM 的新结果", result.rewritten_query)
|
||||
self.assertEqual("intent_analysis", analyzer._cache.saved["cache_type"])
|
||||
|
||||
|
||||
class ExamRegressionTests(unittest.TestCase):
|
||||
def test_analysis_prompt_respects_requested_max_total(self):
|
||||
captured = {}
|
||||
|
||||
def fake_call(_self, prompt):
|
||||
captured["prompt"] = prompt
|
||||
return json.dumps(
|
||||
{
|
||||
"total_knowledge_points": 2,
|
||||
"suitable_types": ["single_choice"],
|
||||
"question_types": {
|
||||
"single_choice": 3,
|
||||
"multiple_choice": 0,
|
||||
"true_false": 0,
|
||||
"fill_blank": 0,
|
||||
"subjective": 0,
|
||||
},
|
||||
"reason": "测试",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
chunks = [{"content": "知识点" * 80, "section": "第一章"}]
|
||||
with patch.object(QuestionGenerator, "_call_llm", fake_call):
|
||||
result = analyze_document_for_exam(chunks, max_total=3)
|
||||
|
||||
self.assertIn("所有数量之和不要超过 3", captured["prompt"])
|
||||
self.assertLessEqual(sum(result["question_types"].values()), 3)
|
||||
|
||||
def test_fill_blank_rejects_non_list_student_answer(self):
|
||||
result = grade_fill_blank(
|
||||
{
|
||||
"question_id": "q1",
|
||||
"content": {"answer": [["正确答案"]], "data": {"blank_count": 1}},
|
||||
"student_answer": "正确答案",
|
||||
"max_score": 2,
|
||||
}
|
||||
)
|
||||
self.assertEqual("failed", result["grading_status"])
|
||||
self.assertEqual(0, result["score"])
|
||||
|
||||
def test_fill_blank_rejects_non_string_items(self):
|
||||
result = grade_fill_blank(
|
||||
{
|
||||
"question_id": "q1",
|
||||
"content": {"answer": [["正确答案"]], "data": {"blank_count": 1}},
|
||||
"student_answer": [{"text": "正确答案"}],
|
||||
"max_score": 2,
|
||||
}
|
||||
)
|
||||
self.assertEqual("failed", result["grading_status"])
|
||||
self.assertIn("第 1 项", result["details"]["error"])
|
||||
|
||||
|
||||
class SyncInvalidationTests(unittest.TestCase):
|
||||
def test_document_delete_clears_semantic_cache(self):
|
||||
class FakeDb:
|
||||
def delete_document_hash(self, _document_id):
|
||||
return None
|
||||
|
||||
class FakeKbManager:
|
||||
def delete_document(self, _kb_name, _document_name):
|
||||
return 1
|
||||
|
||||
class FakeSemanticCache:
|
||||
def __init__(self):
|
||||
self.cleared = False
|
||||
|
||||
def clear(self):
|
||||
self.cleared = True
|
||||
|
||||
service = object.__new__(KnowledgeSyncService)
|
||||
service.documents_path = "unused"
|
||||
service.db = FakeDb()
|
||||
semantic_cache = FakeSemanticCache()
|
||||
change = DocumentChange(
|
||||
document_id="public/example.pdf",
|
||||
document_name="example.pdf",
|
||||
change_type=ChangeType.DELETED,
|
||||
old_hash="old",
|
||||
new_hash=None,
|
||||
change_time=datetime.now(),
|
||||
)
|
||||
|
||||
with (
|
||||
patch("knowledge.manager.get_kb_manager", return_value=FakeKbManager()),
|
||||
patch("knowledge.sync.CACHE_AVAILABLE", False),
|
||||
patch("core.semantic_cache.get_semantic_cache", return_value=semantic_cache),
|
||||
):
|
||||
self.assertTrue(service.process_change(change))
|
||||
|
||||
self.assertTrue(semantic_cache.cleared)
|
||||
|
||||
|
||||
class ConfigTemplateTests(unittest.TestCase):
|
||||
def test_config_example_defines_all_direct_imports(self):
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
template_tree = ast.parse((root / "config.example.py").read_text(encoding="utf-8"))
|
||||
defined = {
|
||||
node.name
|
||||
for node in template_tree.body
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
}
|
||||
for node in template_tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
defined.update(target.id for target in node.targets if isinstance(target, ast.Name))
|
||||
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
||||
defined.add(node.target.id)
|
||||
|
||||
imported = set()
|
||||
excluded = {"venv", ".git", ".data", ".recovery", "__pycache__"}
|
||||
for path in root.rglob("*.py"):
|
||||
if any(part in excluded for part in path.parts):
|
||||
continue
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module == "config":
|
||||
imported.update(alias.name for alias in node.names)
|
||||
|
||||
self.assertEqual([], sorted(imported - defined))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user