VLM router (POST /vlm/analyze):
- Proxies to Spark 2 (Qwen3-VL via vLLM, OpenAI-compatible /v1/chat/completions)
- Port of BTE Svrnty.Bte.Domain/Features/AssetContext/OpenAiVlmClient.cs
+ VlmRubric.cs (rubric prompt builder + score parser)
- Anthropic dialect intentionally dropped — sovereign-only
- New rubric_mode="raw" passes brand_context through verbatim so BTE
ExtractBrandSaga / ImageSetSourceReader (extraction-style prompts that
expect their own JSON schema) get unwrapped JSON back without losing
the score-axis path
FLUX router (POST /flux/render):
- Proxies to Spark 1 (FLUX.2-dev on ComfyUI; /prompt + /history poll + /view)
- Port of SparkBComfyClient.cs + LocalFluxImageProvider.cs + StopgapFluxWorkflow.cs
- Accepts a pre-assembled workflow_json (BTE IRecipeAssembler emits one)
or builds the stopgap FLUX.2 graph from prompt + dims
Tests (pytest):
- test_vlm_parse.py — rubric prompt + score parse, 502 on Spark-down, mocked round-trip
- test_flux_workflow.py — stopgap graph shape, seed variance/determinism, 502 on Spark-down
- test_healthz.py updated (palette/rembg still 4a stubs)
16 pytest tests green.
Smoke (no Spark reachable):
- GET /healthz → 200 {"status":"ok"}
- POST /vlm/analyze → 502 "Spark 2 unreachable" (clear error)
- POST /flux/render → 502 "Spark 1 unreachable" (clear error)
Per BTE refactor audit §3 V — vision capabilities extracted from BTE to the
sovereign vision gateway. Phase 4c (delete-from-BTE) + Phase 4d (HTTP adapter)
follow in BTE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""Pytest port of BTE's StopgapFluxWorkflowTests + smoke for /flux/render."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import patch
|
|
|
|
import httpx
|
|
from fastapi.testclient import TestClient
|
|
|
|
from svrnty_vision.routers.flux import build_stopgap_workflow
|
|
from svrnty_vision.server import app
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
def test_stopgap_workflow_includes_prompt_and_dimensions() -> None:
|
|
raw = build_stopgap_workflow("a sunlit plate of food", 1024, 1024)
|
|
assert "a sunlit plate of food" in raw
|
|
graph = json.loads(raw)
|
|
assert graph["5"]["inputs"]["width"] == 1024
|
|
assert graph["5"]["inputs"]["height"] == 1024
|
|
assert graph["10"]["inputs"]["vae_name"] == "flux2-vae.safetensors"
|
|
assert graph["11"]["inputs"]["type"] == "flux2"
|
|
assert graph["12"]["inputs"]["unet_name"] == "flux2_dev_fp8mixed.safetensors"
|
|
|
|
|
|
def test_stopgap_workflow_seeds_vary_per_call() -> None:
|
|
"""ComfyUI dedupes identical workflows (execution_cached → empty outputs)."""
|
|
a = build_stopgap_workflow("x", 512, 512)
|
|
b = build_stopgap_workflow("x", 512, 512)
|
|
assert a != b
|
|
|
|
|
|
def test_stopgap_workflow_uses_explicit_seed_when_supplied() -> None:
|
|
a = build_stopgap_workflow("x", 512, 512, seed=42)
|
|
b = build_stopgap_workflow("x", 512, 512, seed=42)
|
|
assert a == b
|
|
|
|
|
|
def test_render_requires_workflow_or_prompt() -> None:
|
|
response = client.post("/flux/render", json={"width": 512, "height": 512})
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_render_returns_502_when_spark1_unreachable() -> None:
|
|
class _StubClient:
|
|
def __init__(self, *a, **kw):
|
|
pass
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *a):
|
|
return False
|
|
|
|
async def post(self, *a, **kw):
|
|
raise httpx.ConnectError("no comfy")
|
|
|
|
async def get(self, *a, **kw):
|
|
raise httpx.ConnectError("no comfy")
|
|
|
|
with patch("svrnty_vision.routers.flux.httpx.AsyncClient", _StubClient):
|
|
response = client.post(
|
|
"/flux/render",
|
|
json={"prompt": "test", "width": 512, "height": 512},
|
|
)
|
|
assert response.status_code == 502
|