test_p3_m4_contract.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """P3-M4 regression: unified task contract + batch_scheduler field alignment.
  2. Run: python scripts/test_p3_m4_contract.py (exit 0 = PASS)
  3. Uses an isolated temp state file for the BatchScheduler (never touches the
  4. real output/scheduler_state.json). No web server or Motor-CAD required.
  5. """
  6. import os
  7. import sys
  8. import tempfile
  9. sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "web", "backend"))
  10. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  11. # ---- 1) task_contract: status normalization ----
  12. from app.services.task_contract import ( # noqa: E402
  13. normalize_status,
  14. is_terminal,
  15. merge_adaptive_fields,
  16. TASK_STATUS_PENDING,
  17. TASK_STATUS_COMPLETED,
  18. )
  19. assert normalize_status("queued") == TASK_STATUS_PENDING, normalize_status("queued")
  20. assert normalize_status("pending") == TASK_STATUS_PENDING
  21. assert normalize_status("completed_with_errors") == TASK_STATUS_COMPLETED
  22. assert normalize_status("canceled") == "cancelled"
  23. assert normalize_status("mystery") == "mystery" # pass through
  24. assert is_terminal("completed") and is_terminal("failed") and is_terminal("cancelled")
  25. assert not is_terminal("pending") and not is_terminal("queued")
  26. print("[1] task_contract status normalization OK")
  27. # ---- 2) task_contract: adaptive field merge ----
  28. merged = merge_adaptive_fields({}, task_type="adaptive_batch", loop_id="L1",
  29. batch_id=2, point_ids=[1, 2, 3], dynamic=True)
  30. assert merged["task_type"] == "adaptive_batch"
  31. assert merged["loop_id"] == "L1" and merged["batch_id"] == 2
  32. assert merged["point_ids"] == [1, 2, 3] and merged["dynamic"] is True
  33. defaults = merge_adaptive_fields({})
  34. assert defaults["task_type"] == "scan" and defaults["dynamic"] is False
  35. assert defaults["point_ids"] == [] and defaults["loop_id"] is None
  36. print("[2] task_contract adaptive field merge OK")
  37. # ---- 3) BatchScheduler: adaptive fields on add_task + summary ----
  38. from app.services.batch_scheduler import BatchScheduler # noqa: E402
  39. tmp_state = os.path.join(tempfile.mkdtemp(), "sched.json")
  40. sch = BatchScheduler(state_file=tmp_state)
  41. t = sch.add_task(
  42. task_id="batch-1", task_name="adaptive-L1-b0", task_type="adaptive_batch",
  43. loop_id="L1", batch_id=0, point_ids=[10, 11], dynamic=True,
  44. parameters=[{"airgap_mm": 1.0, "point_id": 10}],
  45. )
  46. assert t["task_type"] == "adaptive_batch", t
  47. assert t["loop_id"] == "L1" and t["batch_id"] == 0, t
  48. assert t["point_ids"] == [10, 11] and t["dynamic"] is True, t
  49. assert t["status"] == "queued", t
  50. # legacy call still works (no new args)
  51. t2 = sch.add_task(task_id="scan-1", task_name="plain")
  52. assert t2["task_type"] == "scan" and t2["dynamic"] is False, t2
  53. # get_next_task transitions queued -> running
  54. nxt = sch.get_next_task()
  55. assert nxt is not None and nxt["task_id"] == "batch-1"
  56. assert nxt["status"] == "running" and nxt["loop_id"] == "L1"
  57. sch.complete_task("batch-1")
  58. # get_next_task picks the legacy scan task (wait_for already satisfied)
  59. nxt2 = sch.get_next_task()
  60. assert nxt2 is not None and nxt2["task_id"] == "scan-1", nxt2
  61. sch.complete_task("scan-1")
  62. # statistics summary exposes new fields
  63. stats = sch.get_statistics()
  64. summaries = stats["recent_completed"] + stats["running_tasks"] + stats["queued_tasks"]
  65. assert any(s.get("task_id") == "batch-1" and s.get("task_type") == "adaptive_batch"
  66. and s.get("loop_id") == "L1" for s in summaries), stats
  67. print("[3] BatchScheduler adaptive fields aligned OK")
  68. # ---- 4) persist / reload keeps fields ----
  69. sch2 = BatchScheduler(state_file=tmp_state)
  70. assert os.path.exists(tmp_state)
  71. print("[4] scheduler state file exists:", os.path.exists(tmp_state))
  72. print("\nALL P3-M4 CONTRACT TESTS PASSED")