160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""RegistrationStore 与本地 Grok 客服 MCP 共用数据文件的并发测试。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import tempfile
|
|
import threading
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import customer_service_policy as policy
|
|
from registration_store import RegistrationStore, process_registration_reply
|
|
|
|
|
|
class RegistrationStoreConcurrencyTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temp_dir = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp_dir.cleanup)
|
|
self.path = Path(self.temp_dir.name) / "registration_leads.json"
|
|
self.path_patch = mock.patch.object(
|
|
policy,
|
|
"REGISTRATIONS_PATH",
|
|
self.path,
|
|
)
|
|
self.path_patch.start()
|
|
self.addCleanup(self.path_patch.stop)
|
|
|
|
def _mcp_register(self, session_id: str) -> dict:
|
|
return policy.record_registration(
|
|
session_id=session_id,
|
|
customer_message="帮我预约一下,我最近空腹血糖偏高",
|
|
contact_name="测试客户",
|
|
)
|
|
|
|
def test_existing_store_refreshes_after_mcp_write(self) -> None:
|
|
store = RegistrationStore(str(self.path))
|
|
result = self._mcp_register("00112233445566778899aabbccddeeff")
|
|
|
|
self.assertTrue(result["registered"])
|
|
leads = store.list_leads()
|
|
self.assertEqual(1, len(leads))
|
|
self.assertEqual("pending_human_confirmation", leads[0]["status"])
|
|
self.assertEqual(1, store.pending_count())
|
|
|
|
def test_ui_mutation_reloads_and_preserves_new_mcp_lead(self) -> None:
|
|
store = RegistrationStore(str(self.path))
|
|
self._mcp_register("00112233445566778899aabbccddeeff")
|
|
|
|
ui_lead = store.add_or_update(
|
|
session_id="ui-session",
|
|
contact="UI 客户",
|
|
symptom="待补充",
|
|
status="pending_symptom",
|
|
)
|
|
|
|
payload = json.loads(self.path.read_text(encoding="utf-8"))
|
|
sessions = {item["session_id"] for item in payload["leads"]}
|
|
self.assertEqual(
|
|
{"00112233445566778899aabbccddeeff", "ui-session"},
|
|
sessions,
|
|
)
|
|
self.assertIn(ui_lead["id"], {item["id"] for item in payload["leads"]})
|
|
|
|
def test_parallel_ui_and_mcp_writes_do_not_lose_records(self) -> None:
|
|
store = RegistrationStore(str(self.path))
|
|
barrier = threading.Barrier(2)
|
|
errors: list[BaseException] = []
|
|
|
|
def mcp_writer() -> None:
|
|
try:
|
|
barrier.wait()
|
|
for index in range(10):
|
|
self._mcp_register(f"{index + 1:032x}")
|
|
except BaseException as exc: # pragma: no cover - assertion below
|
|
errors.append(exc)
|
|
|
|
def ui_writer() -> None:
|
|
try:
|
|
barrier.wait()
|
|
for index in range(10):
|
|
store.add_or_update(
|
|
session_id=f"ui-session-{index}",
|
|
contact=f"UI 客户 {index}",
|
|
symptom="待补充",
|
|
status="pending_symptom",
|
|
)
|
|
except BaseException as exc: # pragma: no cover - assertion below
|
|
errors.append(exc)
|
|
|
|
threads = [
|
|
threading.Thread(target=mcp_writer),
|
|
threading.Thread(target=ui_writer),
|
|
]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join(timeout=10)
|
|
|
|
self.assertFalse(any(thread.is_alive() for thread in threads))
|
|
self.assertEqual([], errors)
|
|
leads = store.list_leads()
|
|
self.assertEqual(20, len(leads))
|
|
self.assertEqual(20, len({item["session_id"] for item in leads}))
|
|
|
|
def test_save_failure_is_not_swallowed(self) -> None:
|
|
store = RegistrationStore(str(self.path))
|
|
with mock.patch.object(
|
|
policy,
|
|
"_atomic_write_json",
|
|
side_effect=policy.LocalStoreError("模拟保存失败"),
|
|
):
|
|
with self.assertRaisesRegex(policy.LocalStoreError, "模拟保存失败"):
|
|
store.add_or_update(
|
|
session_id="ui-session",
|
|
contact="UI 客户",
|
|
symptom="待补充",
|
|
status="pending_symptom",
|
|
)
|
|
|
|
self.assertFalse(self.path.exists())
|
|
|
|
def test_pending_count_refreshes_all_pending_statuses(self) -> None:
|
|
self.path.write_text(
|
|
json.dumps(
|
|
{
|
|
"leads": [
|
|
{"id": "a", "status": "pending_symptom"},
|
|
{"id": "b", "status": "pending_human_confirmation"},
|
|
{"id": "c", "status": "booked"},
|
|
{"id": "d", "status": "done"},
|
|
]
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
store = RegistrationStore(str(self.path))
|
|
self.assertEqual(3, store.pending_count())
|
|
|
|
def test_legacy_registration_helper_never_claims_booking_success(self) -> None:
|
|
store = RegistrationStore(str(self.path))
|
|
reply, lead = process_registration_reply(
|
|
session_id="legacy-session",
|
|
user_text="请帮我预约一下,我最近空腹血糖偏高",
|
|
reply_text="我先帮您处理。",
|
|
store=store,
|
|
)
|
|
|
|
self.assertIsNotNone(lead)
|
|
self.assertEqual("pending_human_confirmation", lead["status"])
|
|
self.assertIn("人工", reply)
|
|
self.assertIn("尚未预约成功", reply)
|
|
self.assertNotIn("已帮您预约", reply)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|