test_internal_e2e.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import json
  2. from pathlib import Path
  3. import httpx
  4. import pytest
  5. from script_build_host.domain.records import BuildStatus
  6. from script_build_host.internal_e2e import _wait_for_root
  7. def test_recovery_checkpoint_fixtures_cover_all_segment_boundaries() -> None:
  8. path = Path(__file__).parent / "fixtures" / "recovery_checkpoints.json"
  9. payload = json.loads(path.read_text(encoding="utf-8"))
  10. assert set(payload["checkpoints"]) == {"direction", "structure", "portfolio", "root"}
  11. assert all(
  12. item["input_snapshot_digest"].startswith("sha256:")
  13. for item in payload["checkpoints"].values()
  14. )
  15. @pytest.mark.asyncio
  16. async def test_mission_poll_allows_only_bounded_startup_failures() -> None:
  17. calls = 0
  18. def handler(_request: httpx.Request) -> httpx.Response:
  19. nonlocal calls
  20. calls += 1
  21. if calls <= 5:
  22. return httpx.Response(500, json={"error_code": "LEDGER_NOT_READY"})
  23. return httpx.Response(
  24. 200,
  25. json={
  26. "root_task_id": "root",
  27. "tasks": [
  28. {
  29. "task_id": "root",
  30. "status": "blocked",
  31. "blocked_reason": "BOUNDARY",
  32. }
  33. ],
  34. },
  35. )
  36. async with httpx.AsyncClient(
  37. transport=httpx.MockTransport(handler), base_url="http://internal"
  38. ) as client:
  39. result = await _wait_for_root(
  40. client,
  41. 7,
  42. expected_status="blocked",
  43. expected_reason="BOUNDARY",
  44. timeout_seconds=1,
  45. poll_seconds=0,
  46. )
  47. assert result["task_id"] == "root"
  48. assert calls == 6
  49. @pytest.mark.asyncio
  50. async def test_mission_poll_surfaces_persistent_server_failure() -> None:
  51. def handler(_request: httpx.Request) -> httpx.Response:
  52. return httpx.Response(500, json={"error_code": "InternalServerError"})
  53. async with httpx.AsyncClient(
  54. transport=httpx.MockTransport(handler), base_url="http://internal"
  55. ) as client:
  56. with pytest.raises(RuntimeError, match="mission poll failed with HTTP 500"):
  57. await _wait_for_root(
  58. client,
  59. 7,
  60. expected_status="blocked",
  61. expected_reason="BOUNDARY",
  62. timeout_seconds=1,
  63. poll_seconds=0,
  64. )
  65. @pytest.mark.asyncio
  66. async def test_mission_poll_stops_when_build_is_terminal() -> None:
  67. class FailedState:
  68. async def get_status(self, _script_build_id):
  69. return BuildStatus.FAILED
  70. def handler(_request: httpx.Request) -> httpx.Response:
  71. return httpx.Response(
  72. 200,
  73. json={
  74. "root_task_id": "root",
  75. "tasks": [{"task_id": "root", "status": "needs_replan"}],
  76. },
  77. )
  78. async with httpx.AsyncClient(
  79. transport=httpx.MockTransport(handler), base_url="http://internal"
  80. ) as client:
  81. with pytest.raises(RuntimeError, match="terminal status=failed"):
  82. await _wait_for_root(
  83. client,
  84. 7,
  85. legacy_state=FailedState(),
  86. expected_status="blocked",
  87. expected_reason="BOUNDARY",
  88. timeout_seconds=1,
  89. poll_seconds=0,
  90. )