88278a55e8
Long desc is Hook + Benefits (+ brief trust close) only; forbid Full specs / Attribute list / product-name header that re-lists DECLARED ATTRIBUTES or DIMENSION FACT BLOCK. Defensively strip trailing Category:/Length:/Board Feet: labeled dumps in parse.
642 lines
28 KiB
Python
642 lines
28 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_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_call2_seed_has_short_long_structure(env):
|
||
"""Call 2 seed: plain-text short+long, woodworker tone, JSON dual output (not brand voice)."""
|
||
client, _conn = env
|
||
from backend.inference import CALL2_SEED
|
||
|
||
body = client.get("/api/v1/prompts/call2").json()
|
||
assert body["active_content"] == CALL2_SEED
|
||
assert "SHORT DESCRIPTION" in CALL2_SEED
|
||
assert "LONG DESCRIPTION" in CALL2_SEED
|
||
assert "PLAIN TEXT ONLY" in CALL2_SEED
|
||
assert "plain text only" in CALL2_SEED.lower()
|
||
assert "No HTML tags" in CALL2_SEED
|
||
assert "woodworkers choosing stock" in CALL2_SEED
|
||
assert "elevate" in CALL2_SEED # banned-phrase guidance
|
||
assert "stunning" in CALL2_SEED
|
||
assert "Lead with a practical outcome" in CALL2_SEED
|
||
assert "Hook" in CALL2_SEED
|
||
assert "Brand voice and Geography sections when provided" in CALL2_SEED
|
||
assert "Whidbey" not in CALL2_SEED
|
||
assert "Pacific Northwest" not in CALL2_SEED
|
||
assert '"short_description"' in CALL2_SEED
|
||
assert '"description"' in CALL2_SEED
|
||
assert "<HTML long description>" not in CALL2_SEED
|
||
assert "plain text long description" in CALL2_SEED
|
||
# No Full-specs / attribute metadata dump in long desc (specs live on Woo fields).
|
||
assert "Hook + Benefits only" in CALL2_SEED
|
||
assert "Do NOT restate DECLARED" in CALL2_SEED
|
||
assert 'no "Full specs"' in CALL2_SEED or "Do NOT add a Full specs" in CALL2_SEED
|
||
assert "Category:/Figure:/Edge:" in CALL2_SEED
|
||
assert "1–2 key traits" in CALL2_SEED
|
||
assert "labeled catalog" in CALL2_SEED or "labeled metadata list" in CALL2_SEED
|
||
# Must not instruct a Full-specs numbered part (old 4-part structure).
|
||
assert "3. Full specs" not in CALL2_SEED
|
||
assert "Four parts, in order" not in CALL2_SEED
|
||
|
||
|
||
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_short_and_description(env, monkeypatch):
|
||
"""Call 2 persists both fields as plain text (HTML from model is stripped)."""
|
||
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,short_description,"
|
||
"status,server_rev,client_rev,created_at,updated_at) "
|
||
"VALUES ('s1','SLAB-01',100,2,20,18,'Walnut Slab','old short','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
|
||
import json as _json
|
||
|
||
payload = _json.dumps({
|
||
"short_description": "Kiln-dried walnut live-edge stock for a dining table — clear midspan, usable width. Ready to finish.",
|
||
"description": "<p><strong>Dining table stock.</strong></p><ul><li><strong>Stable:</strong> kiln dried</li></ul>",
|
||
})
|
||
monkeypatch.setattr(InferenceClient, "chat", lambda self, messages: payload)
|
||
r = client.post("/api/v1/slabs/s1/inference/call2")
|
||
assert r.status_code == 200, r.text
|
||
body = r.json()
|
||
assert "Dining table stock" in body["description"]
|
||
assert "kiln dried" in body["description"]
|
||
assert "<p>" not in body["description"]
|
||
assert "<strong>" not in body["description"]
|
||
assert body["short_description"].startswith("Kiln-dried walnut")
|
||
row = conn.execute(
|
||
"SELECT short_description, description, content_source FROM slabs WHERE id='s1'"
|
||
).fetchone()
|
||
assert row["short_description"].startswith("Kiln-dried walnut")
|
||
assert "<p>" not in row["description"]
|
||
assert "- Stable:" in row["description"] or "Stable:" in row["description"]
|
||
assert row["content_source"] == "llm"
|
||
|
||
|
||
def test_call2_html_fallback_keeps_prior_short(env, monkeypatch):
|
||
"""Non-JSON Call 2 body becomes plain-text description; prior short is left alone."""
|
||
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,short_description,"
|
||
"status,server_rev,client_rev,created_at,updated_at) "
|
||
"VALUES ('s1','SLAB-01',100,2,20,18,'Walnut Slab','keep me','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"] == "A fine walnut slab."
|
||
assert "short_description" not in r.json()
|
||
row = conn.execute(
|
||
"SELECT short_description, description, content_source FROM slabs WHERE id='s1'"
|
||
).fetchone()
|
||
assert row["short_description"] == "keep me"
|
||
assert row["description"] == "A fine walnut slab."
|
||
assert row["content_source"] == "llm"
|
||
|
||
|
||
def test_parse_call2_response_json_and_fenced():
|
||
from backend.inference import parse_call2_response, _html_to_plain
|
||
import json as _json
|
||
|
||
short, desc = parse_call2_response(
|
||
_json.dumps({"short_description": " Benefit first. ", "description": " <p>Long</p> "})
|
||
)
|
||
assert short == "Benefit first."
|
||
assert desc == "Long"
|
||
fenced = "```json\n" + _json.dumps({"short_description": "S", "description": "<p>D</p>"}) + "\n```"
|
||
short2, desc2 = parse_call2_response(fenced)
|
||
assert short2 == "S"
|
||
assert desc2 == "D"
|
||
short3, desc3 = parse_call2_response("<p>only long</p>")
|
||
assert short3 is None
|
||
assert desc3 == "only long"
|
||
|
||
# Defensive strip: lists become "- " bullets; entities unescaped; no tags left.
|
||
plain = _html_to_plain(
|
||
"<p>Hook.</p><ul><li><strong>Stable:</strong> kiln dried</li>"
|
||
"<li>Live edge</li></ul><p>Trust & ship.</p>"
|
||
)
|
||
assert "<" not in plain
|
||
assert "Hook." in plain
|
||
assert "- Stable: kiln dried" in plain
|
||
assert "- Live edge" in plain
|
||
assert "Trust & ship." in plain
|
||
# Already-plain text is unchanged aside from trim.
|
||
assert _html_to_plain(" Kiln-dried walnut for a bar top. ") == "Kiln-dried walnut for a bar top."
|
||
|
||
|
||
def test_strip_attribute_dump_trailing_and_header():
|
||
"""Defensive: drop labeled Category:/Length: dumps from long description."""
|
||
from backend.inference import _strip_attribute_dump, parse_call2_response
|
||
import json as _json
|
||
|
||
hook_benefits = (
|
||
"Kiln-dried maple suited to a compact bar top.\n"
|
||
"- Stable stock: kiln dried, ready to mill.\n"
|
||
"- Live edge on one side for a natural apron.\n"
|
||
"- Character grade with usable burl figure."
|
||
)
|
||
dump = (
|
||
"Maple Slab\n"
|
||
"- Category: Live edge wood\n"
|
||
"- Figure: Burl, Curl, Figured\n"
|
||
"- Edge: Live Edge 1 side\n"
|
||
"- Grade: Character\n"
|
||
"- Moisture: Kiln dried\n"
|
||
"- Length: 45.0in\n"
|
||
"- Board Feet: 12.4"
|
||
)
|
||
trust = "Ships from the mill; ask about returns in Brand voice."
|
||
full = f"{hook_benefits}\n\n{dump}\n\n{trust}"
|
||
cleaned = _strip_attribute_dump(full)
|
||
assert "Category:" not in cleaned
|
||
assert "Board Feet:" not in cleaned
|
||
assert "Maple Slab" not in cleaned
|
||
assert "Kiln-dried maple" in cleaned
|
||
assert "Live edge on one side" in cleaned
|
||
assert "Ships from the mill" in cleaned
|
||
|
||
# Mid-body Full specs header also stripped; benefits bullets without Label: kept.
|
||
mid = (
|
||
"Hook line.\n"
|
||
"- Good for a bench seat.\n"
|
||
"\n"
|
||
"Full specs\n"
|
||
"- Species: Maple\n"
|
||
"- Length: 45.0in\n"
|
||
"- Thickness: 2.0in\n"
|
||
"\n"
|
||
"Neutral trust close."
|
||
)
|
||
mid_clean = _strip_attribute_dump(mid)
|
||
assert "Full specs" not in mid_clean
|
||
assert "Species:" not in mid_clean
|
||
assert "Good for a bench seat" in mid_clean
|
||
assert "Neutral trust close" in mid_clean
|
||
|
||
# Fewer than 3 attr lines: leave alone (not a dump).
|
||
short_list = "Hook.\n- Length: 45.0in\n- Thickness: 2in\nTrust."
|
||
assert _strip_attribute_dump(short_list) == short_list.strip()
|
||
|
||
# parse_call2_response applies strip on description (not short).
|
||
payload = _json.dumps({
|
||
"short_description": "Short with - Length: 1in - Width: 2in - Thickness: 3in ok",
|
||
"description": full,
|
||
})
|
||
short, desc = parse_call2_response(payload)
|
||
assert short and "Length: 1in" in short # short untouched by attr-dump strip
|
||
assert "Category:" not in desc
|
||
assert "Board Feet:" not in desc
|
||
assert "Kiln-dried maple" in desc
|
||
|
||
|
||
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: "<p>ok</p>")
|
||
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"
|
||
|
||
|
||
def test_assemble_call2_includes_declared_attrs_and_dims(env):
|
||
"""Call 2 text = seed + DECLARED ATTRIBUTES + dimension fact + brand/geo; shared assembler."""
|
||
client, conn = env
|
||
conn.execute(
|
||
"INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES "
|
||
"(10,'Species','species',NULL,10),"
|
||
"(11,'Black Walnut','black-walnut',10,11),"
|
||
"(20,'Wcat','wcat',NULL,20),"
|
||
"(21,'Slabs & Lumber','snl',20,21),"
|
||
"(22,'Live edge wood','liveedge',21,22)"
|
||
)
|
||
conn.execute(
|
||
"INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES "
|
||
"(6,'Edge type','pa_etype',6),(8,'Figure','pa_fig',8),"
|
||
"(9,'Grade','pa_gr',9),(7,'Moisture','pa_mst',7)"
|
||
)
|
||
conn.execute(
|
||
"INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES "
|
||
"(321,6,'Live Edge 1 side','le1',321),"
|
||
"(328,8,'Curl','curl',328),"
|
||
"(342,9,'Fancy','fnc',342),"
|
||
"(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 OR REPLACE INTO settings(key,value,value_enc) VALUES "
|
||
"('brand_voice','Warm and precise.',NULL),"
|
||
"('geo_context','Pacific Northwest mill.',NULL)"
|
||
)
|
||
conn.execute(
|
||
"INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,width_min_in,width_max_in,bdft,"
|
||
"species_id,wood_category_ids,edge_type_term_id,figure_term_ids,grade_term_id,moisture_term_id,"
|
||
"feat_tag_ids,title,short_description,status,server_rev,client_rev,created_at,updated_at) "
|
||
"VALUES ('s1','SLAB-01',114.0,1.75,24.3,18.0,24.3,18.4,11,?,321,?,342,326,?,"
|
||
"'Figured Black Walnut Live Edge Slab','short line','ready',1,1,'t','t')",
|
||
(json.dumps([22]), json.dumps([328]), json.dumps([1])),
|
||
)
|
||
png = _rgba_png(200, 200)
|
||
conn.execute(
|
||
"INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p9','s1',1,?, 't')",
|
||
(png,),
|
||
)
|
||
conn.commit()
|
||
from backend.inference import assemble_call2, assemble_call2_text, CALL2_SEED
|
||
|
||
assembled = assemble_call2(conn, "s1")
|
||
assert assembled is not None
|
||
text = assembled["text"]
|
||
assert "DECLARED ATTRIBUTES" in text
|
||
assert "species: Black Walnut" in text
|
||
assert "primary wood category: Live edge wood" in text
|
||
assert "edge: Live Edge 1 side" in text
|
||
assert "figure: Curl" in text
|
||
assert "grade: Fancy" in text
|
||
assert "moisture: Kiln dried" in text
|
||
assert "features: knot" in text
|
||
assert "DIMENSION FACT BLOCK" in text
|
||
assert "length=114.0in" in text
|
||
assert "Brand voice: Warm and precise." in text
|
||
assert "Geo: Pacific Northwest mill." in text
|
||
assert CALL2_SEED.split("\n")[0] in text
|
||
assert "SHORT DESCRIPTION" in text
|
||
assert "LONG DESCRIPTION" in text
|
||
assert "short_description" in CALL2_SEED
|
||
assert '"description"' in CALL2_SEED or "\"description\"" in CALL2_SEED
|
||
# Prior short must not be fed back into the prompt (Call 2 writes it).
|
||
assert "short_description: short line" not in text
|
||
assert "Write BOTH" in CALL2_SEED or "BOTH the short" in CALL2_SEED
|
||
# No raw base64 in View-prompt shape
|
||
assert "photos" in assembled
|
||
assert assembled["photos"][0]["id"] == "p9"
|
||
assert assembled["photos"][0]["seq"] == 1
|
||
assert assembled["photos"][0]["bytes"] > 0
|
||
assert "base64" not in json.dumps(assembled["photos"])
|
||
# View prompt GET mirrors assembler (no model)
|
||
r = client.get("/api/v1/slabs/s1/inference/call2/prompt")
|
||
assert r.status_code == 200, r.text
|
||
body = r.json()
|
||
assert body["text"] == text
|
||
assert body["photos"][0]["id"] == "p9"
|
||
assert "data:image" not in json.dumps(body)
|
||
|
||
|
||
def test_call2_post_uses_same_assembler_text(env, monkeypatch):
|
||
"""POST call2 sends the same assembled text View prompt returns."""
|
||
client, conn = env
|
||
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','true',NULL)")
|
||
conn.execute(
|
||
"INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES "
|
||
"('brand_voice','voice',NULL),('geo_context','geo',NULL)"
|
||
)
|
||
conn.execute(
|
||
"INSERT INTO slabs(id,sku,length_in,thickness_in,width_avg_in,width_min_in,width_max_in,bdft,"
|
||
"title,status,server_rev,client_rev,created_at,updated_at) "
|
||
"VALUES ('s1','SLAB-01',100,2,20,18,22,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,?, 't')",
|
||
(_rgba_png(64, 64),),
|
||
)
|
||
conn.commit()
|
||
from backend.inference import InferenceClient, assemble_call2
|
||
|
||
expected = assemble_call2(conn, "s1")["text"]
|
||
captured = {}
|
||
|
||
def fake_chat(self, messages):
|
||
captured["text"] = messages[0]["content"][-1]["text"]
|
||
return "<p>ok</p>"
|
||
|
||
monkeypatch.setattr(InferenceClient, "chat", fake_chat)
|
||
r = client.post("/api/v1/slabs/s1/inference/call2")
|
||
assert r.status_code == 200, r.text
|
||
assert captured["text"] == expected
|
||
assert "DECLARED ATTRIBUTES" in captured["text"]
|
||
assert "DIMENSION FACT BLOCK" in captured["text"]
|