test_architecture_artifacts.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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 function.attr == "route"
  21. and isinstance(function.value, ast.Name)
  22. and function.value.id == "bp"
  23. ):
  24. count += 1
  25. return count
  26. def test_required_architecture_artifacts_exist():
  27. required = {
  28. "ARCHITECTURE_OVERVIEW.md",
  29. "OPENAPI.yaml",
  30. "FRONTEND_REMOVAL_INVENTORY.md",
  31. "DATA_MODEL.md",
  32. "DEPLOYMENT_SOURCE_OF_TRUTH.md",
  33. "ADR-001-authentication.md",
  34. "ADR-002-workflow-engine.md",
  35. "ADR-003-cross-store-consistency.md",
  36. "NEXT_ITERATION_ROADMAP.md",
  37. }
  38. assert required <= {path.name for path in ARCH.glob("*")}
  39. def test_openapi_route_inventory_matches_flask_source():
  40. source_count = _route_count()
  41. contract = (ARCH / "OPENAPI.yaml").read_text(encoding="utf-8")
  42. assert source_count > 100
  43. assert "openapi: 3.1.0" in contract
  44. assert f"x-route-count: {source_count}" in contract
  45. assert contract.count("operationId:") == source_count
  46. def test_openapi_generator_is_reproducible(tmp_path):
  47. generated = tmp_path / "OPENAPI.yaml"
  48. subprocess.run(
  49. [
  50. sys.executable,
  51. str(ROOT / "scripts" / "generate_openapi.py"),
  52. "--output",
  53. str(generated),
  54. ],
  55. cwd=ROOT,
  56. check=True,
  57. )
  58. assert generated.read_bytes() == (ARCH / "OPENAPI.yaml").read_bytes()
  59. def test_contract_ci_regenerates_and_checks_the_committed_inventory():
  60. workflow = (ROOT / ".github" / "workflows" / "contracts.yml").read_text(
  61. encoding="utf-8"
  62. )
  63. assert "scripts/generate_openapi.py" in workflow
  64. assert "git diff --exit-code" in workflow
  65. assert "tests/test_architecture_artifacts.py" in workflow
  66. def test_architecture_decisions_capture_product_boundaries():
  67. combined = "\n".join(
  68. (ARCH / name).read_text(encoding="utf-8")
  69. for name in (
  70. "DATA_MODEL.md",
  71. "DEPLOYMENT_SOURCE_OF_TRUTH.md",
  72. "ADR-001-authentication.md",
  73. "ADR-002-workflow-engine.md",
  74. "ADR-003-cross-store-consistency.md",
  75. "NEXT_ITERATION_ROADMAP.md",
  76. )
  77. )
  78. for required_term in (
  79. "管理员",
  80. "编辑者",
  81. "查看者",
  82. "DataFlow",
  83. "n8n Workflow",
  84. "同一环境只允许一个当前生效版本",
  85. "Qwen",
  86. "DeepSeek",
  87. "每日全量一致性巡检",
  88. "app/",
  89. "deployment/app/",
  90. ):
  91. assert required_term in combined
  92. def test_removal_inventory_is_bound_to_the_recovery_backup():
  93. inventory = (ARCH / "FRONTEND_REMOVAL_INVENTORY.md").read_text(
  94. encoding="utf-8"
  95. )
  96. assert "DataOps-platform-backup-before-cleanup-20260716-170321.tar.gz" in inventory
  97. assert "866c6c4aff127f88497a5ddf39ac8d8da93008cc784ae6f276f80a7cd17d4ef0" in inventory
  98. assert "348" in inventory
  99. def test_release_sync_and_package_include_versioned_migrations():
  100. sync = (ROOT / "deployment" / "sync_release.sh").read_text(encoding="utf-8")
  101. package = (ROOT / "deployment" / "package_release.sh").read_text(
  102. encoding="utf-8"
  103. )
  104. run_script = (ROOT / "scripts" / "run_dataops.sh").read_text(encoding="utf-8")
  105. assert 'copy_tree "${ROOT_DIR}/migrations"' in sync
  106. assert 'copy_file "${ROOT_DIR}/alembic.ini"' in sync
  107. assert 'copy_into_release "${SCRIPT_DIR}/migrations"' in package
  108. assert 'copy_into_release "${SCRIPT_DIR}/alembic.ini"' in package
  109. assert 'alembic" -c "${APP_DIR}/alembic.ini" upgrade head' in run_script