test_host_client.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import asyncio
  2. from pathlib import Path
  3. import httpx
  4. from app.host_client import HostClient
  5. def test_backend_has_no_local_runtime_file_dependency() -> None:
  6. source = "\n".join(
  7. path.read_text(encoding="utf-8")
  8. for path in (Path(__file__).parents[1] / "app").glob("*.py")
  9. )
  10. assert "SCRIPT_BUILD_AGENT_DATA_ROOT" not in source
  11. assert ".local" not in source
  12. def test_host_client_reads_authoritative_api_and_forwards_only_auth_headers() -> None:
  13. async def handler(request: httpx.Request) -> httpx.Response:
  14. assert request.url.path == "/api/pattern/script_builds"
  15. assert request.headers["authorization"] == "Bearer test"
  16. assert "x-secret-debug" not in request.headers
  17. return httpx.Response(200, json={"items": []})
  18. client = HostClient(
  19. "http://host.test",
  20. transport=httpx.MockTransport(handler),
  21. )
  22. async def run() -> dict:
  23. try:
  24. return await client.list_runs(
  25. {"authorization": "Bearer test", "x-secret-debug": "do-not-forward"}
  26. )
  27. finally:
  28. await client.aclose()
  29. payload = asyncio.run(run())
  30. assert payload == {"items": []}
  31. def test_journey_reads_incrementally_and_caches_immutable_inputs() -> None:
  32. calls: list[str] = []
  33. async def handler(request: httpx.Request) -> httpx.Response:
  34. path = request.url.path
  35. calls.append(str(request.url))
  36. if path.endswith("/mission"):
  37. return httpx.Response(
  38. 200,
  39. json={
  40. "tasks": [
  41. {
  42. "task_id": "task-1",
  43. "current_spec": {
  44. "version": 1,
  45. "context_refs": ["script-build://task-contracts/sha256/contract-1"],
  46. },
  47. }
  48. ]
  49. },
  50. )
  51. if path.endswith("/input-summary"):
  52. return httpx.Response(200, json={"input_snapshot_id": "snapshot-1"})
  53. if path.endswith("/mission/events"):
  54. if request.url.params.get("after"):
  55. return httpx.Response(200, json={"events": [], "next_cursor": "cursor-1"})
  56. return httpx.Response(
  57. 200,
  58. json={
  59. "events": [{"sequence": 1}],
  60. "next_cursor": "cursor-1",
  61. },
  62. )
  63. if path.endswith("/traces"):
  64. return httpx.Response(
  65. 200,
  66. json={"traces": [{"trace_id": "root-1", "status": "running"}]},
  67. )
  68. if path.endswith("/contract"):
  69. return httpx.Response(200, json={"task_id": "task-1", "task_kind": "structure"})
  70. if path.endswith("/messages"):
  71. after = int(request.url.params.get("after_sequence", "0"))
  72. messages = [{"sequence": 1, "role": "assistant"}] if after == 0 else []
  73. return httpx.Response(200, json={"messages": messages})
  74. raise AssertionError(path)
  75. async def run() -> tuple[dict, dict]:
  76. client = HostClient("http://host.test", transport=httpx.MockTransport(handler))
  77. try:
  78. first = await client.journey_source(7, {})
  79. second = await client.journey_source(7, {})
  80. return first, second
  81. finally:
  82. await client.aclose()
  83. first, second = asyncio.run(run())
  84. assert first["events"] == second["events"] == [{"sequence": 1}]
  85. assert first["messages"] == second["messages"]
  86. assert sum("input-summary" in value for value in calls) == 1
  87. assert sum(value.endswith("/contract") for value in calls) == 1
  88. assert any("after_sequence=1" in value for value in calls)