mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
The settings panel collapsed four causes into one message -- "the picked preset's own values could not be read" -- with no indication of what to do about it. The overwhelmingly common cause has an obvious fix, and it isn't an edge case: an install pulls its sidecar as SIDECAR_TAG:-latest regardless of which Bambuddy channel it is on, so a current Bambuddy talking to a sidecar that predates POST /profiles/resolve is the normal state, not a misconfiguration. Those users would have seen an amber warning on every slice with nothing pointing at the sidecar image. resolve_profile now returns ResolvedProfile(values, reason) instead of None for everything, the route passes the reason through, and the panel picks its message from it: sidecar_outdated -> name the fix: update the sidecar image sidecar_unavailable -> the sidecar did not answer not_configured -> no sidecar is configured preset_unresolved -> the previous generic wording A request that fails outright maps to sidecar_unavailable, since a backend we cannot reach and a sidecar that will not answer are the same thing from the dialog. Every variant still ends with "anything you don't change still uses the preset" -- that reassurance is the point of the notice, and it is true whichever way the lookup failed. Tests pin the distinction rather than just the happy path: a 404 and a 500 must produce different reasons, and each panel case asserts both that its own message appears and that the "update the sidecar image" line does not leak into the others.
98 lines
4.4 KiB
Python
98 lines
4.4 KiB
Python
"""Tests for resolving a preset's effective values via the sidecar.
|
|
|
|
The slice modal's settings panel needs the values a preset actually sets, not
|
|
the option schema's compiled-in defaults. Only the sidecar can answer that: a
|
|
"Standard" pick is a ``{inherits: ...}`` stub on our side, and local/cloud
|
|
presets are deltas whose remainder lives in the sidecar's bundled profile tree.
|
|
"""
|
|
|
|
import json
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from backend.app.services.slicer_api import SlicerApiService, SlicerApiUnavailableError
|
|
|
|
PROCESS_STUB = json.dumps({"inherits": "0.20mm Standard @BBL X1C", "from": "system"})
|
|
|
|
|
|
def _service(handler) -> SlicerApiService:
|
|
transport = httpx.MockTransport(handler)
|
|
client = httpx.AsyncClient(transport=transport)
|
|
return SlicerApiService("http://sidecar:3003", client=client)
|
|
|
|
|
|
class TestResolveProfile:
|
|
@pytest.mark.asyncio
|
|
async def test_returns_the_flattened_values(self):
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
assert request.url.path == "/profiles/resolve"
|
|
body = json.loads(request.content)
|
|
assert body["category"] == "process"
|
|
# The stub goes out as an object, not a JSON string.
|
|
assert body["profile"]["inherits"] == "0.20mm Standard @BBL X1C"
|
|
return httpx.Response(200, json={"profile": {"line_width": "0.42", "wall_loops": "2"}})
|
|
|
|
service = _service(handler)
|
|
result = await service.resolve_profile(PROCESS_STUB, "process")
|
|
assert result.values == {"line_width": "0.42", "wall_loops": "2"}
|
|
assert result.reason == "ok"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_sidecar_without_the_endpoint_is_reported_as_outdated(self):
|
|
# Older images 404 here. This is the dominant case in practice -- an
|
|
# install pulls SIDECAR_TAG:-latest regardless of its own release
|
|
# channel -- and it is the one with a fix the user can act on, so it
|
|
# must not be flattened into the generic failure.
|
|
service = _service(lambda request: httpx.Response(404, json={"message": "Not Found"}))
|
|
result = await service.resolve_profile(PROCESS_STUB, "process")
|
|
assert result.values is None
|
|
assert result.reason == "sidecar_outdated"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_sidecar_error_is_not_reported_as_outdated(self):
|
|
# A broken sidecar and an old one call for different advice.
|
|
service = _service(lambda request: httpx.Response(500, json={"message": "boom"}))
|
|
result = await service.resolve_profile(PROCESS_STUB, "process")
|
|
assert result.values is None
|
|
assert result.reason == "sidecar_unavailable"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unreachable_sidecar_still_raises(self):
|
|
# Distinct from "too old": the caller reports this as slicing being
|
|
# unavailable rather than silently showing defaults forever.
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
raise httpx.ConnectError("refused")
|
|
|
|
with pytest.raises(SlicerApiUnavailableError):
|
|
await _service(handler).resolve_profile(PROCESS_STUB, "process")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unparseable_preset_content_blames_the_preset(self):
|
|
service = _service(lambda request: httpx.Response(200, json={"profile": {}}))
|
|
result = await service.resolve_profile("not json", "process")
|
|
assert result.values is None
|
|
assert result.reason == "preset_unresolved"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_response_without_a_profile_object_returns_no_values(self):
|
|
# Guards against reading a differently-shaped body as if it were values.
|
|
service = _service(lambda request: httpx.Response(200, json={"ok": True}))
|
|
result = await service.resolve_profile(PROCESS_STUB, "process")
|
|
assert result.values is None
|
|
assert result.reason == "sidecar_unavailable"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_an_already_flat_preset_round_trips(self):
|
|
flat = json.dumps({"line_width": "0.45", "type": "process"})
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
body = json.loads(request.content)
|
|
assert "inherits" not in body["profile"]
|
|
return httpx.Response(200, json={"profile": json.loads(flat)})
|
|
|
|
assert (await _service(handler).resolve_profile(flat, "process")).values == {
|
|
"line_width": "0.45",
|
|
"type": "process",
|
|
}
|