mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 19:32:21 +02:00
Server-side photo editor: natural-language editor agent route
(/api/editor/agent), photo_editor tool family + style guide, image-op
provider proxy with progress, email server hardening, gallery/hwfit
routes, and graceful shutdown.
Source: vendor/photocraft (ad1cff44) editor-server files, merged onto
upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/B_conflicts.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
"""Tests for the image-op progress contract (HARNESS-owned).
|
|
|
|
The 7011 side accepts an optional client-supplied ``request_id`` on heavy image
|
|
ops and exposes ``GET /api/image/edit-progress`` which proxies the provider's
|
|
``/v1/images/progress/{request_id}`` route. When the provider has no such route
|
|
the endpoint returns ``{"available": false}`` so the client keeps an
|
|
indeterminate bar.
|
|
"""
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
import httpx
|
|
|
|
import routes.gallery_routes as gallery
|
|
|
|
|
|
def _route(path):
|
|
return next(r for r in gallery.setup_gallery_routes().routes if r.path == path)
|
|
|
|
|
|
class _Req:
|
|
def __init__(self, query):
|
|
self.query_params = query
|
|
|
|
|
|
def _fake_db():
|
|
class Db:
|
|
def close(self):
|
|
pass
|
|
|
|
def query(self, *args, **kwargs):
|
|
return self
|
|
|
|
def filter(self, *args, **kwargs):
|
|
return self
|
|
|
|
def first(self):
|
|
return None
|
|
|
|
return Db()
|
|
|
|
|
|
def test_edit_progress_requires_request_id(monkeypatch):
|
|
monkeypatch.setattr(gallery, "require_privilege", lambda *a: "alice")
|
|
route = _route("/api/image/edit-progress")
|
|
try:
|
|
asyncio.run(route.endpoint(_Req({})))
|
|
except Exception as exc: # FastAPI HTTPException
|
|
assert getattr(exc, "status_code", None) == 400
|
|
else:
|
|
raise AssertionError("expected a 400 for a missing request_id")
|
|
|
|
|
|
def test_edit_progress_rejects_an_unsafe_request_id(monkeypatch):
|
|
monkeypatch.setattr(gallery, "require_privilege", lambda *a: "alice")
|
|
monkeypatch.setattr(gallery, "SessionLocal", lambda: _fake_db())
|
|
route = _route("/api/image/edit-progress")
|
|
try:
|
|
asyncio.run(route.endpoint(_Req({"request_id": "../secret"})))
|
|
except Exception as exc:
|
|
assert getattr(exc, "status_code", None) == 400
|
|
else:
|
|
raise AssertionError("expected a 400 for an unsafe request_id")
|
|
|
|
|
|
def test_edit_progress_returns_provider_payload(monkeypatch):
|
|
endpoint = SimpleNamespace(base_url="http://100.69.120.65:8188/v1", api_key="k")
|
|
monkeypatch.setattr(gallery, "SessionLocal", lambda: _fake_db())
|
|
monkeypatch.setattr(gallery, "require_privilege", lambda *a: "alice")
|
|
monkeypatch.setattr(gallery, "_first_visible_image_endpoint", lambda *a: endpoint)
|
|
seen = {}
|
|
|
|
def respond(request):
|
|
seen["url"] = str(request.url)
|
|
# Live seneca contract stages: loading | editing | compositing | done | error.
|
|
return httpx.Response(
|
|
200,
|
|
json={"stage": "editing", "step": 3, "steps": 8, "eta_s": 42.0, "progress": 0.375},
|
|
)
|
|
|
|
client = httpx.AsyncClient
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda **k: client(transport=httpx.MockTransport(respond), **k))
|
|
out = asyncio.run(_route("/api/image/edit-progress").endpoint(_Req({"request_id": "abc-123"})))
|
|
assert out["available"] is True
|
|
assert out["stage"] == "editing" # passed through unchanged
|
|
assert out["step"] == 3 and out["steps"] == 8
|
|
assert seen["url"].endswith("/images/progress/abc-123")
|
|
|
|
|
|
def test_edit_progress_available_false_when_provider_has_no_route(monkeypatch):
|
|
endpoint = SimpleNamespace(base_url="http://100.69.120.65:8188/v1", api_key="k")
|
|
monkeypatch.setattr(gallery, "SessionLocal", lambda: _fake_db())
|
|
monkeypatch.setattr(gallery, "require_privilege", lambda *a: "alice")
|
|
monkeypatch.setattr(gallery, "_first_visible_image_endpoint", lambda *a: endpoint)
|
|
|
|
client = httpx.AsyncClient
|
|
monkeypatch.setattr(
|
|
httpx, "AsyncClient",
|
|
lambda **k: client(transport=httpx.MockTransport(lambda req: httpx.Response(404)), **k),
|
|
)
|
|
out = asyncio.run(_route("/api/image/edit-progress").endpoint(_Req({"request_id": "abc"})))
|
|
assert out["available"] is False
|
|
|
|
|
|
def test_edit_progress_available_false_when_unreachable(monkeypatch):
|
|
endpoint = SimpleNamespace(base_url="http://100.69.120.65:8188/v1", api_key="k")
|
|
monkeypatch.setattr(gallery, "SessionLocal", lambda: _fake_db())
|
|
monkeypatch.setattr(gallery, "require_privilege", lambda *a: "alice")
|
|
monkeypatch.setattr(gallery, "_first_visible_image_endpoint", lambda *a: endpoint)
|
|
|
|
class Boom:
|
|
async def __aenter__(self):
|
|
raise httpx.ConnectError("nope")
|
|
|
|
async def __aexit__(self, *a):
|
|
return False
|
|
|
|
monkeypatch.setattr(httpx, "AsyncClient", lambda **k: Boom())
|
|
out = asyncio.run(_route("/api/image/edit-progress").endpoint(_Req({"request_id": "abc"})))
|
|
assert out["available"] is False
|