test_knowledge_dynamic_update.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. from __future__ import annotations
  2. import os
  3. import uuid
  4. import pytest
  5. from sqlalchemy import create_engine, text
  6. from sqlalchemy.orm import Session
  7. from app.core.knowledge.point_builder import build_knowledge_snapshot
  8. pytestmark = pytest.mark.integration
  9. class FixedEmbeddingProvider:
  10. profile_key = "qwen:test:1024"
  11. def __init__(self):
  12. self.calls: list[str] = []
  13. def embed(self, texts: list[str]) -> list[list[float]]:
  14. self.calls.extend(texts)
  15. return [[float(index + 1)] * 1024 for index, _text in enumerate(texts)]
  16. @pytest.fixture()
  17. def database_url():
  18. value = os.environ.get("TEST_DATABASE_URL")
  19. if not value:
  20. pytest.skip("TEST_DATABASE_URL is not configured")
  21. return value
  22. def _snapshot(source_uid: str, domain_uid: str, version: int, owner: str):
  23. return build_knowledge_snapshot(
  24. "DataFlow",
  25. {
  26. "uid": source_uid,
  27. "version": version,
  28. "name": "customer_sync",
  29. "purpose": "sync customer data",
  30. "owner": owner,
  31. "business_domain_uid": domain_uid,
  32. },
  33. )
  34. def test_sql_repository_atomically_switches_active_points_and_reuses_embeddings(
  35. database_url,
  36. ):
  37. from app.core.knowledge.repository import SqlKnowledgeRepository
  38. from app.core.knowledge.sync import KnowledgeSyncService
  39. engine = create_engine(database_url)
  40. source_uid = str(uuid.uuid4())
  41. domain_uid = str(uuid.uuid4())
  42. profile_id = str(uuid.uuid4())
  43. try:
  44. with engine.begin() as connection:
  45. connection.execute(
  46. text(
  47. """
  48. INSERT INTO public.knowledge_embedding_profiles (
  49. id, provider, model, dimension, distance, status, config_hash,
  50. activated_at
  51. ) VALUES (
  52. CAST(:id AS uuid), 'qwen', 'test', 1024, 'cosine',
  53. 'active', :config_hash, CURRENT_TIMESTAMP
  54. )
  55. """
  56. ),
  57. {"id": profile_id, "config_hash": "a" * 64},
  58. )
  59. first_embedder = FixedEmbeddingProvider()
  60. with Session(engine) as session, session.begin():
  61. repository = SqlKnowledgeRepository(
  62. session,
  63. embedding_profile_id=profile_id,
  64. embedding_profile_key=first_embedder.profile_key,
  65. generation=1,
  66. workspace=f"dataops-global-{domain_uid}-g1",
  67. )
  68. first = KnowledgeSyncService(
  69. repository=repository, embedder=first_embedder
  70. ).sync(_snapshot(source_uid, domain_uid, 1, "team-a"))
  71. assert first.embedded_chunk_count == 3
  72. second_embedder = FixedEmbeddingProvider()
  73. with Session(engine) as session, session.begin():
  74. repository = SqlKnowledgeRepository(
  75. session,
  76. embedding_profile_id=profile_id,
  77. embedding_profile_key=second_embedder.profile_key,
  78. generation=1,
  79. workspace=f"dataops-global-{domain_uid}-g1",
  80. )
  81. second = KnowledgeSyncService(
  82. repository=repository, embedder=second_embedder
  83. ).sync(_snapshot(source_uid, domain_uid, 2, "team-b"))
  84. assert second.diff_counts == {
  85. "added": 0,
  86. "modified": 1,
  87. "deleted": 0,
  88. "unchanged": 2,
  89. }
  90. assert second_embedder.calls == ["team-b"]
  91. with engine.connect() as connection:
  92. active_document = connection.execute(
  93. text(
  94. "SELECT object_version, point_set_hash FROM public.governance_documents "
  95. "WHERE object_uid = CAST(:uid AS uuid) AND status = 'active'"
  96. ),
  97. {"uid": source_uid},
  98. ).one()
  99. active_points = connection.execute(
  100. text(
  101. "SELECT semantic_path, content FROM public.knowledge_points "
  102. "WHERE source_uid = CAST(:uid AS uuid) AND status = 'active' "
  103. "ORDER BY semantic_path"
  104. ),
  105. {"uid": source_uid},
  106. ).all()
  107. change_sets = connection.execute(
  108. text(
  109. "SELECT id::text, status FROM public.knowledge_change_sets "
  110. "WHERE source_uid = CAST(:uid AS uuid) ORDER BY source_revision"
  111. ),
  112. {"uid": source_uid},
  113. ).all()
  114. assert active_document[0] == 2
  115. assert active_document[1]
  116. assert dict(active_points)["owner"] == "team-b"
  117. assert [row[1] for row in change_sets] == [
  118. "canonical_active",
  119. "canonical_active",
  120. ]
  121. from app.core.knowledge.admin import rollback_change_set
  122. with Session(engine) as session, session.begin():
  123. assert rollback_change_set(session, change_sets[1][0]) is True
  124. with engine.connect() as connection:
  125. rolled_back_document = connection.execute(
  126. text(
  127. "SELECT object_version FROM public.governance_documents "
  128. "WHERE object_uid = CAST(:uid AS uuid) AND status = 'active'"
  129. ),
  130. {"uid": source_uid},
  131. ).scalar_one()
  132. rolled_back_owner = connection.execute(
  133. text(
  134. "SELECT content FROM public.knowledge_points "
  135. "WHERE point_key = :point_key AND status = 'active'"
  136. ),
  137. {"point_key": f"DataFlow/{source_uid}/owner"},
  138. ).scalar_one()
  139. assert rolled_back_document == 1
  140. assert rolled_back_owner == "team-a"
  141. finally:
  142. with engine.begin() as connection:
  143. connection.execute(
  144. text(
  145. "DELETE FROM public.knowledge_index_projections "
  146. "WHERE document_id IN (SELECT id FROM public.governance_documents "
  147. "WHERE object_uid = CAST(:uid AS uuid))"
  148. ),
  149. {"uid": source_uid},
  150. )
  151. connection.execute(
  152. text(
  153. "DELETE FROM public.knowledge_chunk_embeddings "
  154. "WHERE chunk_id IN (SELECT c.id FROM public.governance_chunks c "
  155. "JOIN public.governance_documents d ON d.id = c.document_id "
  156. "WHERE d.object_uid = CAST(:uid AS uuid))"
  157. ),
  158. {"uid": source_uid},
  159. )
  160. connection.execute(
  161. text(
  162. "DELETE FROM public.governance_documents "
  163. "WHERE object_uid = CAST(:uid AS uuid)"
  164. ),
  165. {"uid": source_uid},
  166. )
  167. connection.execute(
  168. text(
  169. "DELETE FROM public.knowledge_point_dependencies "
  170. "WHERE from_point_key LIKE :prefix"
  171. ),
  172. {"prefix": f"DataFlow/{source_uid}/%"},
  173. )
  174. connection.execute(
  175. text(
  176. "DELETE FROM public.knowledge_points "
  177. "WHERE source_uid = CAST(:uid AS uuid)"
  178. ),
  179. {"uid": source_uid},
  180. )
  181. connection.execute(
  182. text(
  183. "DELETE FROM public.knowledge_change_sets "
  184. "WHERE source_uid = CAST(:uid AS uuid)"
  185. ),
  186. {"uid": source_uid},
  187. )
  188. connection.execute(
  189. text(
  190. "DELETE FROM public.knowledge_embedding_profiles "
  191. "WHERE id = CAST(:id AS uuid)"
  192. ),
  193. {"id": profile_id},
  194. )
  195. engine.dispose()