test_integration.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. """Integration test for dual-system workflow (System 2 <-> System 1).
  2. Tests the full closed loop:
  3. 1. Create project
  4. 2. Create simulation plan
  5. 3. Upload scan results CSV (simulating System 2 execution)
  6. 4. Import results to experience library
  7. 5. Query experience library (list/get/update/delete)
  8. 6. Query analytics (trend/pareto/sensitivity/overview/stats/similar)
  9. Uses FastAPI TestClient (no real server needed).
  10. All source is ASCII.
  11. """
  12. import io
  13. import json
  14. import sys
  15. import os
  16. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  17. from fastapi.testclient import TestClient
  18. from app.main import app
  19. client = TestClient(app)
  20. # Sample CSV content simulating scan_results.csv from System 2
  21. SAMPLE_CSV = """run_index,status,seconds,Airgap,RMSCurrent,MagnetThickness,tavg_nm,ripple_pct,efficiency_pct,total_losses_w,copper_loss_w,iron_loss_w
  22. 0,OK,120,0.8,20,5,2.800,3.50,86.0,50.0,30.0,15.0
  23. 1,OK,118,1.0,20,5,2.500,3.20,85.2,48.0,28.0,14.0
  24. 2,OK,121,1.2,20,5,2.300,2.90,84.8,45.0,26.0,13.0
  25. 3,OK,125,1.0,25,5,3.000,4.10,86.5,55.0,35.0,15.0
  26. 4,FAILED,0,1.5,20,5,0,0,0,0,0,0
  27. """
  28. passed = 0
  29. failed = 0
  30. def test(name, condition, detail=""):
  31. global passed, failed
  32. if condition:
  33. passed += 1
  34. print(f" [PASS] {name}")
  35. else:
  36. failed += 1
  37. print(f" [FAIL] {name} {detail}")
  38. print("=" * 60)
  39. print("Dual-System Integration Test (System 2 <-> System 1)")
  40. print("=" * 60)
  41. # 1. Health check
  42. print("\n[1] Health Check")
  43. r = client.get("/api/health")
  44. test("health endpoint returns 200", r.status_code == 200, f"status={r.status_code}")
  45. test("health status is ok", r.json().get("status") == "ok")
  46. # 2. Create project
  47. print("\n[2] Create Project")
  48. r = client.post("/api/projects", json={
  49. "name": "Integration Test Project",
  50. "topology": "SSSR",
  51. "description": "Created by integration test",
  52. "boundary_conditions": {"outer_radius_mm": 100, "speed_rpm": 3000}
  53. })
  54. test("create project returns 201", r.status_code == 201, f"status={r.status_code}")
  55. project_id = r.json().get("id")
  56. test("project has id", project_id is not None)
  57. test("project name correct", r.json().get("name") == "Integration Test Project")
  58. # 3. Create plan
  59. print("\n[3] Create Simulation Plan")
  60. r = client.post("/api/plans", json={
  61. "project_id": project_id,
  62. "name": "Test Airgap Scan",
  63. "plan_id": "INT-TEST-001",
  64. "topology": "SSSR",
  65. "model_path": "models/test.mot",
  66. "plan_data": {
  67. "plan_id": "INT-TEST-001",
  68. "model_path": "models/test.mot",
  69. "variables": [
  70. {"name": "Airgap", "display_name": "Airgap", "unit": "mm", "values": [0.8, 1.0, 1.2]},
  71. {"name": "RMSCurrent", "display_name": "RMS Current", "unit": "A", "values": [20, 25]}
  72. ]
  73. },
  74. "estimated_points": 6
  75. })
  76. test("create plan returns 201", r.status_code == 201, f"status={r.status_code}, body={r.text}")
  77. plan_id = r.json().get("id")
  78. test("plan has id", plan_id is not None)
  79. # 4. Download plan (System 2 fetches plan)
  80. print("\n[4] Download Plan (System 2 -> System 1)")
  81. r = client.get(f"/api/plans/{plan_id}/download")
  82. test("download plan returns 200", r.status_code == 200, f"status={r.status_code}")
  83. plan_data = r.json()
  84. test("download has plan_data", "plan_data" in plan_data)
  85. test("plan_data has variables", "variables" in plan_data.get("plan_data", {}))
  86. # 5. Upload results CSV (System 2 uploads results)
  87. print("\n[5] Upload Results CSV (System 2 -> System 1)")
  88. csv_file = ("scan_results.csv", SAMPLE_CSV.encode("utf-8"), "text/csv")
  89. r = client.post(
  90. f"/api/plans/{plan_id}/upload-results",
  91. files={"file": csv_file}
  92. )
  93. test("upload results returns 201", r.status_code == 201, f"status={r.status_code}, body={r.text}")
  94. upload_result = r.json()
  95. test("upload reports count", upload_result.get("count", 0) > 0, f"result={upload_result}")
  96. test("upload reports 5 results", upload_result.get("count") == 5, f"count={upload_result.get('count')}")
  97. # 6. Query plan results
  98. print("\n[6] Query Plan Results")
  99. r = client.get(f"/api/plans/{plan_id}/results")
  100. test("get results returns 200", r.status_code == 200)
  101. results = r.json().get("items", [])
  102. test("results has 5 items", len(results) == 5, f"count={len(results)}")
  103. ok_results = [r for r in results if r.get("status") == "OK"]
  104. test("4 OK results", len(ok_results) == 4, f"ok_count={len(ok_results)}")
  105. # 7. Import results to experience library
  106. print("\n[7] Import Results to Experience Library")
  107. r = client.post(f"/api/experience/from-plan/{plan_id}", json={
  108. "tags": ["integration-test", "auto-imported"],
  109. "rating": 3,
  110. "auto_conclusion": True
  111. })
  112. test("import returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
  113. import_result = r.json()
  114. test("imported 4 cases", import_result.get("imported") == 4, f"imported={import_result.get('imported')}")
  115. # 8. Query experience library
  116. print("\n[8] Query Experience Library")
  117. r = client.get("/api/experience?limit=50")
  118. test("list experience returns 200", r.status_code == 200)
  119. cases = r.json().get("items", [])
  120. test("at least 4 cases", len(cases) >= 4, f"count={len(cases)}")
  121. # Find our imported cases
  122. test_cases = [c for c in cases if "integration-test" in c.get("tags", [])]
  123. test("found integration-test cases", len(test_cases) >= 4, f"count={len(test_cases)}")
  124. if test_cases:
  125. case_id = test_cases[0]["id"]
  126. # Get single case
  127. r = client.get(f"/api/experience/{case_id}")
  128. test("get single case returns 200", r.status_code == 200)
  129. test("case has params", bool(r.json().get("params")))
  130. test("case has metrics", bool(r.json().get("metrics")))
  131. test("case has auto-generated conclusion", bool(r.json().get("conclusion")))
  132. # Update case
  133. r = client.put(f"/api/experience/{case_id}", json={
  134. "conclusion": "Updated by integration test",
  135. "tags": ["integration-test", "updated"],
  136. "rating": 5
  137. })
  138. test("update case returns 200", r.status_code == 200)
  139. test("updated conclusion", r.json().get("conclusion") == "Updated by integration test")
  140. test("updated rating", r.json().get("rating") == 5)
  141. # 9. Analytics: experience stats
  142. print("\n[9] Analytics: Experience Stats")
  143. r = client.get("/api/analytics/experience/stats")
  144. test("experience stats returns 200", r.status_code == 200)
  145. stats = r.json()
  146. test("stats has total", "total" in stats)
  147. test("stats has topology_distribution", "topology_distribution" in stats)
  148. test("stats has metric_ranges", "metric_ranges" in stats)
  149. # 10. Analytics: similar case search
  150. print("\n[10] Analytics: Similar Case Search")
  151. r = client.post("/api/analytics/experience/similar?top_k=3&tolerance=0.5", json={
  152. "params": {"Airgap": 1.0, "RMSCurrent": 20}
  153. })
  154. test("similar search returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
  155. similar = r.json().get("items", [])
  156. test("found similar cases", len(similar) >= 1, f"count={len(similar)}")
  157. if similar:
  158. test("similar has similarity_score", "similarity_score" in similar[0])
  159. # 11. Analytics: plan trend
  160. print("\n[11] Analytics: Plan Trend")
  161. r = client.get(f"/api/analytics/plans/{plan_id}/trend?param_key=Airgap&metric_key=tavg_nm")
  162. test("trend returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
  163. trend = r.json()
  164. test("trend has points", "points" in trend and len(trend["points"]) >= 3)
  165. test("trend points sorted by x", all(trend["points"][i][0] <= trend["points"][i+1][0] for i in range(len(trend["points"])-1)))
  166. # 12. Analytics: Pareto frontier
  167. print("\n[12] Analytics: Pareto Frontier")
  168. r = client.get(f"/api/analytics/plans/{plan_id}/pareto?x_metric=total_losses_w&y_metric=efficiency_pct")
  169. test("pareto returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
  170. pareto = r.json()
  171. test("pareto has all_points", "all_points" in pareto)
  172. test("pareto has pareto_points", "pareto_points" in pareto)
  173. test("pareto total count = 4", pareto.get("total_count") == 4, f"count={pareto.get('total_count')}")
  174. # 13. Analytics: sensitivity
  175. print("\n[13] Analytics: Parameter Sensitivity")
  176. r = client.get(f"/api/analytics/plans/{plan_id}/sensitivity?metric_key=tavg_nm")
  177. test("sensitivity returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
  178. sens = r.json()
  179. test("sensitivity has items", "items" in sens)
  180. if sens.get("items"):
  181. test("sensitivity sorted by abs_correlation desc",
  182. all(sens["items"][i]["abs_correlation"] >= sens["items"][i+1]["abs_correlation"]
  183. for i in range(len(sens["items"])-1)))
  184. # 14. Analytics: project overview
  185. print("\n[14] Analytics: Project Overview")
  186. r = client.get(f"/api/analytics/projects/{project_id}/overview")
  187. test("project overview returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
  188. overview = r.json()
  189. test("overview has total_plans", overview.get("total_plans") >= 1)
  190. test("overview has total_results", overview.get("total_results") >= 5)
  191. test("overview has ok_results", overview.get("ok_results") >= 4)
  192. test("overview has best_efficiency_pct", "best_efficiency_pct" in overview)
  193. test("overview has best_torque_nm", "best_torque_nm" in overview)
  194. # 15. Metric definitions
  195. print("\n[15] Metric Definitions")
  196. r = client.get("/api/analytics/metrics")
  197. test("metrics returns 200", r.status_code == 200)
  198. metrics = r.json().get("metrics", [])
  199. test("at least 10 metrics", len(metrics) >= 10, f"count={len(metrics)}")
  200. test("metric has key/label/unit", all("key" in m and "label" in m and "unit" in m for m in metrics))
  201. # 16. Delete test cases (cleanup)
  202. print("\n[16] Cleanup: Delete Test Cases")
  203. r = client.get("/api/experience?limit=50")
  204. test_cases = [c for c in r.json().get("items", []) if "integration-test" in c.get("tags", [])]
  205. deleted = 0
  206. for c in test_cases:
  207. r = client.delete(f"/api/experience/{c['id']}")
  208. if r.status_code == 204:
  209. deleted += 1
  210. test(f"deleted {len(test_cases)} test cases", deleted == len(test_cases), f"deleted={deleted}")
  211. # Summary
  212. print("\n" + "=" * 60)
  213. print(f"TEST SUMMARY: {passed} passed, {failed} failed")
  214. print("=" * 60)
  215. if failed > 0:
  216. sys.exit(1)
  217. else:
  218. print("\nAll integration tests passed!")
  219. sys.exit(0)