"""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_seed_is_locked_expert_prompt(env): """#86: shipped Call 1 seed is the locked expert prompt (no free-text bags, no grade).""" client, _conn = env from backend.inference import CALL1_SEED body = client.get("/api/v1/prompts/call1").json() assert body["active_content"] == CALL1_SEED assert "wood identification expert" in CALL1_SEED assert "feature_tag_ids" in CALL1_SEED assert "Decide wood categories before edge type" in CALL1_SEED assert "highest-confidence" in CALL1_SEED assert "Wild edge" in CALL1_SEED assert "cookies and burl" in CALL1_SEED.lower() or "cookies and burl" in CALL1_SEED assert "grade_term_id" not in CALL1_SEED assert '"character"' not in CALL1_SEED assert '"inclusions"' not in CALL1_SEED assert "character\":" not in CALL1_SEED.replace(" ", "") def test_taxonomy_lists_call1_closed_shape(): """#86: Call 1 lists are closed Woo picks only — no grade/thickness/moisture; feat names stripped.""" import tempfile, os, shutil tmp = tempfile.mkdtemp(prefix="tax_") os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "t.db") os.environ["SLAB_IMAGES_DIR"] = tmp os.environ.setdefault("WOO_BASE_URL", "https://x.com") os.environ.setdefault( "SLAB_AES_KEY", base64.b64encode(b"0123456789abcdef0123456789abcdef").decode(), ) db_mod._reset_for_tests() conn = db_mod.init_db() conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (10,'Species','species',NULL,10)") conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (11,'Walnut','walnut',10,11)") conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (20,'Wcat','wcat',NULL,20)") conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (21,'Live edge wood','liveedge',20,21)") conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (6,'Edge type','pa_etype',6)") conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (8,'Figure','pa_fig',8)") conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (9,'Grade','pa_gr',9)") conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (10,'Thickness','pa_thk',10)") conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (7,'Moisture','pa_mst',7)") conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (321,6,'Square edge','sqe',321)") conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (328,8,'Plain','pl',328)") conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (342,9,'Fancy','fnc',342)") conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (350,10,'1\"- 1 1/2\"','4-6qtr',350)") conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (326,7,'Kiln dried','kd',326)") conn.execute("INSERT INTO woo_tags(id,name,slug,woo_id) VALUES (1,'feat-knot','feat-knot',1)") conn.execute("INSERT INTO woo_tags(id,name,slug,woo_id) VALUES (2,'fig-curl','fig-curl',2)") conn.commit() from backend.inference import _taxonomy_lists tax = _taxonomy_lists(conn) assert set(tax.keys()) == {"species", "wood_categories", "edge_terms", "figure_terms", "feature_tags"} assert tax["species"] == [{"id": 11, "name": "Walnut"}] assert tax["wood_categories"] == [{"id": 21, "name": "Live edge wood"}] assert tax["edge_terms"] == [{"id": 321, "name": "Square edge"}] assert tax["figure_terms"] == [{"id": 328, "name": "Plain"}] # feat- prefix stripped for the model; fig- tags are not feature_tags assert tax["feature_tags"] == [{"id": 1, "name": "knot"}] assert "grade_terms" not in tax assert "thickness_terms" not in tax assert "moisture_terms" not in tax db_mod._reset_for_tests() shutil.rmtree(tmp, ignore_errors=True) def test_gate_feature_tag_ids_not_free_text(): """#86: gate keeps feature_tag_ids at threshold; drops free-text bags and grade.""" from backend.inference import _gate parsed = { "species_id": 11, "species_confidence": 0.9, "wood_category_ids": [21], "wood_category_confidence": 0.8, "edge_type_term_id": 321, "edge_confidence": 0.85, "figure_term_ids": [328, 335], "figure_confidence": 0.75, "feature_tag_ids": [1], "feature_tag_confidence": 0.8, "grade_term_id": 342, "grade_confidence": 0.99, "character": ["rustic"], "inclusions": ["bark"], "voids": [], "checks": ["end"], } out = _gate(parsed) assert out["species_id"] == 11 assert out["wood_category_ids"] == [21] assert out["edge_type_term_id"] == 321 assert out["figure_term_ids"] == [328, 335] assert out["feature_tag_ids"] == [1] assert "grade_term_id" not in out assert "character" not in out assert "inclusions" not in out assert "voids" not in out assert "checks" not in out # below threshold feature tags dropped low = {**parsed, "feature_tag_confidence": 0.2} assert "feature_tag_ids" not in _gate(low) def test_call1_hard_disabled_410(env, monkeypatch): """overnight-whole-app: Call 1 OUT — POST always 410, never chats the model.""" 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')") 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 called = {"n": 0} def fake_chat(self, messages): called["n"] += 1 return "{}" monkeypatch.setattr(InferenceClient, "chat", fake_chat) r = client.post("/api/v1/slabs/s1/inference/call1") assert r.status_code == 410, r.text body = r.json() assert body["error"]["code"] == "call1_disabled" assert called["n"] == 0 assert conn.execute("SELECT COUNT(*) AS n FROM inference_log").fetchone()["n"] == 0 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: "
A fine walnut slab.
") r = client.post("/api/v1/slabs/s1/inference/call2") assert r.status_code == 200, r.text assert r.json()["description"] == "A fine walnut slab.
" row = conn.execute("SELECT description, content_source FROM slabs WHERE id='s1'").fetchone() assert row["description"] == "A fine walnut slab.
" assert row["content_source"] == "llm" def test_call1_disabled_even_when_inference_off(env): """Call 1 hard-disable wins over inference_enabled=false (still 410, not 409).""" 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 == 410 assert r.json()["error"]["code"] == "call1_disabled" 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_redact_request_for_log_strips_data_urls_keeps_text(): """#87: log sanitizer replaces data-URL images with photo refs; keeps text.""" from backend.inference import _redact_request_for_log b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" messages = [{ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64 * 20}"}}, {"type": "text", "text": "LISTS:\n{\"species\":[]}\n\nSKU=X length_in=1 thickness_in=2"}, ], }] meta = [ {"photo_id": "p1", "seq": 1, "bytes": 100, "infer_long_edge_px": 64}, {"photo_id": "p2", "seq": 2, "bytes": 200, "infer_long_edge_px": 128}, ] out = _redact_request_for_log(messages, meta) blob = json.dumps(out) assert "data:image" not in blob assert "base64" not in blob.lower() assert "LISTS:" in blob assert "SKU=X" in blob parts = out[0]["content"] assert parts[0]["type"] == "image_ref" assert parts[0]["photo_id"] == "p1" assert parts[0]["seq"] == 1 assert parts[1]["photo_id"] == "p2" assert parts[2]["type"] == "text" assert "LISTS:" in parts[2]["text"] def test_call1_log_skipped_while_disabled(env): """#87 Call 1 log path dormant while Call 1 is hard-disabled (overnight-whole-app).""" 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')" ) conn.commit() r = client.post("/api/v1/slabs/s1/inference/call1") assert r.status_code == 410 assert conn.execute("SELECT COUNT(*) AS n FROM inference_log").fetchone()["n"] == 0 def test_call2_log_redacts_images(env, monkeypatch): """#87: Call 2 log also redacts image data-URLs.""" 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','ready',1,1,'t','t')" ) png = _rgba_png(200, 200) conn.execute( "INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p2','s1',1,?, 't')", (png,), ) conn.commit() from backend.inference import InferenceClient monkeypatch.setattr(InferenceClient, "chat", lambda self, messages: "ok
") r = client.post("/api/v1/slabs/s1/inference/call2") assert r.status_code == 200, r.text payload = conn.execute( "SELECT request_payload FROM inference_log WHERE call=2 ORDER BY id DESC LIMIT 1" ).fetchone()["request_payload"] assert "data:image" not in payload assert "image_ref" in payload assert "p2" in payload assert "DIMENSION FACT BLOCK" in payload def test_call1_sends_long_edge_dormant_while_disabled(env): """METADATA-35 Call 1 sizing path dormant while Call 1 is hard-disabled.""" client, _conn = env r = client.post("/api/v1/slabs/missing/inference/call1") assert r.status_code == 410 assert r.json()["error"]["code"] == "call1_disabled"