# Test spec contract Status: SPEC · 2026-09-18 · reconciled against the finished PoR. Defines the affirmative test bar. It cites requirement IDs; `docs/REQUIREMENTS.md` is normative. The human walkthrough is `docs/UAT-PRACTICE.md`. ## Requirements impact | Requirement | Impact on this design | |---|---| | QUALITY-5, QUALITY-10, QUALITY-15 | TDD for the deterministic core; vectors TV-1..TV-10 | | QUALITY-20, QUALITY-35, QUALITY-50 | inference-off suite, Woo-mock integration, Compose UI E2E | | QUALITY-25, QUALITY-30 | evidence rules; Ty owns gate exit | | QUALITY-40, QUALITY-45 | property checks and bdft identity | | QUALITY-55 .. QUALITY-100 | pipeline vectors | | QUALITY-105, QUALITY-110 | dishonest proxies; M3 exit evidence | | QUALITY-115, QUALITY-120, QUALITY-125 | browser E2E, no raw codes, AES round-trip | | APP-45, APP-55, APP-60, APP-65 | health, logging, accessibility, log redaction | | SETTINGS-80, PUBLISH-30/31/35 | failure handling under test | ## 1. Layers and harness | Layer | Written | Location | Rule source | |---|---|---|---| | Unit | TDD before the code exists | `backend/tests/unit/`, `frontend/src/__tests__/` | deterministic core and validation | | API contract | with the feature | `backend/tests/contract/` (pytest + httpx TestClient) | `docs/contracts/API.md` | | Integration | with the feature | `backend/tests/integration/` (respx Woo mock) | `docs/contracts/WOO-API.md`, `docs/contracts/CONTENT-WOO.md` | | UI / E2E | with the feature | `frontend/e2e/` (Playwright, Compose stack, mocked Woo) | `docs/contracts/UX.md` | | Ops | with deploy config | `deploy/tests/` (shell) | `deploy/` | Allowed results are `pass`, `fail`, `not run`, and optionally `blocked`. Every `pass` or `fail` needs runner evidence; no evidence means `not run`. The only bug log is Gitea issues. Ty reads raw results and owns gate exit; an agent never declares a gate green (`QUALITY-25`, `QUALITY-30`). ## 2. Fundamentals (FUND) - FUND-1..4 — no browser/DOM/network deps in pure tests; semantic selectors; real Tab navigation; reactivity flush. (test practice) - FUND-5, FUND-6, FUND-7 — Tab order, 320 px reflow, visible focus ring → `APP-60` - FUND-8..11 — app factory per test, lifespan, override cleanup, no real network (test practice) - FUND-12, FUND-13, FUND-16 — FK and constraint enforcement, migration integrity (test practice) - FUND-14 — WAL with no stale reads (test practice) - FUND-15 — illegal status transitions rejected → `APP-5` - FUND-17 — fresh nonce per encrypt; tamper fails → `SETTINGS-25` - FUND-18 — ciphertext absent from GET; only configured booleans → `SETTINGS-75` - FUND-19 — missing key refuses startup → `SETTINGS-25` - FUND-20 — Basic Auth with application passwords over HTTPS, not consumer keys → `SETTINGS-15` - FUND-21 — 401/400/404 mapped, not passed raw → `APP-50` - FUND-22 — pre-create SKU lookup, duplicate race → `PUBLISH-15` - FUND-23 — timeouts/network errors graceful, no stack → `PUBLISH-30`, `PUBLISH-35` - FUND-24 — no secret material in logs → `APP-65` - FUND-25 — HTTPS, health through proxy, port collision → `OPS-5`, `OPS-15`, `OPS-20` ## 3. Deterministic vectors (TV) - TV-1 — green patch not touching the border survives; U2Net not invoked → `QUALITY-55` - TV-2 — dark interior wood survives; flood fill isolates border-connected regions → `QUALITY-60` - TV-3 — 96 in, 1.5 in, 0.64 sqft yields 0.96 bdft; 1 sqft at 1 in equals 1 bdft → `QUALITY-65` - TV-4 — 6 in stations on a 96 in axis report min, max, average → `QUALITY-70` - TV-5 — one square end, one 45 degree cut; axis follows prevailing length → `QUALITY-75` - TV-6 — 3:4 crop, 90 percent fill at extremes, transparent PNG, 800 px floor → `QUALITY-80` - TV-7 — undersized source warns, is not upscaled, not publishable until retake → `QUALITY-85` - TV-8 — 0.96 bdft at 12.50 gives 12.00; a 150.00 override is stored → `QUALITY-90` - TV-9 — 1 in rounds up into the synced Woo thickness band → `QUALITY-95` - TV-10 — half-up 2 decimal rounding cases → `QUALITY-100` Validation: - SKU charset 3–24 alphanumeric and hyphen → `REVIEW-45` - title/short_description/description within limits → `REVIEW-50` AES: - round-trip decrypt; ciphertext never in GET → `QUALITY-125`, `SETTINGS-75` - missing key refuses startup → `SETTINGS-25` API contract / health / draft: - `GET /api/health` shape and non-200 on DB/volume failure → `APP-45` - `POST /api/v1/slabs` multipart returns 201 calibrated → `docs/contracts/API.md`, `APP-5` ## 4. Gate C and Woo - `SLAB-UAT-*` creates a draft regardless of setting → `PUBLISH-10` - duplicate SKU before create returns 409 with `edit_sku` / `open_existing` → `PUBLISH-15` - test-woo ok, resyncs taxonomy → `SETTINGS-40` - WOO_BASE_URL read-only, PUT rejected → `SETTINGS-5`, `SETTINGS-85` - startup refuses missing or http WOO_BASE_URL → `SETTINGS-10` - Woo create stores `woo_product_id`, purges images → `PUBLISH-25` - Woo 409 mapped to field recovery, draft retained → `PUBLISH-15` - taxonomy anchor unchanged on no-op, bumps on change → `SETTINGS-55` - failed publish marks failed, keeps photos, writes sync_log, retries to success → `PUBLISH-30` ## 5. Inference - with inference off: Call 1 skipped, no inference calls, suite passes → `METADATA-75`, `QUALITY-20` - test-inference returns ok, mode stateful/stateless, vision_capable → `SETTINGS-45` - test-inference fail blocks Call 2 only → `SETTINGS-45` - Call 1 at or above 0.7 pre-fills; below stays empty; override wins → `METADATA-45`, `METADATA-50` - Call 1 sends clean processed photos at 1024, synced taxonomy, user metadata → `METADATA-35` - Call 2 user-triggered only, fresh full context → `METADATA-55`, `METADATA-60` - model does not invent dimensions → `METADATA-65` - generation logged → `METADATA-66` - prompts stored with revisions → `METADATA-105` ## 6. UI end-to-end (UIE / E2E) - capture controls, overlay, axis, numbers, ready gates → `CAPTURE-40`, `CAPTURE-45`, `CAPTURE-50`, `CAPTURE-60`, `CAPTURE-70`, `CAPTURE-80`, `CAPTURE-85`, `CAPTURE-90`, `REVIEW-5`, `REVIEW-20` - review pre-fill and editability → `METADATA-45`, `REVIEW-40` - duplicate SKU, stale value, success, failure messages → `PUBLISH-15`, `PUBLISH-20`, `PUBLISH-25`, `PUBLISH-30`, `PUBLISH-35` - inference-off content path, Generate text disabled → `METADATA-70`, `METADATA-110` - settings screen and read-only URL → `SETTINGS-5`, `SETTINGS-15`, `SETTINGS-20`, `SETTINGS-30`, `SETTINGS-35`, `SETTINGS-65` - cross-browser capture through publish → `QUALITY-115` ## 7. API surface - revisions and conflict → `APP-20` - PUT stores derived, no recompute → `APP-40` - secrets absent from GET → `SETTINGS-75` - infer-taxon/content 202 and poll; timeout → `METADATA-35`, `METADATA-115` - publish preconditions → `REVIEW-5` - taxonomy sync, anchor, cache retention → `SETTINGS-50`, `SETTINGS-55` - delete soft, published undeletable → `docs/contracts/API.md` - test-woo fails closed → `SETTINGS-80` - taxonomy re-pull and 1 hour publish refresh → `SETTINGS-60` - species picker synced only → `METADATA-5` ## 8. Ops and instrumentation - compose health, inventory, test-woo, draft status → `OPS-15`, `SETTINGS-40`, `SETTINGS-65`, `APP-45` - backup and restore retention and cycle → `OPS-30`, `OPS-35` - health non-200 on DB/volume, `woo_reachable` false → `APP-45` - request id in logs → `APP-55` - inference_log and sync_log written → `METADATA-66`, `PUBLISH-30` ## 9. Purged cases Removed because the behavior was superseded: four-knob cases (edge offset, feather), the shared validation module and its 412 path, `previous_response_id` and the per-slab thread, "inventory photos" as the Call 1 input, prompt files edited without restart, 80 percent / 1600 px crop values, and manual override of the computed fields. ## 10. New cases - keyboard tab order, focus ring, and 320 px reflow → `APP-60` - no secret material in any log output → `APP-65` ## 11. Rules that do not change Inference is optional and the full suite passes with it off. `bdft = sqft × thickness_in`, no divide-by-12. The client is authoritative for deterministic math; the server stores the numbers. No new product locks live in this document.