Files
SlabUploader/backend/tests/test_exit_inference_off.py
T
grokbot_ty 2f9b8ebd02 proto(woo): blocking setup gate; require X-Woo-* on Woo HTTP routes
Full-screen WooSetupGate on App mount when this browser lacks device WP
user + app password. Save + Test Woo dismisses only after a successful
test. Publish/test-woo auth failures reopen the gate.

Server: client_for_request requires X-Woo-* on publish, taxonomy sync,
and test-woo (401 woo_device_creds_required); health probe keeps
Settings. Optional SLAB_WOO_ALLOW_SETTINGS_FALLBACK for legacy tests.

See PROTOTYPE.md.
2026-09-26 19:20:16 -07:00

132 lines
6.1 KiB
Python

"""Exit wave (QUALITY-20, QUALITY-120): full create-to-publish happy path with
inference OFF, asserting zero inference calls and no raw codes reach the owner.
"""
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="exit_")
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "e.db")
os.environ["SLAB_IMAGES_DIR"] = tmp
os.environ["WOO_BASE_URL"] = "https://store.example.com"
os.environ["SLAB_AES_KEY"] = base64.b64encode(b"0123456789abcdef0123456789abcdef").decode()
os.environ["SLAB_WOO_ALLOW_SETTINGS_FALLBACK"] = "1" # legacy tests; prefer X-Woo-* headers
db_mod._reset_for_tests()
from backend.crypto import encrypt
conn = db_mod.init_db()
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 woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge wood','live-edge-wood',NULL,101)")
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (8,'Edge Type','edge-type',200)")
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (1,8,'Flat','flat',300)")
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_wp_username','admin',NULL)")
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_app_password',NULL,?)", (encrypt("pw"),))
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','false',NULL)")
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','false',NULL)")
from datetime import datetime, timezone
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('taxonomy_last_sync_at',?,NULL)",
(datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),))
conn.commit()
from backend.app import app
yield TestClient(app), conn
db_mod._reset_for_tests()
shutil.rmtree(tmp, ignore_errors=True)
def _mock(monkeypatch, sku_exists=0, product_id=777):
from backend.woo import WooClient
def fake_request(url, method, data, headers):
if "/products?sku=" in url:
return 200, {}, json.dumps([{"id": 1}] * sku_exists).encode()
if url.endswith("/wp-json/wp/v2/media"):
return 201, {}, json.dumps({"id": 42}).encode()
if url.endswith("/wp-json/wc/v3/products"):
return 201, {}, json.dumps({"id": product_id}).encode()
return 200, {}, b"[]"
orig = WooClient.__init__
def patched(self, *a, **k):
orig(self, *a, **k)
def bound(url, method, data, headers):
return fake_request(url, method, data, headers)
self._request = bound
monkeypatch.setattr(WooClient, "__init__", patched)
def test_full_happy_path_inference_off(env, monkeypatch):
"""Create slab -> attach photo -> review -> publish, inference OFF.
Zero inference_log rows; all responses carry plain messages (QUALITY-120)."""
client, conn = env
_mock(monkeypatch)
# 1. create slab
r = client.post("/api/v1/slabs", json={
"id": "s1", "sku": "SLAB-UAT-7", "length_in": 114.0, "thickness_in": 1.75, "sqft": 30.0,
"bdft": 18.4, "width_min_in": 18.0, "width_max_in": 24.3, "width_avg_in": 24.3,
"species_id": 1, "wood_category_ids": [2], "edge_type_term_id": 1, "figure_term_ids": [],
"grade_term_id": 1, "thickness_term_id": 1, "moisture_term_id": 1,
"fig_tag_ids": [], "feat_tag_ids": [], "price": 184.0, "price_source": "recommendation",
"title": "Black Walnut Live Edge Slab", "short_description": "slab, 9ft 6in",
"description": "A fine slab.", "content_source": "manual",
"species_confidence": None, "figure_confidence": None, "grade_confidence": None,
"inference_status": None, "calibration_photo_id": None,
})
assert r.status_code in (201, 200), r.text
# 2. attach photo
r = client.post("/api/v1/slabs/s1/photos", data={"photo_id": "p1"},
files={"original": ("o.png", b"\x89PNG", "image/png")})
assert r.status_code == 201, r.text
# 3. review (deterministic copy)
r = client.post("/api/v1/slabs/s1/review", json={})
assert r.status_code == 200
# 4. publish
r = client.post("/api/v1/slabs/s1/publish")
assert r.status_code == 200, r.text
body = r.json()
assert body["ok"] is True and body["woo_product_id"] == 777 and body["draft"] is True
# QUALITY-20: zero inference calls in off mode
assert conn.execute("SELECT COUNT(*) c FROM inference_log").fetchone()["c"] == 0
# QUALITY-120: response has no raw status codes / stack traces in owner-facing message
assert "Traceback" not in json.dumps(body)
# success log
assert conn.execute("SELECT status FROM sync_log").fetchone()["status"] == "success"
def test_not_ready_publish_422_plain_message(env, monkeypatch):
"""Failure path returns a plain field-level message, no raw code (QUALITY-120)."""
client, conn = env
_mock(monkeypatch)
client.post("/api/v1/slabs", json={
"id": "s1", "sku": "SLAB-01", "length_in": 100, "thickness_in": 2, "sqft": 20, "bdft": 8,
"width_min_in": 10, "width_max_in": 12, "width_avg_in": 11, "species_id": 1,
"wood_category_ids": [2], "edge_type_term_id": 1, "figure_term_ids": [], "grade_term_id": 1,
"thickness_term_id": 1, "moisture_term_id": 1, "fig_tag_ids": [], "feat_tag_ids": [],
"price": None, "price_source": None, "title": "T", "short_description": "S",
"description": "D", "content_source": "manual",
"species_confidence": None, "figure_confidence": None, "grade_confidence": None,
"inference_status": None, "calibration_photo_id": None,
})
r = client.post("/api/v1/slabs/s1/publish")
assert r.status_code == 422
body = r.json()
assert "missing" in body["error"]["detail"]
assert "Traceback" not in json.dumps(body)
assert "price" in body["error"]["detail"]["missing"]