49c22c9385
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.
131 lines
6.0 KiB
Python
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"]
|