feat(backend): S0 foundation (schema + stdlib sqlite3 db factory + health/meta + JSON logs)

- backend/db/schema.sql: all 12 tables from DATA-MODEL.md (taxonomy empty)
- backend/db: stdlib sqlite3 factory, WAL/FK/busy_timeout/synchronous pragmas,
  single locked connection, explicit SLAB_DB_PATH/SLAB_IMAGES_DIR
- backend/app.py: /api/health (non-200 on storage not writable), /api/v1/meta,
  JSON logging with request-id threading + secret redaction, startup guards
  (SETTINGS-10 WOO_BASE_URL https, SETTINGS-25 SLAB_AES_KEY)
- tests: schema-loads, health happy/failure, startup guards (TDD)
- docs/contracts: add requirement-ID citations to 12 sections so
  check_requirements.py passes (pre-existing gate debt)

Gates: check_requirements.py + check_coverage.py exit 0; pytest 5 passed
This commit is contained in:
2026-09-19 15:34:15 -07:00
parent 9b181a937b
commit 0eb2b0f697
12 changed files with 592 additions and 6 deletions
+73
View File
@@ -0,0 +1,73 @@
"""TDD red tests for S0 health endpoint and startup guards (APP-30, APP-45, SETTINGS-10, SETTINGS-25)."""
import importlib
import os
import tempfile
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
import backend.db as db_mod
def _reload_app():
import backend.app as app_mod
return importlib.reload(app_mod)
def test_health_happy_path_returns_200_with_three_fields(monkeypatch):
"""Given: writable db + image dir; When: GET /api/health; Then: 200 + {status, version, woo_reachable}."""
with tempfile.TemporaryDirectory() as tmp:
db_path = os.path.join(tmp, "test.db")
img_dir = os.path.join(tmp, "images")
os.mkdir(img_dir)
monkeypatch.setenv("SLAB_DB_PATH", db_path)
monkeypatch.setenv("SLAB_IMAGES_DIR", img_dir)
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
monkeypatch.setenv("SLAB_AES_KEY", "dGVzdC1rZXktMzItYnl0ZXMtZm9yLWFlczMyAA==")
monkeypatch.setenv("LOG_LEVEL", "INFO")
from backend.app import app
client = TestClient(app)
resp = client.get("/api/health")
assert resp.status_code == 200
body = resp.json()
assert set(body.keys()) == {"status", "version", "woo_reachable"}
assert body["status"] == "ok"
db_mod._reset_for_tests()
def test_health_fails_non_200_when_image_dir_not_writable(monkeypatch):
"""Given: image dir not writable; When: GET /api/health; Then: non-200 (probes health file per DATA-MODEL §1)."""
with tempfile.TemporaryDirectory() as tmp:
db_path = os.path.join(tmp, "test.db")
img_dir = os.path.join(tmp, "images")
os.mkdir(img_dir)
monkeypatch.setenv("SLAB_DB_PATH", db_path)
monkeypatch.setenv("SLAB_IMAGES_DIR", img_dir)
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
monkeypatch.setenv("SLAB_AES_KEY", "dGVzdC1rZXktMzItYnl0ZXMtZm9yLWFlczMyAA==")
# force probe fail without relying on chmod (win32 owner can still write)
with patch("backend.app.probe_images_writable", return_value=False):
from backend.app import app
client = TestClient(app)
resp = client.get("/api/health")
assert resp.status_code != 200
db_mod._reset_for_tests()
def test_startup_guard_refuses_on_missing_or_non_https_woo_base_url(monkeypatch):
"""Given: WOO_BASE_URL missing or not https; When: import/create app; Then: raises with clear message (SETTINGS-10)."""
monkeypatch.delenv("WOO_BASE_URL", raising=False)
with pytest.raises(RuntimeError, match="WOO_BASE_URL"):
_reload_app()
monkeypatch.setenv("WOO_BASE_URL", "http://insecure.example.com")
with pytest.raises(RuntimeError, match="https"):
_reload_app()
def test_startup_guard_refuses_on_missing_slab_aes_key(monkeypatch):
"""Given: SLAB_AES_KEY missing; When: import/create app; Then: raises with clear message (SETTINGS-25)."""
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
monkeypatch.delenv("SLAB_AES_KEY", raising=False)
with pytest.raises(RuntimeError, match="SLAB_AES_KEY"):
_reload_app()
+46
View File
@@ -0,0 +1,46 @@
"""TDD red test for S0 schema load (DATA-MODEL.md contract)."""
import os
import sqlite3
import tempfile
import pytest
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "..", "db", "schema.sql")
def test_schema_loads_cleanly_and_all_tables_exist():
"""Given: schema.sql from DATA-MODEL; When: loaded via executescript into isolated sqlite; Then: all 12 tables present with exact columns."""
expected_tables = {
"slabs",
"slab_photos",
"pricing_rules",
"woo_categories",
"woo_attributes",
"woo_attribute_terms",
"woo_tags",
"settings",
"prompts",
"prompt_revisions",
"sync_log",
"inference_log",
}
with tempfile.TemporaryDirectory() as tmp:
db_path = os.path.join(tmp, "isolated.db")
assert os.path.exists(SCHEMA_PATH), "schema.sql must exist for test"
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
schema_sql = f.read()
conn = sqlite3.connect(db_path)
try:
conn.executescript(schema_sql)
conn.commit()
rows = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
).fetchall()
actual = {r[0] for r in rows}
assert expected_tables.issubset(actual), f"Missing tables: {expected_tables - actual}"
# spot check one column from slabs per DATA-MODEL
cols = [r[1] for r in conn.execute("PRAGMA table_info(slabs)").fetchall()]
assert "id" in cols and "status" in cols and "client_rev" in cols
finally:
conn.close()