test_knowledge_memory.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import asyncio
  2. from uuid import uuid4
  3. import pytest
  4. from sqlalchemy import select
  5. from test_after_sales import scenario # noqa: F401
  6. from zhifu.modules.identity.access_models import StoreGrant
  7. from zhifu.modules.knowledge.maintenance import clean
  8. from zhifu.modules.knowledge.vector import connection
  9. from zhifu.modules.memory.models import MemoryJob, MemoryProfile
  10. from zhifu.modules.memory.service import read, snapshot
  11. from zhifu.modules.memory.worker import claim
  12. pytestmark = pytest.mark.integration
  13. MEMORY = "/openapi/v1/me/memory"
  14. async def document(context):
  15. async with context["app"].state.sessions.begin() as session:
  16. session.add(
  17. StoreGrant(
  18. tenant_id=context["tenant"].bytes,
  19. principal_id=context["accounts"]["agent"].bytes,
  20. store_id=context["store"].bytes,
  21. permission="knowledge:write",
  22. )
  23. )
  24. path = f"/admin/v1/stores/{context['store']}/knowledge/{uuid4()}"
  25. response = await context["call"](
  26. "PUT",
  27. path,
  28. "agent",
  29. {
  30. "expected_version": 0,
  31. "title": "键盘清洁说明",
  32. "text": "清洁机械键盘前,请断开电源,使用干布擦拭表面。禁止直接水洗。",
  33. },
  34. )
  35. assert response.status_code == 200, response.text
  36. return path
  37. async def cleanup_vectors(tenant):
  38. def remove():
  39. with connection() as (client, collection):
  40. client.delete(collection, filter=f'tenant == "{tenant.hex}"')
  41. await asyncio.to_thread(remove)
  42. async def test_publication_and_revocation(scenario): # noqa: F811
  43. path = await document(scenario)
  44. query_path = f"/openapi/v1/stores/{scenario['store']}/knowledge/search"
  45. body = {"query": "键盘可以用水清洗吗"}
  46. try:
  47. assert (await scenario["call"]("POST", query_path, payload=body)).json()["items"] == []
  48. response = await scenario["call"](
  49. "POST", path + "/publish", "agent", {"expected_version": 1}
  50. )
  51. assert response.status_code == 200, response.text
  52. result = await scenario["call"]("POST", query_path, payload=body)
  53. assert result.status_code == 200, result.text
  54. assert "禁止直接水洗" in result.json()["items"][0]["text"]
  55. assert (await scenario["call"]("POST", query_path, "foreign", body)).status_code == 404
  56. assert (
  57. await scenario["call"]("POST", path + "/revoke", payload={"expected_version": 1})
  58. ).status_code == 403
  59. response = await scenario["call"](
  60. "POST", path + "/revoke", "agent", {"expected_version": 1}
  61. )
  62. assert response.status_code == 200, response.text
  63. assert (await scenario["call"]("POST", query_path, payload=body)).json()["items"] == []
  64. assert (
  65. await scenario["call"]("POST", path + "/publish", "agent", {"expected_version": 1})
  66. ).status_code == 409
  67. await clean(scenario["app"].state.sessions)
  68. with connection() as (client, collection):
  69. assert client.query(collection, filter=f'tenant == "{scenario["tenant"].hex}"') == []
  70. finally:
  71. await cleanup_vectors(scenario["tenant"])
  72. async def test_memory_consent_isolation_and_clear(scenario): # noqa: F811
  73. call = scenario["call"]
  74. assert (await call("GET", MEMORY)).json()["consent"] is False
  75. body = {"expected_revision": 0, "communication": {"response_style": "concise"}}
  76. assert (await call("PUT", MEMORY, payload=body)).status_code == 403
  77. response = await call(
  78. "PUT", MEMORY + "/consent", payload={"expected_revision": 0, "enabled": True}
  79. )
  80. assert response.status_code == 200, response.text
  81. body["expected_revision"] = 1
  82. response = await call("PUT", MEMORY, payload=body)
  83. assert response.status_code == 200, response.text
  84. assert (await call("GET", MEMORY)).json()["content"]["communication"] == {
  85. "response_style": "concise"
  86. }
  87. assert (await call("GET", MEMORY, "other")).json()["content"] == {}
  88. assert (await call("GET", MEMORY, "agent")).status_code == 403
  89. assert (await call("PUT", MEMORY, payload=body)).status_code == 409
  90. response = await call("DELETE", MEMORY + "?expected_revision=2")
  91. assert response.status_code == 200, response.text
  92. assert (await call("GET", MEMORY)).json()["content"] == {}
  93. async def test_memory_clear_invalidates_queued_extraction(scenario): # noqa: F811
  94. call = scenario["call"]
  95. await call("PUT", MEMORY + "/consent", payload={"expected_revision": 0, "enabled": True})
  96. response = await call(
  97. "POST", "/openapi/v1/conversations", payload={"store_id": str(scenario["store"])}
  98. )
  99. path = "/openapi/v1/conversations/" + response.json()["conversation_id"]
  100. response = await call(
  101. "POST",
  102. path + "/messages",
  103. payload={
  104. "text": "以后请简短回复",
  105. "expected_control_version": 1,
  106. },
  107. )
  108. assert response.status_code == 201, response.text
  109. await call("DELETE", MEMORY + "?expected_revision=1")
  110. assert await claim(scenario["app"].state.sessions) is None
  111. async with scenario["app"].state.sessions() as session:
  112. job = await session.scalar(
  113. select(MemoryJob).where(MemoryJob.tenant_id == scenario["tenant"].bytes)
  114. )
  115. assert job.status == "CANCELLED"
  116. row = await session.get(
  117. MemoryProfile, (scenario["tenant"].bytes, scenario["accounts"]["customer"].bytes)
  118. )
  119. assert await read(row.tenant_id, row.subject_id, snapshot(row)) == {}