Files
SlabUploader/backend/tests/test_exit_inference_off.py
T
Ty 49c22c9385 feat(greenfield): rebuild SlabUploader M1-M3 on clean foundation, all gates green
Clean re-implementation of the full product (not a port of the old M3 tree):
- P0 foundation: 13-table schema + per-call sqlite factory (thread-safe),
  FastAPI app (health, JSON logging + secret redaction, CORS, startup guard).
- P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure,
  png crop/inventory/overlay, EXIF decodeOrientedBitmap.
- P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop
  to batch) + photos-ready lineup.
- P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing,
  inference Call1/Call2, review, publish, settings, slabs, photos) + real FE
  review/publish/settings screens consuming the API (no hardcoded Woo ids).
- P4 gate exit: check_requirements + check_coverage both exit 0
  (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green.

Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
2026-09-25 06:57:29 -07:00

131 lines
6.0 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_")
monkeypatch.setenv("SLAB_DB_PATH", os.path.join(tmp, "e.db"))
monkeypatch.setenv("SLAB_IMAGES_DIR", tmp)
monkeypatch.setenv("WOO_BASE_URL", "https://store.example.com")
monkeypatch.setenv("SLAB_AES_KEY", base64.b64encode(b"0123456789abcdef0123456789abcdef").decode())
from backend.crypto import encrypt
db_mod.init_db()
conn = db_mod.get_connection()
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
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"]