test_deploy_check.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. """Synthetic deployment diagnostics; no external traffic or personal data."""
  2. import json
  3. from pathlib import Path
  4. import tempfile
  5. import unittest
  6. from unittest.mock import Mock, patch
  7. from scripts import deploy_check
  8. from src.dms_schema import DEFAULT_SCHEMA
  9. class FakeMilvus:
  10. def __init__(self, **kwargs):
  11. self.db = None
  12. self.index_count = 3
  13. def list_databases(self, **kwargs):
  14. return ['test_qa']
  15. def use_database(self, name, **kwargs):
  16. self.db = name
  17. def list_collections(self, **kwargs):
  18. return ['index_x', 'content_x']
  19. def query(self, name, **kwargs):
  20. return [{'count(*)': self.index_count if name == 'index_x' else 5}]
  21. class FakeQcc:
  22. def search(self, name):
  23. return {'candidates': [{'Name': name, 'KeyNo': 'synthetic'}]}
  24. def basic(self, company):
  25. return {'data': {'KeyNo': company['KeyNo']}}
  26. def honors(self, company):
  27. return {'complete': True, 'records': [], 'sources': [{}], 'error': None}
  28. class FakeDmsSessions:
  29. def __init__(self, config):
  30. self.config = config
  31. def connect(self):
  32. return self
  33. class FakeDmsCompany:
  34. connected = []
  35. def __init__(self, config):
  36. self.config = config
  37. def connect(self, identity):
  38. self.connected.append(identity)
  39. return self
  40. class DeployCheckTests(unittest.TestCase):
  41. def setUp(self):
  42. temporary = tempfile.TemporaryDirectory()
  43. self.addCleanup(temporary.cleanup)
  44. self.root = Path(temporary.name)
  45. (self.root / 'knowledge/prepared/ingestion').mkdir(parents=True)
  46. self.manifest = {'complete': True, 'snapshot': 'synthetic', 'counts': {'qa': 3},
  47. 'databases': {'qa': {'database': 'test_qa', 'index_collection': 'index_x',
  48. 'content_collection': 'content_x'}}}
  49. (self.root / 'knowledge/prepared/ingestion/manifest.json').write_text(json.dumps(self.manifest))
  50. (self.root / 'config').mkdir()
  51. self.schema = json.loads(json.dumps(DEFAULT_SCHEMA))
  52. (self.root / 'config/dms_schema.json').write_text(json.dumps(self.schema), encoding='utf-8')
  53. (self.root / '.env').write_text('DMS_BASE_URL=https://example.invalid/dms\nDMS_TOKEN=synthetic\n')
  54. def test_milvus_compares_published_counts(self):
  55. with (patch('src.step2_knowledge_ingest.ingest.config', return_value={
  56. 'MILVUS_URI': 'http://synthetic.invalid', 'MILVUS_TIMEOUT_SECONDS': 2,
  57. 'MILVUS_DB_PREFIX': 'test'}),
  58. patch('src.step2_knowledge_ingest.ingest.load', return_value=[{'kind': 'qa'}] * 3),
  59. patch('src.step2_knowledge_ingest.ingest.content_parts', side_effect=[[0, 1], [0, 1], [0]] * 3)):
  60. self._check_milvus_counts()
  61. def _check_milvus_counts(self):
  62. healthy = deploy_check.milvus(self.root, FakeMilvus)
  63. self.assertTrue(healthy['ok'])
  64. self.assertEqual(healthy['libraries']['qa']['content_segments'], 5)
  65. client = FakeMilvus()
  66. client.index_count = 2
  67. changed = deploy_check.milvus(self.root, lambda **kwargs: client)
  68. self.assertFalse(changed['ok'])
  69. self.assertEqual(changed['libraries']['qa']['index_delta'], -1)
  70. self.manifest['databases']['qa']['database'] = 'wrong_qa'
  71. (self.root / 'knowledge/prepared/ingestion/manifest.json').write_text(json.dumps(self.manifest))
  72. self.assertEqual(deploy_check.milvus(self.root, FakeMilvus)['libraries']['qa']['code'],
  73. 'database_missing_or_mismatched')
  74. def test_redis_pings_once_and_closes(self):
  75. client = Mock()
  76. client.ping.return_value = True
  77. with patch('src.step3_question_answer.context.load_context_config',
  78. return_value=Mock(url='redis://synthetic.invalid')):
  79. self.assertTrue(deploy_check.redis_check(self.root, lambda *args, **kwargs: client)['ok'])
  80. client.ping.assert_called_once()
  81. client.close.assert_called_once()
  82. def test_dms_checks_session_and_company_schema_without_content_reads(self):
  83. FakeDmsCompany.connected = []
  84. with patch.dict('os.environ', {}, clear=True):
  85. report = deploy_check.dms(self.root, FakeDmsSessions, FakeDmsCompany)
  86. self.assertTrue(report['ok'])
  87. self.assertEqual(report['scopes']['sessions']['columns'], 3)
  88. self.assertEqual(report['scopes']['company']['columns'], 2)
  89. self.assertEqual(FakeDmsCompany.connected,
  90. [self.schema['columns'][alias]['id'] for alias in ('basic', 'honor')])
  91. def test_dms_uses_configured_column_ids_and_names(self):
  92. self.schema['columns']['basic']['id'] = 2888
  93. self.schema['columns']['honor']['id'] = 2886
  94. self.schema['columns']['basic']['name'] = '企业档案'
  95. self.schema['columns']['basic']['fields']['c_name'] = 'c_business_name'
  96. (self.root / 'config/dms_schema.json').write_text(json.dumps(self.schema), encoding='utf-8')
  97. FakeDmsCompany.connected = []
  98. observed = []
  99. class CapturingCompany(FakeDmsCompany):
  100. def __init__(self, config):
  101. super().__init__(config)
  102. observed.append(config)
  103. with patch.dict('os.environ', {}, clear=True):
  104. report = deploy_check.dms(self.root, FakeDmsSessions, CapturingCompany)
  105. self.assertTrue(report['ok'])
  106. self.assertEqual(FakeDmsCompany.connected, [2888, 2886])
  107. self.assertEqual(observed[0].scope_names, ('企业档案', '企业荣誉信息'))
  108. self.assertEqual(observed[0].schema['columns']['basic']['fields']['c_name'], 'c_business_name')
  109. def test_dms_reports_independent_scopes_and_redacts_errors(self):
  110. class BrokenSession(FakeDmsSessions):
  111. def connect(self):
  112. raise RuntimeError('secret-url')
  113. with patch.dict('os.environ', {}, clear=True):
  114. report = deploy_check.dms(self.root, BrokenSession, FakeDmsCompany)
  115. self.assertFalse(report['ok'])
  116. self.assertEqual(report['scopes']['sessions']['code'], 'request_failed')
  117. self.assertTrue(report['scopes']['company']['ok'])
  118. self.assertNotIn('secret-url', json.dumps(report))
  119. def test_deepseek_requires_actual_content(self):
  120. config = Mock(model='synthetic', api_key='secret')
  121. with patch('src.step1_knowledge_prepare.summarize.load_config', return_value=config):
  122. self.assertFalse(deploy_check.deepseek(self.root, lambda payload, cfg: {'choices': []})['ok'])
  123. self.assertTrue(deploy_check.deepseek(self.root, lambda payload, cfg: {
  124. 'choices': [{'message': {'content': 'OK'}}]})['ok'])
  125. def test_qcc_checks_three_interfaces_without_echoing_company(self):
  126. report = deploy_check.qcc(self.root, 'synthetic company', FakeQcc())
  127. self.assertTrue(report['ok'])
  128. self.assertEqual(set(report['interfaces']), {'886', '410', '1001'})
  129. self.assertNotIn('synthetic company', json.dumps(report))
  130. fake = FakeQcc()
  131. fake.search = lambda name: {'candidates': []}
  132. report = deploy_check.qcc(self.root, 'synthetic company', fake)
  133. self.assertFalse(report['ok'])
  134. self.assertEqual(report['interfaces']['410']['code'], 'exact_company_not_found')
  135. def test_run_continues_on_failure_and_hides_exception_message(self):
  136. with (patch.object(deploy_check, 'milvus', side_effect=RuntimeError('secret-url')),
  137. patch.object(deploy_check, 'redis_check', return_value=deploy_check.result(True, 'connected')),
  138. patch.object(deploy_check, 'dms', return_value=deploy_check.result(True, 'usable')),
  139. patch.object(deploy_check, 'deepseek', return_value=deploy_check.result(True, 'usable')),
  140. patch.object(deploy_check, 'qcc', return_value=deploy_check.result(True, 'usable'))):
  141. with patch('builtins.print') as printed:
  142. self.assertEqual(deploy_check.run(self.root, 'synthetic'), 1)
  143. output = printed.call_args.args[0]
  144. self.assertNotIn('secret-url', output)
  145. self.assertEqual(json.loads(output)['checks']['milvus']['code'], 'request_failed')
  146. def test_independent_checks_only_call_selected_service(self):
  147. for action, selected in (('check-milvus', 'milvus'), ('check-redis', 'redis'),
  148. ('check-dms', 'dms'), ('check-deepseek', 'deepseek'), ('check-qcc', 'qcc')):
  149. with self.subTest(action=action):
  150. probes = {name: Mock(return_value=deploy_check.result(True, 'usable'))
  151. for name in ('milvus', 'redis_check', 'dms', 'deepseek', 'qcc')}
  152. with (patch.object(deploy_check, 'milvus', probes['milvus']),
  153. patch.object(deploy_check, 'redis_check', probes['redis_check']),
  154. patch.object(deploy_check, 'dms', probes['dms']),
  155. patch.object(deploy_check, 'deepseek', probes['deepseek']),
  156. patch.object(deploy_check, 'qcc', probes['qcc']),
  157. patch('builtins.print') as printed):
  158. self.assertEqual(deploy_check.run(self.root, 'synthetic', action), 0)
  159. self.assertEqual(set(json.loads(printed.call_args.args[0])['checks']), {selected})
  160. for name, probe in probes.items():
  161. self.assertEqual(probe.call_count, int(name == ('redis_check' if selected == 'redis' else selected)))