migration: preserve mentor and llm streaming slice
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import hashlib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import llm_stream
|
||||
import mentor_agent
|
||||
from backend.features.mentor import agent as canonical_agent
|
||||
from backend.llm import stream as canonical_stream
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
ORIGINAL_ROOT = APP_ROOT.parent
|
||||
|
||||
MENTOR_SERVICE_METHODS = {
|
||||
"mentor_setup",
|
||||
"save_mentor_preferences",
|
||||
"mentor_stream",
|
||||
"mentor_messages",
|
||||
"clear_mentor_messages",
|
||||
"_validate_mentor_history",
|
||||
"_build_mentor_context",
|
||||
"_mentor_market_matrix",
|
||||
}
|
||||
|
||||
LLM_SERVICE_METHODS = {
|
||||
"_personal_llm_profile",
|
||||
"_platform_llm_profile",
|
||||
"_profile_configured",
|
||||
"_resolved_llm_profile",
|
||||
"llm_primary_api_key",
|
||||
"llm_primary_base_url",
|
||||
"llm_primary_model",
|
||||
"llm_fallback_api_key",
|
||||
"llm_fallback_base_url",
|
||||
"llm_fallback_model",
|
||||
"llm_source",
|
||||
"llm_configured",
|
||||
"llm_fallback_configured",
|
||||
"save_llm_settings",
|
||||
"save_llm_mode",
|
||||
"test_llm_profile",
|
||||
"_validate_llm_profile",
|
||||
"llm_access_status",
|
||||
"_platform_usage_today",
|
||||
"_platform_usage_today_for_user",
|
||||
"test_system_llm_profile",
|
||||
}
|
||||
|
||||
MENTOR_REPOSITORY_METHODS = {
|
||||
"save_mentor_exchange",
|
||||
"list_mentor_messages",
|
||||
"delete_mentor_messages",
|
||||
"list_mentor_preferences",
|
||||
"save_mentor_preferences",
|
||||
}
|
||||
|
||||
LLM_REPOSITORY_METHODS = {"record_llm_usage", "count_llm_usage_since"}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def class_methods(path: Path, class_name: str) -> dict[str, str]:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
owner = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.ClassDef) and node.name == class_name
|
||||
)
|
||||
return {
|
||||
node.name: ast.dump(node, include_attributes=False)
|
||||
for node in owner.body
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
}
|
||||
|
||||
|
||||
def assignments(path: Path, names: set[str]) -> dict[str, str]:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
result = {}
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
|
||||
continue
|
||||
target = node.targets[0]
|
||||
if isinstance(target, ast.Name) and target.id in names:
|
||||
result[target.id] = ast.dump(node.value, include_attributes=False)
|
||||
return result
|
||||
|
||||
|
||||
class MentorLLMSliceSourceEquivalenceTests(unittest.TestCase):
|
||||
def assert_methods_equal(
|
||||
self,
|
||||
original_path: Path,
|
||||
original_class: str,
|
||||
migrated_path: Path,
|
||||
migrated_class: str,
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
original = class_methods(original_path, original_class)
|
||||
migrated = class_methods(migrated_path, migrated_class)
|
||||
self.assertEqual(set(migrated), expected)
|
||||
for name in sorted(expected):
|
||||
self.assertEqual(migrated[name], original[name], name)
|
||||
|
||||
def test_mentor_agent_and_stream_accumulator_are_exact_files(self) -> None:
|
||||
self.assertEqual(
|
||||
sha256(ORIGINAL_ROOT / "mentor_agent.py"),
|
||||
sha256(APP_ROOT / "backend" / "features" / "mentor" / "agent.py"),
|
||||
)
|
||||
self.assertEqual(
|
||||
sha256(ORIGINAL_ROOT / "llm_stream.py"),
|
||||
sha256(APP_ROOT / "backend" / "llm" / "stream.py"),
|
||||
)
|
||||
|
||||
def test_compatibility_modules_are_canonical_module_objects(self) -> None:
|
||||
self.assertIs(mentor_agent, canonical_agent)
|
||||
self.assertIs(llm_stream, canonical_stream)
|
||||
|
||||
def test_mentor_service_methods_are_exact_original_ast(self) -> None:
|
||||
self.assert_methods_equal(
|
||||
ORIGINAL_ROOT / "server.py",
|
||||
"DashboardService",
|
||||
APP_ROOT / "backend" / "features" / "mentor" / "service.py",
|
||||
"MentorServiceMixin",
|
||||
MENTOR_SERVICE_METHODS,
|
||||
)
|
||||
|
||||
def test_llm_service_methods_are_exact_original_ast(self) -> None:
|
||||
original = class_methods(ORIGINAL_ROOT / "server.py", "DashboardService")
|
||||
migrated = class_methods(
|
||||
APP_ROOT / "backend" / "llm" / "service.py", "LLMServiceMixin"
|
||||
)
|
||||
self.assertEqual(set(migrated), LLM_SERVICE_METHODS)
|
||||
adapted = {"_platform_usage_today", "_platform_usage_today_for_user"}
|
||||
for name in sorted(LLM_SERVICE_METHODS - adapted):
|
||||
self.assertEqual(migrated[name], original[name], name)
|
||||
source = (APP_ROOT / "backend" / "llm" / "service.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn(
|
||||
"return self._platform_usage_today_for_user(self.current_user_id)", source
|
||||
)
|
||||
self.assertIn("def _platform_usage_today_for_user(self, user_id: int)", source)
|
||||
|
||||
def test_mentor_and_llm_repositories_are_exact_original_ast(self) -> None:
|
||||
self.assert_methods_equal(
|
||||
ORIGINAL_ROOT / "database.py",
|
||||
"ReviewDatabase",
|
||||
APP_ROOT / "backend" / "features" / "mentor" / "repository.py",
|
||||
"MentorRepositoryMixin",
|
||||
MENTOR_REPOSITORY_METHODS,
|
||||
)
|
||||
self.assert_methods_equal(
|
||||
ORIGINAL_ROOT / "database.py",
|
||||
"ReviewDatabase",
|
||||
APP_ROOT / "backend" / "llm" / "repository.py",
|
||||
"LLMAuditRepositoryMixin",
|
||||
LLM_REPOSITORY_METHODS,
|
||||
)
|
||||
|
||||
def test_mentor_data_profiles_are_exact_original_values(self) -> None:
|
||||
names = {"MENTOR_DATA_PROFILES", "MENTOR_INDEX_UNIVERSE", "MENTOR_ETF_UNIVERSE"}
|
||||
self.assertEqual(
|
||||
assignments(ORIGINAL_ROOT / "server.py", names),
|
||||
assignments(
|
||||
APP_ROOT / "backend" / "features" / "mentor" / "service.py",
|
||||
names,
|
||||
),
|
||||
)
|
||||
|
||||
def test_original_classes_no_longer_duplicate_moved_methods(self) -> None:
|
||||
remaining_service = class_methods(
|
||||
APP_ROOT / "backend" / "application.py", "DashboardService"
|
||||
)
|
||||
remaining_database = class_methods(APP_ROOT / "database.py", "ReviewDatabase")
|
||||
remaining_http = class_methods(
|
||||
APP_ROOT / "backend" / "application.py", "RequestHandler"
|
||||
)
|
||||
self.assertTrue(MENTOR_SERVICE_METHODS.isdisjoint(remaining_service))
|
||||
self.assertTrue(LLM_SERVICE_METHODS.isdisjoint(remaining_service))
|
||||
self.assertTrue(MENTOR_REPOSITORY_METHODS.isdisjoint(remaining_database))
|
||||
self.assertTrue(LLM_REPOSITORY_METHODS.isdisjoint(remaining_database))
|
||||
self.assertTrue(
|
||||
{"stream_mentor_chat", "save_llm_settings", "save_llm_mode", "test_llm_settings"}
|
||||
.isdisjoint(remaining_http)
|
||||
)
|
||||
|
||||
def test_http_mixins_preserve_stream_and_model_endpoints(self) -> None:
|
||||
mentor_http = class_methods(
|
||||
APP_ROOT / "backend" / "features" / "mentor" / "http.py",
|
||||
"MentorHttpMixin",
|
||||
)
|
||||
llm_http = class_methods(APP_ROOT / "backend" / "llm" / "http.py", "LLMHttpMixin")
|
||||
self.assertEqual(set(mentor_http), {"stream_mentor_chat"})
|
||||
self.assertEqual(
|
||||
set(llm_http), {"save_llm_settings", "save_llm_mode", "test_llm_settings"}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user