diff --git a/photomap/backend/invokeai_client.py b/photomap/backend/invokeai_client.py index 3f04a222..bb2a428a 100644 --- a/photomap/backend/invokeai_client.py +++ b/photomap/backend/invokeai_client.py @@ -285,7 +285,18 @@ async def fetch_board_image_names( The special board id ``"none"`` is InvokeAI's Uncategorized bucket. Returned names include their file extension (``{uuid}.png`` style). Raises 502 on any network error or non-200 response. + + Canvas intermediates (region masks, staging composites) and + control/mask-category assets are excluded, matching what InvokeAI's own + gallery shows. Servers that predate these query params ignore them and + return the unfiltered list. """ + # httpx repeats list values (categories=general&categories=user), which + # is the encoding FastAPI expects for list[ImageCategory]. + filter_params = { + "is_intermediate": "false", + "categories": ["general", "user"], + } all_names: list[str] = [] try: async with httpx.AsyncClient(timeout=_BOARD_FETCH_TIMEOUT) as client: @@ -297,7 +308,7 @@ async def fetch_board_image_names( async def _do( headers: dict[str, str], url: str = names_url ) -> httpx.Response: - return await client.get(url, headers=headers) + return await client.get(url, params=filter_params, headers=headers) response = await _request_with_auth_fallback( base_url, username, password, _do diff --git a/tests/backend/test_invokeai_client.py b/tests/backend/test_invokeai_client.py new file mode 100644 index 00000000..c0c69776 --- /dev/null +++ b/tests/backend/test_invokeai_client.py @@ -0,0 +1,77 @@ +"""Tests for the raw HTTP behaviour of ``photomap.backend.invokeai_client``. + +The board-index tests (``test_invokeai_board_index.py``) monkeypatch +``fetch_board_image_names`` wholesale, so the request-building details are +covered here instead — most importantly that board fetches ask InvokeAI to +exclude canvas intermediates and mask/control assets, mirroring what the +InvokeAI gallery itself displays. +""" + +import pytest + +from photomap.backend import invokeai_client + + +class _Resp: + def __init__(self, status_code=200, json_body=None, text=""): + self.status_code = status_code + self._json = json_body if json_body is not None else [] + self.text = text + + def json(self): + return self._json + + +class _RecordingClient: + """httpx.AsyncClient stub that records each GET and returns scripted responses.""" + + def __init__(self, script): + self._script = list(script) + self.calls: list[dict] = [] + + def __call__(self, *args, **kwargs): + return self + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return False + + async def get(self, url, **kwargs): + self.calls.append({"url": url, "params": kwargs.get("params")}) + return self._script.pop(0) + + +@pytest.fixture(autouse=True) +def _clear_token_cache(): + invokeai_client._invalidate_token_cache() + yield + invokeai_client._invalidate_token_cache() + + +@pytest.mark.asyncio +async def test_fetch_board_image_names_filters_out_intermediates(monkeypatch): + """Board fetches must request only non-intermediate general/user images.""" + stub = _RecordingClient( + [ + _Resp(json_body=["aaa.png", "bbb.png"]), + _Resp(json_body=["bbb.png", "ccc.png"]), + ] + ) + monkeypatch.setattr(invokeai_client.httpx, "AsyncClient", stub) + + names = await invokeai_client.fetch_board_image_names( + "http://localhost:9090", ["board-1", "none"], None, None + ) + + assert names == ["aaa.png", "bbb.png", "ccc.png"] + assert [call["url"] for call in stub.calls] == [ + "http://localhost:9090/api/v1/boards/board-1/image_names", + "http://localhost:9090/api/v1/boards/none/image_names", + ] + for call in stub.calls: + assert call["params"] == { + "is_intermediate": "false", + "categories": ["general", "user"], + }