|
|
@@ -0,0 +1,252 @@
|
|
|
+"""Integration test for dual-system workflow (System 2 <-> System 1).
|
|
|
+
|
|
|
+Tests the full closed loop:
|
|
|
+1. Create project
|
|
|
+2. Create simulation plan
|
|
|
+3. Upload scan results CSV (simulating System 2 execution)
|
|
|
+4. Import results to experience library
|
|
|
+5. Query experience library (list/get/update/delete)
|
|
|
+6. Query analytics (trend/pareto/sensitivity/overview/stats/similar)
|
|
|
+
|
|
|
+Uses FastAPI TestClient (no real server needed).
|
|
|
+All source is ASCII.
|
|
|
+"""
|
|
|
+
|
|
|
+import io
|
|
|
+import json
|
|
|
+import sys
|
|
|
+import os
|
|
|
+
|
|
|
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
+
|
|
|
+from fastapi.testclient import TestClient
|
|
|
+from app.main import app
|
|
|
+
|
|
|
+client = TestClient(app)
|
|
|
+
|
|
|
+# Sample CSV content simulating scan_results.csv from System 2
|
|
|
+SAMPLE_CSV = """run_index,status,seconds,Airgap,RMSCurrent,MagnetThickness,tavg_nm,ripple_pct,efficiency_pct,total_losses_w,copper_loss_w,iron_loss_w
|
|
|
+0,OK,120,0.8,20,5,2.800,3.50,86.0,50.0,30.0,15.0
|
|
|
+1,OK,118,1.0,20,5,2.500,3.20,85.2,48.0,28.0,14.0
|
|
|
+2,OK,121,1.2,20,5,2.300,2.90,84.8,45.0,26.0,13.0
|
|
|
+3,OK,125,1.0,25,5,3.000,4.10,86.5,55.0,35.0,15.0
|
|
|
+4,FAILED,0,1.5,20,5,0,0,0,0,0,0
|
|
|
+"""
|
|
|
+
|
|
|
+passed = 0
|
|
|
+failed = 0
|
|
|
+
|
|
|
+
|
|
|
+def test(name, condition, detail=""):
|
|
|
+ global passed, failed
|
|
|
+ if condition:
|
|
|
+ passed += 1
|
|
|
+ print(f" [PASS] {name}")
|
|
|
+ else:
|
|
|
+ failed += 1
|
|
|
+ print(f" [FAIL] {name} {detail}")
|
|
|
+
|
|
|
+
|
|
|
+print("=" * 60)
|
|
|
+print("Dual-System Integration Test (System 2 <-> System 1)")
|
|
|
+print("=" * 60)
|
|
|
+
|
|
|
+# 1. Health check
|
|
|
+print("\n[1] Health Check")
|
|
|
+r = client.get("/api/health")
|
|
|
+test("health endpoint returns 200", r.status_code == 200, f"status={r.status_code}")
|
|
|
+test("health status is ok", r.json().get("status") == "ok")
|
|
|
+
|
|
|
+# 2. Create project
|
|
|
+print("\n[2] Create Project")
|
|
|
+r = client.post("/api/projects", json={
|
|
|
+ "name": "Integration Test Project",
|
|
|
+ "topology": "SSSR",
|
|
|
+ "description": "Created by integration test",
|
|
|
+ "boundary_conditions": {"outer_radius_mm": 100, "speed_rpm": 3000}
|
|
|
+})
|
|
|
+test("create project returns 201", r.status_code == 201, f"status={r.status_code}")
|
|
|
+project_id = r.json().get("id")
|
|
|
+test("project has id", project_id is not None)
|
|
|
+test("project name correct", r.json().get("name") == "Integration Test Project")
|
|
|
+
|
|
|
+# 3. Create plan
|
|
|
+print("\n[3] Create Simulation Plan")
|
|
|
+r = client.post("/api/plans", json={
|
|
|
+ "project_id": project_id,
|
|
|
+ "name": "Test Airgap Scan",
|
|
|
+ "plan_id": "INT-TEST-001",
|
|
|
+ "topology": "SSSR",
|
|
|
+ "model_path": "models/test.mot",
|
|
|
+ "plan_data": {
|
|
|
+ "plan_id": "INT-TEST-001",
|
|
|
+ "model_path": "models/test.mot",
|
|
|
+ "variables": [
|
|
|
+ {"name": "Airgap", "display_name": "Airgap", "unit": "mm", "values": [0.8, 1.0, 1.2]},
|
|
|
+ {"name": "RMSCurrent", "display_name": "RMS Current", "unit": "A", "values": [20, 25]}
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ "estimated_points": 6
|
|
|
+})
|
|
|
+test("create plan returns 201", r.status_code == 201, f"status={r.status_code}, body={r.text}")
|
|
|
+plan_id = r.json().get("id")
|
|
|
+test("plan has id", plan_id is not None)
|
|
|
+
|
|
|
+# 4. Download plan (System 2 fetches plan)
|
|
|
+print("\n[4] Download Plan (System 2 -> System 1)")
|
|
|
+r = client.get(f"/api/plans/{plan_id}/download")
|
|
|
+test("download plan returns 200", r.status_code == 200, f"status={r.status_code}")
|
|
|
+plan_data = r.json()
|
|
|
+test("download has plan_data", "plan_data" in plan_data)
|
|
|
+test("plan_data has variables", "variables" in plan_data.get("plan_data", {}))
|
|
|
+
|
|
|
+# 5. Upload results CSV (System 2 uploads results)
|
|
|
+print("\n[5] Upload Results CSV (System 2 -> System 1)")
|
|
|
+csv_file = ("scan_results.csv", SAMPLE_CSV.encode("utf-8"), "text/csv")
|
|
|
+r = client.post(
|
|
|
+ f"/api/plans/{plan_id}/upload-results",
|
|
|
+ files={"file": csv_file}
|
|
|
+)
|
|
|
+test("upload results returns 201", r.status_code == 201, f"status={r.status_code}, body={r.text}")
|
|
|
+upload_result = r.json()
|
|
|
+test("upload reports count", upload_result.get("count", 0) > 0, f"result={upload_result}")
|
|
|
+test("upload reports 5 results", upload_result.get("count") == 5, f"count={upload_result.get('count')}")
|
|
|
+
|
|
|
+# 6. Query plan results
|
|
|
+print("\n[6] Query Plan Results")
|
|
|
+r = client.get(f"/api/plans/{plan_id}/results")
|
|
|
+test("get results returns 200", r.status_code == 200)
|
|
|
+results = r.json().get("items", [])
|
|
|
+test("results has 5 items", len(results) == 5, f"count={len(results)}")
|
|
|
+ok_results = [r for r in results if r.get("status") == "OK"]
|
|
|
+test("4 OK results", len(ok_results) == 4, f"ok_count={len(ok_results)}")
|
|
|
+
|
|
|
+# 7. Import results to experience library
|
|
|
+print("\n[7] Import Results to Experience Library")
|
|
|
+r = client.post(f"/api/experience/from-plan/{plan_id}", json={
|
|
|
+ "tags": ["integration-test", "auto-imported"],
|
|
|
+ "rating": 3,
|
|
|
+ "auto_conclusion": True
|
|
|
+})
|
|
|
+test("import returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
|
|
|
+import_result = r.json()
|
|
|
+test("imported 4 cases", import_result.get("imported") == 4, f"imported={import_result.get('imported')}")
|
|
|
+
|
|
|
+# 8. Query experience library
|
|
|
+print("\n[8] Query Experience Library")
|
|
|
+r = client.get("/api/experience?limit=50")
|
|
|
+test("list experience returns 200", r.status_code == 200)
|
|
|
+cases = r.json().get("items", [])
|
|
|
+test("at least 4 cases", len(cases) >= 4, f"count={len(cases)}")
|
|
|
+
|
|
|
+# Find our imported cases
|
|
|
+test_cases = [c for c in cases if "integration-test" in c.get("tags", [])]
|
|
|
+test("found integration-test cases", len(test_cases) >= 4, f"count={len(test_cases)}")
|
|
|
+
|
|
|
+if test_cases:
|
|
|
+ case_id = test_cases[0]["id"]
|
|
|
+ # Get single case
|
|
|
+ r = client.get(f"/api/experience/{case_id}")
|
|
|
+ test("get single case returns 200", r.status_code == 200)
|
|
|
+ test("case has params", bool(r.json().get("params")))
|
|
|
+ test("case has metrics", bool(r.json().get("metrics")))
|
|
|
+ test("case has auto-generated conclusion", bool(r.json().get("conclusion")))
|
|
|
+
|
|
|
+ # Update case
|
|
|
+ r = client.put(f"/api/experience/{case_id}", json={
|
|
|
+ "conclusion": "Updated by integration test",
|
|
|
+ "tags": ["integration-test", "updated"],
|
|
|
+ "rating": 5
|
|
|
+ })
|
|
|
+ test("update case returns 200", r.status_code == 200)
|
|
|
+ test("updated conclusion", r.json().get("conclusion") == "Updated by integration test")
|
|
|
+ test("updated rating", r.json().get("rating") == 5)
|
|
|
+
|
|
|
+# 9. Analytics: experience stats
|
|
|
+print("\n[9] Analytics: Experience Stats")
|
|
|
+r = client.get("/api/analytics/experience/stats")
|
|
|
+test("experience stats returns 200", r.status_code == 200)
|
|
|
+stats = r.json()
|
|
|
+test("stats has total", "total" in stats)
|
|
|
+test("stats has topology_distribution", "topology_distribution" in stats)
|
|
|
+test("stats has metric_ranges", "metric_ranges" in stats)
|
|
|
+
|
|
|
+# 10. Analytics: similar case search
|
|
|
+print("\n[10] Analytics: Similar Case Search")
|
|
|
+r = client.post("/api/analytics/experience/similar?top_k=3&tolerance=0.5", json={
|
|
|
+ "params": {"Airgap": 1.0, "RMSCurrent": 20}
|
|
|
+})
|
|
|
+test("similar search returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
|
|
|
+similar = r.json().get("items", [])
|
|
|
+test("found similar cases", len(similar) >= 1, f"count={len(similar)}")
|
|
|
+if similar:
|
|
|
+ test("similar has similarity_score", "similarity_score" in similar[0])
|
|
|
+
|
|
|
+# 11. Analytics: plan trend
|
|
|
+print("\n[11] Analytics: Plan Trend")
|
|
|
+r = client.get(f"/api/analytics/plans/{plan_id}/trend?param_key=Airgap&metric_key=tavg_nm")
|
|
|
+test("trend returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
|
|
|
+trend = r.json()
|
|
|
+test("trend has points", "points" in trend and len(trend["points"]) >= 3)
|
|
|
+test("trend points sorted by x", all(trend["points"][i][0] <= trend["points"][i+1][0] for i in range(len(trend["points"])-1)))
|
|
|
+
|
|
|
+# 12. Analytics: Pareto frontier
|
|
|
+print("\n[12] Analytics: Pareto Frontier")
|
|
|
+r = client.get(f"/api/analytics/plans/{plan_id}/pareto?x_metric=total_losses_w&y_metric=efficiency_pct")
|
|
|
+test("pareto returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
|
|
|
+pareto = r.json()
|
|
|
+test("pareto has all_points", "all_points" in pareto)
|
|
|
+test("pareto has pareto_points", "pareto_points" in pareto)
|
|
|
+test("pareto total count = 4", pareto.get("total_count") == 4, f"count={pareto.get('total_count')}")
|
|
|
+
|
|
|
+# 13. Analytics: sensitivity
|
|
|
+print("\n[13] Analytics: Parameter Sensitivity")
|
|
|
+r = client.get(f"/api/analytics/plans/{plan_id}/sensitivity?metric_key=tavg_nm")
|
|
|
+test("sensitivity returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
|
|
|
+sens = r.json()
|
|
|
+test("sensitivity has items", "items" in sens)
|
|
|
+if sens.get("items"):
|
|
|
+ test("sensitivity sorted by abs_correlation desc",
|
|
|
+ all(sens["items"][i]["abs_correlation"] >= sens["items"][i+1]["abs_correlation"]
|
|
|
+ for i in range(len(sens["items"])-1)))
|
|
|
+
|
|
|
+# 14. Analytics: project overview
|
|
|
+print("\n[14] Analytics: Project Overview")
|
|
|
+r = client.get(f"/api/analytics/projects/{project_id}/overview")
|
|
|
+test("project overview returns 200", r.status_code == 200, f"status={r.status_code}, body={r.text}")
|
|
|
+overview = r.json()
|
|
|
+test("overview has total_plans", overview.get("total_plans") >= 1)
|
|
|
+test("overview has total_results", overview.get("total_results") >= 5)
|
|
|
+test("overview has ok_results", overview.get("ok_results") >= 4)
|
|
|
+test("overview has best_efficiency_pct", "best_efficiency_pct" in overview)
|
|
|
+test("overview has best_torque_nm", "best_torque_nm" in overview)
|
|
|
+
|
|
|
+# 15. Metric definitions
|
|
|
+print("\n[15] Metric Definitions")
|
|
|
+r = client.get("/api/analytics/metrics")
|
|
|
+test("metrics returns 200", r.status_code == 200)
|
|
|
+metrics = r.json().get("metrics", [])
|
|
|
+test("at least 10 metrics", len(metrics) >= 10, f"count={len(metrics)}")
|
|
|
+test("metric has key/label/unit", all("key" in m and "label" in m and "unit" in m for m in metrics))
|
|
|
+
|
|
|
+# 16. Delete test cases (cleanup)
|
|
|
+print("\n[16] Cleanup: Delete Test Cases")
|
|
|
+r = client.get("/api/experience?limit=50")
|
|
|
+test_cases = [c for c in r.json().get("items", []) if "integration-test" in c.get("tags", [])]
|
|
|
+deleted = 0
|
|
|
+for c in test_cases:
|
|
|
+ r = client.delete(f"/api/experience/{c['id']}")
|
|
|
+ if r.status_code == 204:
|
|
|
+ deleted += 1
|
|
|
+test(f"deleted {len(test_cases)} test cases", deleted == len(test_cases), f"deleted={deleted}")
|
|
|
+
|
|
|
+# Summary
|
|
|
+print("\n" + "=" * 60)
|
|
|
+print(f"TEST SUMMARY: {passed} passed, {failed} failed")
|
|
|
+print("=" * 60)
|
|
|
+
|
|
|
+if failed > 0:
|
|
|
+ sys.exit(1)
|
|
|
+else:
|
|
|
+ print("\nAll integration tests passed!")
|
|
|
+ sys.exit(0)
|