test_plan_schema.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """Tests for plan_schema module - especially string enum parameter support.
  2. Covers:
  3. - FixedParam with string enum values (Winding_Connection, Cooling_Type, etc.)
  4. - Validation of plans with mixed numeric/string params
  5. - generate_full_params preserves string values
  6. - Boundary cases (empty, int, float, str values)
  7. Run: python scripts/test_plan_schema.py
  8. Exit 0 = PASS, non-zero = FAIL.
  9. """
  10. import sys
  11. import os
  12. sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
  13. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "web", "backend"))
  14. from src.plan_schema import (
  15. FixedParam,
  16. ScanVariable,
  17. SimulationPlan,
  18. validate_plan_dict,
  19. parse_plan,
  20. )
  21. def _make_plan(fixed_params, variables=None):
  22. """Helper to create a minimal valid plan dict."""
  23. return {
  24. "plan_id": "test-plan",
  25. "plan_version": "2.0",
  26. "created_at": "2026-08-30T12:00:00",
  27. "topology": "SSSR",
  28. "model_path": "models/test.mot",
  29. "fixed_params": fixed_params,
  30. "variables": variables or [
  31. {"name": "Magnet_Thickness", "values": [2, 3], "unit": "mm"},
  32. ],
  33. "cases": [{"id": "default", "name": "Default", "params": {}}],
  34. "output_metrics": ["tavg_nm", "efficiency_pct"],
  35. }
  36. def test_fixed_param_string_enum():
  37. """Test that FixedParam accepts string enum values.
  38. Regression test for the AI generate-and-save 422 error:
  39. Winding_Connection="Star" caused 'could not convert string to float'.
  40. """
  41. fp = FixedParam.from_dict({
  42. "name": "Winding_Connection",
  43. "value": "Star",
  44. "unit": "",
  45. })
  46. assert fp.value == "Star"
  47. assert isinstance(fp.value, str)
  48. print("PASS: test_fixed_param_string_enum")
  49. def test_fixed_param_numeric_values():
  50. """Test that numeric values still work correctly."""
  51. fp_int = FixedParam.from_dict({"name": "Slot_Number", "value": 12})
  52. assert fp_int.value == 12
  53. assert isinstance(fp_int.value, int)
  54. fp_float = FixedParam.from_dict({"name": "Airgap", "value": 1.5})
  55. assert fp_float.value == 1.5
  56. assert isinstance(fp_float.value, float)
  57. print("PASS: test_fixed_param_numeric_values")
  58. def test_fixed_param_to_dict_roundtrip():
  59. """Test that string values survive to_dict -> from_dict roundtrip."""
  60. original = FixedParam(name="Cooling_Type", value="Natural Convection", unit="")
  61. d = original.to_dict()
  62. assert d["value"] == "Natural Convection"
  63. restored = FixedParam.from_dict(d)
  64. assert restored.value == "Natural Convection"
  65. assert isinstance(restored.value, str)
  66. print("PASS: test_fixed_param_to_dict_roundtrip")
  67. def test_validate_plan_with_string_enums():
  68. """Test that a plan with mixed numeric/string fixed params validates OK."""
  69. plan = _make_plan([
  70. {"name": "Airgap", "value": 1.0, "unit": "mm"},
  71. {"name": "Winding_Connection", "value": "Star", "unit": ""},
  72. {"name": "Cooling_Type", "value": "Natural Convection", "unit": ""},
  73. {"name": "Slot_Number", "value": 12, "unit": ""},
  74. ])
  75. ok, errors = validate_plan_dict(plan, require_model_path=False)
  76. assert ok, f"Plan should validate OK, got errors: {errors}"
  77. assert len(errors) == 0
  78. print("PASS: test_validate_plan_with_string_enums")
  79. def test_generate_full_params_preserves_strings():
  80. """Test that generate_full_params preserves string enum values in all points."""
  81. plan = _make_plan([
  82. {"name": "Airgap", "value": 1.0, "unit": "mm"},
  83. {"name": "Winding_Connection", "value": "Star", "unit": ""},
  84. ])
  85. parsed = parse_plan(plan)
  86. points = parsed.generate_full_params()
  87. assert len(points) == 2 # 2 variable values
  88. for pt in points:
  89. assert pt["Winding_Connection"] == "Star", "String enum must be preserved"
  90. assert pt["Airgap"] == 1.0
  91. assert "Magnet_Thickness" in pt
  92. print("PASS: test_generate_full_params_preserves_strings")
  93. def test_empty_fixed_params():
  94. """Test plan with no fixed params (boundary case)."""
  95. plan = _make_plan([])
  96. ok, errors = validate_plan_dict(plan, require_model_path=False)
  97. assert ok
  98. parsed = parse_plan(plan)
  99. points = parsed.generate_full_params()
  100. assert len(points) == 2
  101. print("PASS: test_empty_fixed_params")
  102. def test_ai_generated_plan_regression():
  103. """Full regression test simulating the exact AI-generated plan that failed.
  104. The AI generator produces Winding_Connection as a string enum. This test
  105. ensures such plans can be parsed, validated, and expanded without the
  106. 'could not convert string to float' error.
  107. """
  108. ai_plan = {
  109. "plan_id": "ai-generated-test",
  110. "plan_version": "2.0",
  111. "created_at": "2026-08-30T12:00:00",
  112. "topology": "SSSR",
  113. "model_path": "models/MARS-12S10P_SSSR.mot",
  114. "fixed_params": [
  115. {"name": "Outer_Rotor_Diameter", "value": 100.0, "unit": "mm"},
  116. {"name": "Inner_Rotor_Diameter", "value": 40.0, "unit": "mm"},
  117. {"name": "Airgap", "value": 1.0, "unit": "mm"},
  118. {"name": "Slot_Number", "value": 12, "unit": ""},
  119. {"name": "Pole_Number", "value": 10, "unit": ""},
  120. {"name": "Winding_Connection", "value": "Star", "unit": ""},
  121. {"name": "Cooling_Type", "value": "Natural Convection", "unit": ""},
  122. {"name": "CurrentDefinition", "value": "Peak", "unit": ""},
  123. {"name": "DCBusVoltage", "value": 48.0, "unit": "V"},
  124. ],
  125. "variables": [
  126. {"name": "Magnet_Thickness", "values": [3, 4, 5], "unit": "mm"},
  127. {"name": "Stator_Outer_Diameter", "values": [80, 90, 100], "unit": "mm"},
  128. ],
  129. "cases": [{"id": "default", "name": "Default", "params": {}}],
  130. "output_metrics": ["tavg_nm", "ripple_pct", "efficiency_pct"],
  131. }
  132. # 1. Parse should not raise
  133. parsed = parse_plan(ai_plan)
  134. assert len(parsed.fixed_params) == 9
  135. assert len(parsed.variables) == 2
  136. # 2. Validate should pass
  137. ok, errors = validate_plan_dict(ai_plan, require_model_path=False)
  138. assert ok, f"Validation failed: {errors}"
  139. # 3. Generate points should preserve string enums
  140. points = parsed.generate_full_params()
  141. assert len(points) == 9 # 3 * 3
  142. for pt in points:
  143. assert pt["Winding_Connection"] == "Star"
  144. assert pt["Cooling_Type"] == "Natural Convection"
  145. assert pt["CurrentDefinition"] == "Peak"
  146. assert isinstance(pt["Outer_Rotor_Diameter"], float)
  147. assert isinstance(pt["Slot_Number"], int)
  148. print("PASS: test_ai_generated_plan_regression")
  149. def main():
  150. tests = [
  151. test_fixed_param_string_enum,
  152. test_fixed_param_numeric_values,
  153. test_fixed_param_to_dict_roundtrip,
  154. test_validate_plan_with_string_enums,
  155. test_generate_full_params_preserves_strings,
  156. test_empty_fixed_params,
  157. test_ai_generated_plan_regression,
  158. ]
  159. passed = 0
  160. failed = 0
  161. for test in tests:
  162. try:
  163. test()
  164. passed += 1
  165. except Exception as e:
  166. print(f"FAIL: {test.__name__}: {e}")
  167. failed += 1
  168. print(f"\n{'='*50}")
  169. print(f"Results: {passed} passed, {failed} failed, {len(tests)} total")
  170. if failed > 0:
  171. sys.exit(1)
  172. else:
  173. print("ALL TESTS PASSED")
  174. sys.exit(0)
  175. if __name__ == "__main__":
  176. main()