test_architecture_artifacts.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import ast
  2. import subprocess
  3. import sys
  4. from pathlib import Path
  5. ROOT = Path(__file__).resolve().parents[1]
  6. ARCH = ROOT / "docs" / "architecture"
  7. def _route_count() -> int:
  8. count = 0
  9. for route_file in (ROOT / "app" / "api").glob("*/*.py"):
  10. tree = ast.parse(route_file.read_text(encoding="utf-8"))
  11. for node in ast.walk(tree):
  12. if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
  13. continue
  14. for decorator in node.decorator_list:
  15. if not isinstance(decorator, ast.Call):
  16. continue
  17. function = decorator.func
  18. if (
  19. isinstance(function, ast.Attribute)
  20. and isinstance(function.value, ast.Name)
  21. and function.value.id == "bp"
  22. and (
  23. function.attr == "route"
  24. or (
  25. route_file.parent.name == "knowledge_base"
  26. and function.attr
  27. in {"get", "post", "put", "patch", "delete"}
  28. )
  29. )
  30. ):
  31. count += 1
  32. return count
  33. def test_required_architecture_artifacts_exist():
  34. required = {
  35. "ARCHITECTURE_OVERVIEW.md",
  36. "OPENAPI.yaml",
  37. "FRONTEND_REMOVAL_INVENTORY.md",
  38. "DATA_MODEL.md",
  39. "DEPLOYMENT_SOURCE_OF_TRUTH.md",
  40. "ADR-001-authentication.md",
  41. "ADR-002-workflow-engine.md",
  42. "ADR-003-cross-store-consistency.md",
  43. "ADR-004-ai-first-kestra-orchestration.md",
  44. "NEXT_ITERATION_ROADMAP.md",
  45. }
  46. assert required <= {path.name for path in ARCH.glob("*")}
  47. def test_openapi_route_inventory_matches_flask_source():
  48. source_count = _route_count()
  49. contract = (ARCH / "OPENAPI.yaml").read_text(encoding="utf-8")
  50. assert source_count > 100
  51. assert "openapi: 3.1.0" in contract
  52. assert f"x-route-count: {source_count}" in contract
  53. assert contract.count("operationId:") == source_count
  54. def test_openapi_generator_is_reproducible(tmp_path):
  55. generated = tmp_path / "OPENAPI.yaml"
  56. subprocess.run(
  57. [
  58. sys.executable,
  59. str(ROOT / "scripts" / "generate_openapi.py"),
  60. "--output",
  61. str(generated),
  62. ],
  63. cwd=ROOT,
  64. check=True,
  65. )
  66. assert generated.read_bytes() == (ARCH / "OPENAPI.yaml").read_bytes()
  67. def test_wp09_contract_and_migration_are_documented():
  68. contract = (ARCH / "OPENAPI.yaml").read_text(encoding="utf-8")
  69. data_model = (ARCH / "DATA_MODEL.md").read_text(encoding="utf-8")
  70. migration = (
  71. ROOT
  72. / "migrations/versions/20260729_350_device_observability.py"
  73. )
  74. for path in (
  75. "/api/development/v1/device-observability/events",
  76. "/api/development/v1/device-observability/import",
  77. "/api/development/v1/device-observability/graph",
  78. "/api/development/v1/device-observability/root-cause",
  79. ):
  80. assert path in contract
  81. assert migration.exists()
  82. assert "device_operational_events" in data_model
  83. assert "device_evidence_relations" in data_model
  84. assert "证据不足,无法确认根因" in data_model
  85. def test_wp10_device_knowledge_contract_and_boundaries_are_documented():
  86. contract = (ARCH / "OPENAPI.yaml").read_text(encoding="utf-8")
  87. data_model = (ARCH / "DATA_MODEL.md").read_text(encoding="utf-8")
  88. golden_set = (
  89. ROOT
  90. / "docs/acceptance/WP10_DEVICE_KNOWLEDGE_GOLDEN_SET.json"
  91. )
  92. for path in (
  93. "/api/knowledge/search",
  94. "/api/knowledge/ask",
  95. "/api/knowledge/sources/{source_uid}",
  96. "/api/knowledge/admin/device-sources",
  97. "/api/knowledge/admin/device-sources/{source_uid}/scope",
  98. "/api/knowledge/admin/query-audits",
  99. ):
  100. assert path in contract
  101. assert "x-response-fields: [" in contract
  102. assert "citations" in contract
  103. assert "evidence" in contract
  104. assert "knowledge_query_audits" in data_model
  105. assert "authorized_sources" in data_model
  106. assert "LightRAG 仍是影子投影" in data_model
  107. assert golden_set.exists()
  108. def test_wp11_governance_metric_contract_and_boundaries_are_documented():
  109. contract = (ARCH / "OPENAPI.yaml").read_text(encoding="utf-8")
  110. data_model = (ARCH / "DATA_MODEL.md").read_text(encoding="utf-8")
  111. for path in (
  112. "/api/development/v1/governance-metrics/summary",
  113. "/api/development/v1/governance-metrics/details",
  114. ):
  115. assert path in contract
  116. for field in ("scope", "metrics", "metric", "state", "records", "total"):
  117. assert field in contract
  118. for term in (
  119. "台账完整率",
  120. "责任覆盖率",
  121. "实体映射率",
  122. "问题闭环率",
  123. "问题复发率",
  124. "rate = null",
  125. "authorized_sources",
  126. "不依赖前端过滤",
  127. ):
  128. assert term in data_model
  129. def test_wp12_security_audit_contract_and_boundaries_are_documented():
  130. contract = (ARCH / "OPENAPI.yaml").read_text(encoding="utf-8")
  131. data_model = (ARCH / "DATA_MODEL.md").read_text(encoding="utf-8")
  132. validation = ROOT / "docs/validation/WP12_SECURITY_AUDIT_EVIDENCE.md"
  133. for path in (
  134. "/api/system/governance-audit/security-checks",
  135. "/api/system/governance-audit/coverage",
  136. "/api/system/governance-audit/events",
  137. "/api/system/governance-audit/seals",
  138. "/api/system/governance-audit/seals/{seal_uid}/verify",
  139. ):
  140. assert path in contract
  141. for term in (
  142. "governance_audit_seals",
  143. "HMAC-SHA256",
  144. "authentication",
  145. "ingestion",
  146. "entity_resolution",
  147. "publication",
  148. "remediation",
  149. "knowledge_query",
  150. "不等于阻止数据库管理员修改",
  151. "50,000",
  152. ):
  153. assert term in data_model
  154. assert validation.exists()
  155. def test_wp12_release_copy_matches_authoritative_backend_files():
  156. for relative in (
  157. "app/__init__.py",
  158. "app/api/system/__init__.py",
  159. "app/api/system/governance_audit.py",
  160. "app/config/config.py",
  161. "app/core/data_source/redaction.py",
  162. "app/core/system/governance_audit.py",
  163. "app/core/system/governance_audit_repository.py",
  164. "app/core/system/permissions.py",
  165. ):
  166. source = ROOT / relative
  167. release = ROOT / "deployment" / relative
  168. assert release.read_bytes() == source.read_bytes(), relative
  169. def test_contract_ci_regenerates_and_checks_the_committed_inventory():
  170. workflow = (ROOT / ".github" / "workflows" / "contracts.yml").read_text(
  171. encoding="utf-8"
  172. )
  173. assert "scripts/generate_openapi.py" in workflow
  174. assert "git diff --exit-code" in workflow
  175. assert "tests/test_architecture_artifacts.py" in workflow
  176. def test_architecture_decisions_capture_product_boundaries():
  177. combined = "\n".join(
  178. (ARCH / name).read_text(encoding="utf-8")
  179. for name in (
  180. "DATA_MODEL.md",
  181. "DEPLOYMENT_SOURCE_OF_TRUTH.md",
  182. "ADR-001-authentication.md",
  183. "ADR-002-workflow-engine.md",
  184. "ADR-003-cross-store-consistency.md",
  185. "ADR-004-ai-first-kestra-orchestration.md",
  186. "NEXT_ITERATION_ROADMAP.md",
  187. )
  188. )
  189. for required_term in (
  190. "管理员",
  191. "编辑者",
  192. "查看者",
  193. "DataFlow",
  194. "n8n Workflow",
  195. "同一环境只允许一个当前生效版本",
  196. "Qwen",
  197. "DeepSeek",
  198. "每日全量一致性巡检",
  199. "app/",
  200. "deployment/app/",
  201. ):
  202. assert required_term in combined
  203. def test_removal_inventory_is_bound_to_the_recovery_backup():
  204. inventory = (ARCH / "FRONTEND_REMOVAL_INVENTORY.md").read_text(
  205. encoding="utf-8"
  206. )
  207. assert "DataOps-platform-backup-before-cleanup-20260716-170321.tar.gz" in inventory
  208. assert "866c6c4aff127f88497a5ddf39ac8d8da93008cc784ae6f276f80a7cd17d4ef0" in inventory
  209. assert "348" in inventory
  210. def test_release_sync_and_package_include_versioned_migrations():
  211. sync = (ROOT / "deployment" / "sync_release.sh").read_text(encoding="utf-8")
  212. package = (ROOT / "deployment" / "package_release.sh").read_text(
  213. encoding="utf-8"
  214. )
  215. run_script = (ROOT / "scripts" / "run_dataops.sh").read_text(encoding="utf-8")
  216. assert 'copy_tree "${ROOT_DIR}/migrations"' in sync
  217. assert 'copy_file "${ROOT_DIR}/alembic.ini"' in sync
  218. assert 'copy_into_release "${SCRIPT_DIR}/migrations"' in package
  219. assert 'copy_into_release "${SCRIPT_DIR}/alembic.ini"' in package
  220. assert 'alembic" -c "${APP_DIR}/alembic.ini" upgrade head' in run_script