test_slicer_preset_values.py 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """Tests for resolving a preset's effective values via the sidecar.
  2. The slice modal's settings panel needs the values a preset actually sets, not
  3. the option schema's compiled-in defaults. Only the sidecar can answer that: a
  4. "Standard" pick is a ``{inherits: ...}`` stub on our side, and local/cloud
  5. presets are deltas whose remainder lives in the sidecar's bundled profile tree.
  6. """
  7. import json
  8. import httpx
  9. import pytest
  10. from backend.app.services.slicer_api import SlicerApiService, SlicerApiUnavailableError
  11. PROCESS_STUB = json.dumps({"inherits": "0.20mm Standard @BBL X1C", "from": "system"})
  12. def _service(handler) -> SlicerApiService:
  13. transport = httpx.MockTransport(handler)
  14. client = httpx.AsyncClient(transport=transport)
  15. return SlicerApiService("http://sidecar:3003", client=client)
  16. class TestResolveProfile:
  17. @pytest.mark.asyncio
  18. async def test_returns_the_flattened_values(self):
  19. def handler(request: httpx.Request) -> httpx.Response:
  20. assert request.url.path == "/profiles/resolve"
  21. body = json.loads(request.content)
  22. assert body["category"] == "process"
  23. # The stub goes out as an object, not a JSON string.
  24. assert body["profile"]["inherits"] == "0.20mm Standard @BBL X1C"
  25. return httpx.Response(200, json={"profile": {"line_width": "0.42", "wall_loops": "2"}})
  26. service = _service(handler)
  27. result = await service.resolve_profile(PROCESS_STUB, "process")
  28. assert result.values == {"line_width": "0.42", "wall_loops": "2"}
  29. assert result.reason == "ok"
  30. @pytest.mark.asyncio
  31. async def test_a_sidecar_without_the_endpoint_is_reported_as_outdated(self):
  32. # Older images 404 here. This is the dominant case in practice -- an
  33. # install pulls SIDECAR_TAG:-latest regardless of its own release
  34. # channel -- and it is the one with a fix the user can act on, so it
  35. # must not be flattened into the generic failure.
  36. service = _service(lambda request: httpx.Response(404, json={"message": "Not Found"}))
  37. result = await service.resolve_profile(PROCESS_STUB, "process")
  38. assert result.values is None
  39. assert result.reason == "sidecar_outdated"
  40. @pytest.mark.asyncio
  41. async def test_a_sidecar_error_is_not_reported_as_outdated(self):
  42. # A broken sidecar and an old one call for different advice.
  43. service = _service(lambda request: httpx.Response(500, json={"message": "boom"}))
  44. result = await service.resolve_profile(PROCESS_STUB, "process")
  45. assert result.values is None
  46. assert result.reason == "sidecar_unavailable"
  47. @pytest.mark.asyncio
  48. async def test_unreachable_sidecar_still_raises(self):
  49. # Distinct from "too old": the caller reports this as slicing being
  50. # unavailable rather than silently showing defaults forever.
  51. def handler(request: httpx.Request) -> httpx.Response:
  52. raise httpx.ConnectError("refused")
  53. with pytest.raises(SlicerApiUnavailableError):
  54. await _service(handler).resolve_profile(PROCESS_STUB, "process")
  55. @pytest.mark.asyncio
  56. async def test_unparseable_preset_content_blames_the_preset(self):
  57. service = _service(lambda request: httpx.Response(200, json={"profile": {}}))
  58. result = await service.resolve_profile("not json", "process")
  59. assert result.values is None
  60. assert result.reason == "preset_unresolved"
  61. @pytest.mark.asyncio
  62. async def test_a_response_without_a_profile_object_returns_no_values(self):
  63. # Guards against reading a differently-shaped body as if it were values.
  64. service = _service(lambda request: httpx.Response(200, json={"ok": True}))
  65. result = await service.resolve_profile(PROCESS_STUB, "process")
  66. assert result.values is None
  67. assert result.reason == "sidecar_unavailable"
  68. @pytest.mark.asyncio
  69. async def test_an_already_flat_preset_round_trips(self):
  70. flat = json.dumps({"line_width": "0.45", "type": "process"})
  71. def handler(request: httpx.Request) -> httpx.Response:
  72. body = json.loads(request.content)
  73. assert "inherits" not in body["profile"]
  74. return httpx.Response(200, json={"profile": json.loads(flat)})
  75. assert (await _service(handler).resolve_profile(flat, "process")).values == {
  76. "line_width": "0.45",
  77. "type": "process",
  78. }