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.
This commit is contained in:
2026-09-25 06:57:29 -07:00
parent 4ca2ef57a7
commit 49c22c9385
106 changed files with 5502 additions and 8863 deletions
+43 -35
View File
@@ -1,5 +1,4 @@
"""S4 Woo client + taxonomy sync tests (mocked transport; never hit the real store)."""
"""Woo client + taxonomy sync tests (mocked transport; never hit the real store)."""
import base64
import json
import os
@@ -10,6 +9,7 @@ import pytest
import backend.db as db_mod
from backend.woo import (
_USER_AGENT,
WooAuthError,
WooClient,
WooConnectionError,
@@ -21,21 +21,20 @@ from backend import taxonomy as taxonomy_mod
@pytest.fixture
def db_dir():
def db_dir(monkeypatch):
tmp = tempfile.mkdtemp(prefix="woo_")
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "t.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()
db_mod._reset_for_tests()
conn = db_mod.init_db()
monkeypatch.setenv("SLAB_DB_PATH", os.path.join(tmp, "t.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())
db_mod.init_db()
conn = db_mod.get_connection()
from backend.crypto import encrypt
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("secret"),))
conn.commit()
yield conn
db_mod._reset_for_tests()
shutil.rmtree(tmp, ignore_errors=True)
@@ -46,8 +45,7 @@ def _client(responses):
def fake_request(url, method, data, headers):
i = min(calls["n"], len(responses) - 1)
calls["n"] += 1
code, hdr, body = responses[i]
return code, hdr, body
return responses[i]
return WooClient("admin", "secret", "https://store.example.com", request=fake_request), calls
@@ -86,15 +84,47 @@ def test_missing_password_fails_closed(db_dir):
WooClient.from_settings("https://store.example.com", "admin", None)
def test_missing_username_fails_closed(db_dir):
with pytest.raises(WooCredentialsMissing):
WooClient.from_settings("https://store.example.com", None, "enc")
def test_bad_ciphertext_fails_closed(db_dir):
with pytest.raises(WooCredentialsMissing):
WooClient.from_settings("https://store.example.com", "admin", "!!!not-base64!!!")
def test_401_raises_auth_error(db_dir):
def fake_request(url, method, data, headers):
raise WooAuthError("authentication rejected (401)")
return 401, {}, b'{"message": "Invalid username"}'
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
with pytest.raises(WooAuthError):
c.get_categories()
def test_403_raises_forbidden_error(db_dir):
def fake_request(url, method, data, headers):
return 403, {}, b"{}"
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
with pytest.raises(WooForbiddenError):
c.get_categories()
def test_requests_include_user_agent(db_dir):
seen_headers = {}
def fake_request(url, method, data, headers):
seen_headers.update(headers)
return 200, {"X-WP-TotalPages": "1"}, b"[]"
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
c.get_categories()
assert seen_headers.get("User-Agent") == _USER_AGENT
assert "SlabUploader" in seen_headers["User-Agent"]
def test_sync_populates_scoped(db_dir):
cats = (200, {"X-WP-TotalPages": "1"}, json.dumps([{"id": 1, "name": "Cedar", "slug": "cedar", "parent_id": None, "woo_id": 10}]).encode())
tags = (200, {"X-WP-TotalPages": "1"}, json.dumps([
@@ -208,25 +238,3 @@ def test_sync_with_slab_species_fk_does_not_fail(db_dir):
}
assert rows[11] == 10 # parent mapped; row kept for FK
assert conn.execute("SELECT species_id FROM slabs WHERE id='slab-fk-1'").fetchone()[0] == 11
def test_403_raises_forbidden_error(db_dir):
def fake_request(url, method, data, headers):
raise WooForbiddenError("request forbidden (403)")
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
with pytest.raises(WooForbiddenError):
c.get_categories()
def test_requests_include_user_agent(db_dir):
seen_headers = {}
def fake_request(url, method, data, headers):
seen_headers.update(headers)
return 200, {"X-WP-TotalPages": "1"}, b"[]"
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
c.get_categories()
assert "User-Agent" in seen_headers
assert "SlabUploader" in seen_headers["User-Agent"]