Files
SlabUploader/backend/tests/test_inference.py
T
Ty 81be17d5cd fix(inference): METADATA-35 fit Call 1/2 images to 1024 long edge
Downscale processed PNGs only on the inference send path (keep alpha, never
upscale). Stored inventory/Woo blobs unchanged. Adds Pillow + unit/e2e-style
tests; cite tests on METADATA-35.
2026-09-20 22:32:14 -07:00

208 lines
8.4 KiB
Python

"""S7 inference tests (METADATA-105/106, 45, 55, 70, SETTINGS-45)."""
import base64
import json
import os
import shutil
import tempfile
import pytest
from fastapi.testclient import TestClient
import backend.db as db_mod
@pytest.fixture
def env(monkeypatch):
tmp = tempfile.mkdtemp(prefix="inf_")
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "i.db")
os.environ["SLAB_IMAGES_DIR"] = tmp
os.environ["WOO_BASE_URL"] = "https://x.com"
os.environ["SLAB_AES_KEY"] = base64.b64encode(b"0123456789abcdef0123456789abcdef").decode()
db_mod._reset_for_tests()
from backend.crypto import encrypt
conn = db_mod.init_db()
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_base_url',NULL,?)", (encrypt("https://infer.example.com"),))
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_api_key',NULL,?)", (encrypt("sk-test"),))
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_model','gpt-test',NULL)")
conn.commit()
from backend.app import app
yield TestClient(app), conn
db_mod._reset_for_tests()
shutil.rmtree(tmp, ignore_errors=True)
def test_prompt_seed_and_revision(env):
client, conn = env
r = client.get("/api/v1/prompts/call1")
assert r.status_code == 200
body = r.json()
assert body["active_content"] is not None
assert len(body["revisions"]) == 1 # seed only
r = client.post("/api/v1/prompts/call1/revisions", json={"content": "revised prompt v2"})
assert r.status_code == 201
new_rev = r.json()["id"]
# new revision becomes active
r = client.get("/api/v1/prompts/call1")
assert r.json()["active_revision_id"] == new_rev
assert r.json()["active_content"] == "revised prompt v2"
assert len(r.json()["revisions"]) == 2
# activate back to seed
seed_rev = 1
r = client.post("/api/v1/prompts/call1/activate", json={"revision_id": seed_rev})
assert r.status_code == 200
assert client.get("/api/v1/prompts/call1").json()["active_content"] is not None
def test_call1_confidence_gate(env, monkeypatch):
client, conn = env
# enable inference
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','true',NULL)")
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Black Walnut','black-walnut',NULL,100)")
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-01',100,2,'calibrated',1,1,'t','t')")
conn.execute("INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,X'89504E47','t')")
conn.commit()
from backend.inference import InferenceClient
def fake_chat(self, messages):
return json.dumps({
"species_id": 1, "species_confidence": 0.9,
"wood_category_ids": [1], "wood_category_confidence": 0.5, # below threshold
"grade_term_id": 7, "grade_confidence": 0.3, # below
"figure_term_ids": [2], "figure_confidence": 0.8, # at/above
})
monkeypatch.setattr(InferenceClient, "chat", fake_chat)
r = client.post("/api/v1/slabs/s1/inference/call1")
assert r.status_code == 200, r.text
s = r.json()["suggestions"]
assert s.get("species_id") == 1
assert s.get("figure_term_ids") == [2]
assert "wood_category_ids" not in s # gated out
assert "grade_term_id" not in s # gated out
# inference_log written with call=1
assert conn.execute("SELECT call FROM inference_log").fetchone()["call"] == 1
def test_call2_stores_description(env, monkeypatch):
client, conn = env
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','true',NULL)")
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,bdft,title,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-01',100,2,20,18,'Walnut Slab','ready',1,1,'t','t')")
conn.execute("INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,X'89504E47','t')")
conn.commit()
from backend.inference import InferenceClient
monkeypatch.setattr(InferenceClient, "chat", lambda self, messages: "<p>A fine walnut slab.</p>")
r = client.post("/api/v1/slabs/s1/inference/call2")
assert r.status_code == 200, r.text
assert r.json()["description"] == "<p>A fine walnut slab.</p>"
row = conn.execute("SELECT description, content_source FROM slabs WHERE id='s1'").fetchone()
assert row["description"] == "<p>A fine walnut slab.</p>"
assert row["content_source"] == "llm"
def test_inference_off_call1_409(env):
client, conn = env
conn.execute("INSERT INTO slabs(id,sku,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-01','calibrated',1,1,'t','t')")
conn.commit()
r = client.post("/api/v1/slabs/s1/inference/call1")
assert r.status_code == 409 # inference_enabled default off
def test_server_description_inference_off(env):
from backend.inference import server_description
d = server_description({"title": "Figured Walnut Slab", "length_in": 114.0, "width_avg_in": 24.0, "thickness_in": 1.75, "bdft": 18.4})
assert "114.0in" in d and "18.4" in d
def test_test_inference_unconfigured(env):
client, conn = env
conn.execute("UPDATE settings SET value_enc=NULL WHERE key='inference_api_key'")
conn.commit()
r = client.post("/api/v1/admin/test-inference")
assert r.status_code == 200
assert r.json()["ok"] is False and r.json()["mode"] == "fail"
def _rgba_png(w: int, h: int) -> bytes:
"""RGBA PNG: opaque body + transparent top-left block (capture-like alpha)."""
from PIL import Image
import io
im = Image.new("RGBA", (w, h), (10, 20, 30, 255))
for y in range(min(80, h)):
for x in range(min(80, w)):
im.putpixel((x, y), (0, 0, 0, 0))
buf = io.BytesIO()
im.save(buf, format="PNG")
return buf.getvalue()
def test_fit_long_edge_png_downscales_over_1024():
"""METADATA-35: Call 1 images long edge <= 1024; never upscale; keep alpha."""
from backend.inference import fit_long_edge_png
from PIL import Image
import io
src = _rgba_png(800, 1067) # inventory 3:4, long edge 1067
out = fit_long_edge_png(src, max_long_edge=1024)
im = Image.open(io.BytesIO(out)).convert("RGBA")
assert max(im.size) == 1024
assert min(im.size) < 1024
w, h = im.size
assert abs((w / h) - (800 / 1067)) < 0.02
# Transparent block still transparent near origin after downscale
assert im.getpixel((0, 0))[3] == 0
assert im.getpixel((5, 5))[3] == 0
def test_fit_long_edge_png_leaves_small_unchanged_dims():
from backend.inference import fit_long_edge_png
from PIL import Image
import io
src = _rgba_png(400, 300)
out = fit_long_edge_png(src, max_long_edge=1024)
im = Image.open(io.BytesIO(out))
assert im.size == (400, 300)
def test_call1_sends_long_edge_at_most_1024(env, monkeypatch):
"""METADATA-35 end-to-end: messages to the model use <=1024 long edge."""
client, conn = env
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','true',NULL)")
conn.execute(
"INSERT INTO slabs(id,sku,length_in,thickness_in,status,server_rev,client_rev,created_at,updated_at) "
"VALUES ('s1','SLAB-01',100,2,'calibrated',1,1,'t','t')"
)
big = _rgba_png(800, 1067)
conn.execute(
"INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,?, 't')",
(big,),
)
conn.commit()
from backend.inference import InferenceClient
from PIL import Image
import io
captured = {}
def fake_chat(self, messages):
captured["messages"] = messages
return json.dumps({"species_id": 1, "species_confidence": 0.9})
monkeypatch.setattr(InferenceClient, "chat", fake_chat)
r = client.post("/api/v1/slabs/s1/inference/call1")
assert r.status_code == 200, r.text
content = captured["messages"][0]["content"]
imgs = [p for p in content if isinstance(p, dict) and p.get("type") == "image_url"]
assert len(imgs) == 1
url = imgs[0]["image_url"]["url"]
assert url.startswith("data:image/png;base64,")
raw = base64.b64decode(url.split(",", 1)[1])
im = Image.open(io.BytesIO(raw))
assert max(im.size) <= 1024