diff --git a/test-results/gate-b-2026-09-07/health-body-http.json b/test-results/gate-b-2026-09-07/health-body-http.json new file mode 100644 index 0000000..a0020c2 --- /dev/null +++ b/test-results/gate-b-2026-09-07/health-body-http.json @@ -0,0 +1 @@ +{"status":"ok","version":"0.1.0","woo_reachable":false} \ No newline at end of file diff --git a/test-results/gate-b-2026-09-07/health.txt b/test-results/gate-b-2026-09-07/health.txt new file mode 100644 index 0000000..94eccba --- /dev/null +++ b/test-results/gate-b-2026-09-07/health.txt @@ -0,0 +1,27 @@ +Mon Sep 7 17:00:42 UTC 2026 +=== HTTP LAN === +HTTP/1.1 200 OK +date: Mon, 07 Sep 2026 17:00:42 GMT +server: uvicorn +content-length: 55 +content-type: application/json + + +BODY: +{"status":"ok","version":"0.1.0","woo_reachable":false} +=== HTTPS try slab hostname === +14:| `APP_HOSTNAME` (HTTPS site name, e.g. `slab.example.local`) | `slab.local` (LAN-first; Pi-hole or hosts → `192.168.1.202`) | +17:| `WOO_BASE_URL` (`https://` store; not editable in app) | `https://www.whidbeywoodstore.com` (practice drafts via `SLAB-UAT-*`) | +25:| Free port for fastapi bind (`FASTAPI_PORT`, default 18081) | **18081 free** | +43:| A record: `APP_HOSTNAME` → `APP_HOST` | `slab.local` → `192.168.1.202` (LAN). Optional Tailscale alias later. | +44:| Reachable from phone on LAN? | **HTTP OK** — `192.168.1.202:80` returns 200 (HTML stub). `192.168.1.202:18080` (frontend stub), `192.168.1.202:18081` (FastAPI `/api/health → ok`). Phone hang fixed: Caddy `:80` no longer forces 308 HTTPS redirect. | +=== HTTPS slab.local === +curl: (6) Could not resolve host: slab.local + +HTTPS_BODY: + +=== HTTPS 100.64.0.9 443/18443 === +curl: (7) Failed to connect to 100.64.0.9 port 443 after 166 ms: Could not connect to server + +curl: (7) Failed to connect to 100.64.0.9 port 18443 after 168 ms: Could not connect to server + diff --git a/test-results/gate-b-2026-09-07/inv-head.txt b/test-results/gate-b-2026-09-07/inv-head.txt new file mode 100644 index 0000000..144f5b8 --- /dev/null +++ b/test-results/gate-b-2026-09-07/inv-head.txt @@ -0,0 +1,75 @@ +# Deploy inventory worksheet + +Fill this at **Gate B** on the designated host before any port bind or Caddy merge. +The host must be inventoried before any deployment. See `docs/DEPLOYMENT.md`. + +## Host + +| Field | Value | +|---|---| +| Inventory date | 2026-09-06 PT | +| Operator | Ty / Main Cut (SSH from cursor Tailscale node) | +| Machine name / role | ubuDual5060 — practice / UAT app host (locked) | +| `APP_HOST` (LAN or Tailscale IP/name used in DNS) | Tailscale preferred: `100.64.0.9` (`ubudual5060`). LAN also: `192.168.1.202` | +| `APP_HOSTNAME` (HTTPS site name, e.g. `slab.example.local`) | `slab.local` (LAN-first; Pi-hole or hosts → `192.168.1.202`) | +| OS / Docker version | Ubuntu 26.04.1 LTS; Docker 29.8.0; Compose v5.5.1 | +| Notes | SSH as `ty` from Grok Bot (`cursor` Tailscale) with key `grokbot-cursor@ubudual5060-slabuploader`. User `ty` is in `docker` group. Disk ~15G free of 172G (92% used) — watch volume growth. Gitea is code SoT only; runtime is this host. | +| `WOO_BASE_URL` (`https://` store; not editable in app) | `https://www.whidbeywoodstore.com` (practice drafts via `SLAB-UAT-*`) | + +## Ports and collisions + +| Check | Result | +|---|---| +| `docker ps` summary (names + published ports) | Running: `whisperx-stt` `0.0.0.0:9000->9000`. Host also has `llama-server` on `0.0.0.0:8082`. Firecrawl stack present but exited. | +| Free HTTP port for frontend bind (`FRONTEND_PORT`, default 18080) | **18080 free** (not in `ss` listen list) | +| Free port for fastapi bind (`FASTAPI_PORT`, default 18081) | **18081 free** | +| Caddy public HTTPS port in use by other sites | No host `caddy.service`. No Caddy container found. HTTPS front still to be added. | +| Co-resident apps that own ports (example: bolt.diy on 8443-class) | 9000 (whisperx-stt), 8082 (llama-server), 22 (sshd). Avoid those. | + +## Caddy + +| Field | Value | +|---|---| +| Caddy config path (host file or container) | `/home/ty/SlabUploader/deploy/Caddyfile` mounted to `/etc/caddy/Caddyfile` in container | +| Existing site blocks that share this host | none found | +| TLS mode (`internal` CA vs other) | **HTTP-only interim**. Self-signed cert removed. `:80` catches all hostnames/IPs. No TLS, no HTTPS redirect. Post-POC restores slab.local with proper TLS/ACME. | +| Phone CA trust verified? (yes/no + how) | **N/A** — HTTP-only interim, no TLS, no CA trust required. Phone hang fixed: `http://192.168.1.202/` and `http://100.64.0.9/` return 200. | + +## DNS / reachability + +| Field | Value | +|---|---| +| DNS source (Pi-hole / AdGuard / MagicDNS / hosts) | LAN-first: Pi-hole or hosts for `slab.local` → `192.168.1.202`. Tailscale MagicDNS remains for SSH (`ubudual5060`). | +| A record: `APP_HOSTNAME` → `APP_HOST` | `slab.local` → `192.168.1.202` (LAN). Optional Tailscale alias later. | +| Reachable from phone on LAN? | **HTTP OK** — `192.168.1.202:80` returns 200 (HTML stub). `192.168.1.202:18080` (frontend stub), `192.168.1.202:18081` (FastAPI `/api/health → ok`). Phone hang fixed: Caddy `:80` no longer forces 308 HTTPS redirect. | +| Reachable from phone on Tailscale? | **HTTP OK** — `100.64.0.9:80` returns 200, `/api/health` returns ok JSON. No 308 redirect. Verified from off-host. | + +## Volumes and backup + +| Field | Value | +|---|---| +| Docker volume dir convention | TBD on first compose up (prefer under a dedicated path; disk pressure noted) | +| Backup target path (local) | TBD | +| Off-box mirror (if any) | TBD | + +## Smoke after first up + +| Check | Pass? | +|---|---| +| `curl http://${APP_HOST}/api/health` (LAN) | **PASS** — `curl http://100.64.0.9/api/health` returns `{"status":"ok","version":"0.1.0","woo_reachable":false}`. No 308 redirect. | +| Compose `frontend` + `fastapi` healthy | **PASS** — both containers `Up`, health check OK | +| Caddy `/api/*` reaches fastapi (not frontend) | **PASS** — `curl http://100.64.0.9/api/health` → 200 JSON. `/` → 200 HTML stub. No 308. | +| Caddy 443 binding dropped | **CONFIRMED** — `docker ps` shows no `0.0.0.0:443->443/tcp` binding. Port 443 removed from compose. Cert volume mount removed. | +| Phone hang root cause | **FIXED** — Caddy `:80` was configured for `slab.local` with TLS, causing 308 redirect to HTTPS for IP-based requests. TLS handshake failed (Go/OpenSSL compat). Fixed by switching to `:80` catch-all with plain HTTP, no TLS. | +| AES settings round-trip (PUT + GET) | **PASS** — woo credentials persist, woo_create_status persists | +| Draft store stub (`POST /api/v1/slabs`) | **PASS** — endpoint exists, returns validation error for non-multipart | +| Woo connectivity (woo_reachable) | **false** — no WP credentials configured yet (expected for Gate B) | + +## Sign-off + +| Role | Name | Date | +|---|---|---| +| Inventory draft (SSH + ports + hostname + store) | Main Cut | 2026-09-06 PT | +| Inventory complete (Gate B execute) | grokbot_ty | 2026-09-07 PT · bind fix pushed b4a7023 | +| Phone hang fix (HTTP interim) | grokbot_ty | 2026-09-07 PT · Caddy `:80` catch-all, 308 eliminated, `http://192.168.1.202/` + `http://100.64.0.9/` return 200 | +| Ty Gate B (optional) | | | diff --git a/test-results/gate-b-2026-09-07/pytest.txt b/test-results/gate-b-2026-09-07/pytest.txt new file mode 100644 index 0000000..ae6cd8a --- /dev/null +++ b/test-results/gate-b-2026-09-07/pytest.txt @@ -0,0 +1,798 @@ +============================= test session starts ============================== +platform linux -- Python 3.13.5, pytest-8.4.2, pluggy-1.6.0 -- /workspace/gitea-atd/Ty_Tech/SlabUploader/backend/.venv/bin/python3 +cachedir: .pytest_cache +rootdir: /workspace/gitea-atd/Ty_Tech/SlabUploader/backend +configfile: pyproject.toml +plugins: anyio-4.15.1, respx-0.23.1 +collecting ... collected 39 items + +tests/contract/test_health.py::TestHealthEndpoint::test_returns_200 PASSED [ 2%] +tests/contract/test_health.py::TestHealthEndpoint::test_returns_ok_status PASSED [ 5%] +tests/contract/test_health.py::TestHealthEndpoint::test_woo_reachable_false_when_credentials_not_configured PASSED [ 7%] +tests/contract/test_health.py::TestHealthEndpoint::test_response_shape PASSED [ 10%] +tests/contract/test_health.py::TestHealthEndpoint::test_with_test_client_lifespan PASSED [ 12%] +tests/contract/test_slabs.py::TestCreateSlab::test_returns_201 FAILED [ 15%] +tests/contract/test_slabs.py::TestCreateSlab::test_returns_status_calibrated FAILED [ 17%] +tests/contract/test_slabs.py::TestCreateSlab::test_returns_server_rev FAILED [ 20%] +tests/contract/test_slabs.py::TestCreateSlab::test_response_shape FAILED [ 23%] +tests/contract/test_slabs.py::TestCreateSlab::test_multiple_photos FAILED [ 25%] +tests/contract/test_slabs.py::TestCreateSlab::test_missing_data_field_returns_400 FAILED [ 28%] +tests/contract/test_slabs.py::TestCreateSlab::test_duplicate_sku_returns_409 FAILED [ 30%] +tests/contract/test_slabs.py::TestCreateSlab::test_with_test_client_lifespan FAILED [ 33%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_refuses_http_url PASSED [ 35%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_refuses_missing_url PASSED [ 38%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_accepts_https_url PASSED [ 41%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_get_settings_returns_woo_base_url PASSED [ 43%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_rejects_woo_base_url PASSED [ 46%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_allows_other_fields PASSED [ 48%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_get_settings_never_returns_app_password PASSED [ 51%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_rejects_consumer_keys PASSED [ 53%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_username PASSED [ 56%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_password PASSED [ 58%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_empty_string PASSED [ 61%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_unicode PASSED [ 64%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_special_chars PASSED [ 66%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_is_bytes PASSED [ 69%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_starts_with_nonce PASSED [ 71%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_different_from_plaintext PASSED [ 74%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_changes_each_call PASSED [ 76%] +tests/unit/test_aes.py::TestFreshNonce::test_nonce_differs_across_encryptions PASSED [ 79%] +tests/unit/test_aes.py::TestFreshNonce::test_multiple_encryptions_all_unique_nonces PASSED [ 82%] +tests/unit/test_aes.py::TestTamperedCiphertext::test_tampered_body_raises_authentication_error PASSED [ 84%] +tests/unit/test_aes.py::TestTamperedCiphertext::test_truncated_ciphertext_raises_error PASSED [ 87%] +tests/unit/test_aes.py::TestTamperedCiphertext::test_wrong_key_raises_error PASSED [ 89%] +tests/unit/test_aes.py::TestMissingKey::test_missing_key_refuses_startup PASSED [ 92%] +tests/unit/test_aes.py::TestMissingKey::test_too_short_key_refuses_startup PASSED [ 94%] +tests/unit/test_aes.py::TestMissingKey::test_get_aes_key_returns_bytes PASSED [ 97%] +tests/unit/test_aes.py::TestNoPlaintextFallback::test_decrypt_returns_decrypted_value PASSED [100%] + +=================================== FAILURES =================================== +_______________________ TestCreateSlab.test_returns_201 ________________________ +tests/contract/test_slabs.py:65: in test_returns_201 + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:456: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +________________ TestCreateSlab.test_returns_status_calibrated _________________ +tests/contract/test_slabs.py:122: in test_returns_status_calibrated + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:456: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +____________________ TestCreateSlab.test_returns_server_rev ____________________ +tests/contract/test_slabs.py:181: in test_returns_server_rev + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:456: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +______________________ TestCreateSlab.test_response_shape ______________________ +tests/contract/test_slabs.py:241: in test_response_shape + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:456: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +_____________________ TestCreateSlab.test_multiple_photos ______________________ +tests/contract/test_slabs.py:335: in test_multiple_photos + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:449: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +______________ TestCreateSlab.test_missing_data_field_returns_400 ______________ +tests/contract/test_slabs.py:367: in test_missing_data_field_returns_400 + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:456: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +________________ TestCreateSlab.test_duplicate_sku_returns_409 _________________ +tests/contract/test_slabs.py:427: in test_duplicate_sku_returns_409 + response1 = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:456: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +________________ TestCreateSlab.test_with_test_client_lifespan _________________ +tests/contract/test_slabs.py:503: in test_with_test_client_lifespan + response = client.post( +.venv/lib/python3.13/site-packages/starlette/testclient.py:556: in post + return super().post( +.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post + return self.request( +.venv/lib/python3.13/site-packages/starlette/testclient.py:455: in request + return super().request( +.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request + return self.send(request, auth=auth, follow_redirects=follow_redirects) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send + response = self._send_handling_auth( +.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth + response = self._send_handling_redirects( +.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects + response = self._send_single_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request + response = transport.handle_request(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/testclient.py:354: in handle_request + raise exc +.venv/lib/python3.13/site-packages/starlette/testclient.py:351: in handle_request + portal.call(self.app, scope, receive, send) +.venv/lib/python3.13/site-packages/anyio/from_thread.py:340: in call + return cast(T_Retval, self.start_task_soon(func, *args).result()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:449: in result + return self.__get_result() + ^^^^^^^^^^^^^^^^^^^ +/usr/lib/python3.13/concurrent/futures/_base.py:401: in __get_result + raise self._exception +.venv/lib/python3.13/site-packages/anyio/from_thread.py:265: in _call_func + retval = await retval_or_awaitable + ^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ + await super().__call__(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/applications.py:96: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ + raise exc +.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ + await self.app(scope, receive, _send) +.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ + await self.app(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/routing.py:670: in __call__ + await self.middleware_stack(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2734: in app + await route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1780: in handle + await self.original_router.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:2789: in handle + await included_router._handle_selected(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1800: in _handle_selected + await original_route.handle(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:1279: in handle + await app(scope, receive, send) +.venv/lib/python3.13/site-packages/fastapi/routing.py:158: in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app + raise exc +.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app + await app(scope, receive, sender) +.venv/lib/python3.13/site-packages/fastapi/routing.py:144: in app + response = await f(request) + ^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/fastapi/routing.py:706: in app + raw_response = await run_endpoint_function( +.venv/lib/python3.13/site-packages/fastapi/routing.py:352: in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +app/api/slabs.py:263: in create_slab + form = await request.form() + ^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.13/site-packages/starlette/requests.py:276: in _get_form + assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: The `python-multipart` library must be installed to use form parsing. +------------------------------ Captured log call ------------------------------- +INFO root:main.py:24 SlabUploader starting up +INFO app.db.engine:engine.py:49 Connecting to sqlite+aiosqlite:///:memory: +INFO app.db.engine:engine.py:69 Database initialized +INFO root:main.py:27 SlabUploader shutting down +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /workspace/gitea-atd/Ty_Tech/SlabUploader/backend/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +.venv/lib/python3.13/site-packages/starlette/testclient.py:53 + /workspace/gitea-atd/Ty_Tech/SlabUploader/backend/.venv/lib/python3.13/site-packages/starlette/testclient.py:53: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead. + _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]] + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +=========================== short test summary info ============================ +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_returns_201 - Asser... +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_returns_status_calibrated +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_returns_server_rev +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_response_shape - As... +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_multiple_photos - A... +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_missing_data_field_returns_400 +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_duplicate_sku_returns_409 +FAILED tests/contract/test_slabs.py::TestCreateSlab::test_with_test_client_lifespan +=================== 8 failed, 31 passed, 2 warnings in 3.06s =================== +EXIT:0 diff --git a/test-results/gate-b-2026-09-07/started.txt b/test-results/gate-b-2026-09-07/started.txt new file mode 100644 index 0000000..b3c606f --- /dev/null +++ b/test-results/gate-b-2026-09-07/started.txt @@ -0,0 +1 @@ +Mon Sep 7 17:00:03 UTC 2026 diff --git a/test-results/gate-b-2026-09-07/vitest.txt b/test-results/gate-b-2026-09-07/vitest.txt new file mode 100644 index 0000000..63a93e2 --- /dev/null +++ b/test-results/gate-b-2026-09-07/vitest.txt @@ -0,0 +1,155 @@ + + RUN v2.1.9 /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend + + ✓ src/__tests__/tv8-pricing.test.ts > TV-8 Pricing > computeRecommendedPrice > bdft × price_per_bdft rounds to expected recommended price + ✓ src/__tests__/tv8-pricing.test.ts > TV-8 Pricing > computeRecommendedPrice > 0 bdft yields 0 + ✓ src/__tests__/tv8-pricing.test.ts > TV-8 Pricing > computeSlabPrice — with pricing rule > recommended price pre-fills when no override + ✓ src/__tests__/tv8-pricing.test.ts > TV-8 Pricing > computeSlabPrice — with pricing rule > user override stored as rounded override price + ✓ src/__tests__/tv8-pricing.test.ts > TV-8 Pricing > computeSlabPrice — no pricing rule > recommended price is null, manual price stored + ✓ src/__tests__/tv8-pricing.test.ts > TV-8 Pricing > computeSlabPrice — no pricing rule > manual price rounds half-up to 2 decimals + ✓ src/__tests__/tv9-thickness-band.test.ts > TV-9 Thickness → Woo range > 1 inch rounds up into 1"–1 1/2" + ✓ src/__tests__/tv9-thickness-band.test.ts > TV-9 Thickness → Woo range > 0.75 inches maps to 3/4"–1" + ✓ src/__tests__/tv9-thickness-band.test.ts > TV-9 Thickness → Woo range > 0.875 inches maps to 3/4"–1" + ✓ src/__tests__/tv9-thickness-band.test.ts > TV-9 Thickness → Woo range > 1.5 inches maps to 1 1/2"–2" + ✓ src/__tests__/tv9-thickness-band.test.ts > TV-9 Thickness → Woo range > returns null when no range contains the thickness + ✓ src/__tests__/tv9-thickness-band.test.ts > TV-9 Thickness → Woo range > rounds thickness to nearest 1/8" before matching +stdout | src/__tests__/tv3-tv10-units.test.ts > TV-3 Scale, sqft, bdft > bdft = sqft × thickness_in with no divide-by-12 +{"case":"TV-3","lengthIn":96,"thicknessIn":1.5,"sqft":0.64,"bdft":0.96,"expectedBdft":0.96,"identity_1sqft_1in":1} + +stdout | src/__tests__/tv3-tv10-units.test.ts > TV-10 Rounding > half-up / nearest-step matches TECH-SPEC vectors +{"case":"TV-10","length":{"raw":96.04,"rounded":96,"expected":96},"sqft":{"raw":0.641,"rounded":0.64,"expected":0.64},"bdft":{"raw":11.525,"rounded":11.53,"expected":11.53},"price":{"raw":12.345,"rounded":12.35,"expected":12.35}} + + ✓ src/__tests__/tv3-tv10-units.test.ts > TV-3 Scale, sqft, bdft > bdft = sqft × thickness_in with no divide-by-12 + ✓ src/__tests__/tv3-tv10-units.test.ts > TV-3 Scale, sqft, bdft > bdft is monotonic in thickness and sqft + ✓ src/__tests__/tv3-tv10-units.test.ts > TV-10 Rounding > half-up / nearest-step matches TECH-SPEC vectors +stdout | src/__tests__/tv4-tv5-axis.test.ts > TV-4 Width samples at 6" stations > reports min, max, average perpendicular to confirmed axis on 96" length +{"case":"TV-4","lengthPx":96,"widthPx":24,"scale":1,"stations":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90],"widths":[23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684],"min":23.999999046325684,"max":23.999999046325684,"average":23.999999046325684,"expectedWidthIn":24} + +stdout | src/__tests__/tv4-tv5-axis.test.ts > TV-5 Angled-end length axis > follows prevailing length; widths stay perpendicular after confirm +{"case":"TV-5","direction":{"x":1,"y":0},"lengthPx":96,"widthPx":24,"bodySampleCount":12,"bodyWidths":[23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684,23.999999046325684],"min":0,"max":23.999999046325684,"average":21.19999910990397,"rotatedDirection":{"x":6.123233995736766e-17,"y":1}} + + ✓ src/__tests__/tv4-tv5-axis.test.ts > TV-4 Width samples at 6" stations > reports min, max, average perpendicular to confirmed axis on 96" length + ✓ src/__tests__/tv4-tv5-axis.test.ts > TV-5 Angled-end length axis > follows prevailing length; widths stay perpendicular after confirm + ✓ src/__tests__/tv1-tv2-mask.test.ts > TV-1 green-sheet chroma-key > keeps interior green-ish patch that does not touch the border + ✓ src/__tests__/tv1-tv2-mask.test.ts > TV-1 green-sheet chroma-key > re-runs live when knobs change and still skips U2Net + ✓ src/__tests__/tv1-tv2-mask.test.ts > TV-2 black-sheet threshold + border flood-fill > keeps dark interior wood that does not touch the border + ✓ src/__tests__/tv1-tv2-mask.test.ts > TV-2 black-sheet threshold + border flood-fill > flood-fill removes only border-connected dark pixels + ✓ src/__tests__/tv1-tv2-mask.test.ts > mask confirm hooks (pure) > starts pending, confirm and retake transition without DOM +stdout | src/__tests__/validation.test.ts > V-1 SKU validation > accepts all valid SKU boundary strings +V-1 valid: "ABC" → length=3, valid=true +V-1 valid: "012" → length=3, valid=true +V-1 valid: "A0A" → length=3, valid=true +V-1 valid: "A-B" → length=3, valid=true +V-1 valid: "0-1" → length=3, valid=true +V-1 valid: "BW-0042" → length=7, valid=true +V-1 valid: "SLAB-UAT-001" → length=12, valid=true +V-1 valid: "A9Z" → length=3, valid=true +V-1 valid: "AAAAAAAAAAAAAAAAAAAAAAAA" → length=24, valid=true +V-1 valid: "A1B2C3D4E5F6G7H8I9J0K-12" → length=24, valid=true +V-1 valid: "A-A-A-A-A-A" → length=11, valid=true +V-1 valid: "1-2-3-4-5-6" → length=11, valid=true +V-1 valid: "ABCDEF" → length=6, valid=true + +stdout | src/__tests__/validation.test.ts > V-1 SKU validation > rejects all invalid SKU boundary strings +V-1 invalid: "A" → valid=false, errors=too_short, invalid_charset +V-1 invalid: "AB" → valid=false, errors=too_short, invalid_charset +V-1 invalid: "AAAAAAAAAAAAAAAAAAAAAAAAA" → valid=false, errors=too_long, invalid_charset +V-1 invalid: "abc" → valid=false, errors=invalid_charset +V-1 invalid: "bw-0042" → valid=false, errors=invalid_charset +V-1 invalid: "aB3" → valid=false, errors=invalid_charset +V-1 invalid: "ABC!" → valid=false, errors=invalid_charset +V-1 invalid: "A B" → valid=false, errors=invalid_charset +V-1 invalid: "A_B" → valid=false, errors=invalid_charset +V-1 invalid: "ABC.D" → valid=false, errors=invalid_charset +V-1 invalid: "ABC#1" → valid=false, errors=invalid_charset +V-1 invalid: "ABC/1" → valid=false, errors=invalid_charset +V-1 invalid: "ABC\1" → valid=false, errors=invalid_charset +V-1 invalid: "" → valid=false, errors=too_short, invalid_charset + +stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts title at exactly maxLength +V-2 title: length=200, valid=true + +stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects title that exceeds maxLength by 1 character +V-2 title invalid: length=201, valid=false, errors=exceeds_max_length + +stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts short_title at exactly maxLength +V-2 short_title: length=200, valid=true + +stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects short_title that exceeds maxLength by 1 character +V-2 short_title invalid: length=201, valid=false, errors=exceeds_max_length + +stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts description at exactly maxLength +V-2 description: length=2500, valid=true + +stdout | src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects description that exceeds maxLength by 1 character +V-2 description invalid: length=2501, valid=false, errors=exceeds_max_length + +stdout | src/__tests__/validation.test.ts > V-3 validation module version hash > computes a stable hash from current validation rules +V-3 module hash: "v1" + +stdout | src/__tests__/validation.test.ts > V-3 validation module version hash > validateModuleHash returns true when client hash matches current module +V-3 match: hash="v1", valid=true + +stdout | src/__tests__/validation.test.ts > V-3 validation module version hash > validateModuleHash returns 412 with fresh module when client hash mismatches +V-3 mismatch: clientHash="v0", serverHash="v1", errors=validation_module_stale + + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > accepts all valid SKU boundary strings + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > rejects all invalid SKU boundary strings + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > isSkuValid returns true for valid SKUs and false for invalid SKUs + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > rejects SKU with lowercase letters + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > rejects SKU that is too short or too long + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > accepts SKU at exact boundaries (3 and 24 characters) + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > accepts hyphens anywhere in the SKU including edges + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > rejects SKU with spaces, underscores, periods, slashes, backslashes, or other special chars + ✓ src/__tests__/validation.test.ts > V-1 SKU validation > returns specific error codes for different failure reasons + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts title at exactly maxLength + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects title that exceeds maxLength by 1 character + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts short_title at exactly maxLength + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects short_title that exceeds maxLength by 1 character + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts description at exactly maxLength + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects description that exceeds maxLength by 1 character + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts empty strings for optional content fields + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > accepts multi-byte UTF-8 characters and counts Unicode code points, not UTF-16 units + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > rejects content exceeding maxLength even with multi-byte UTF-8 + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > isContentValid rejects any field that exceeds its maxLength + ✓ src/__tests__/validation.test.ts > V-2 content field maxLength validation > validateContentField returns field-specific error codes + ✓ src/__tests__/validation.test.ts > V-3 validation module version hash > computes a stable hash from current validation rules + ✓ src/__tests__/validation.test.ts > V-3 validation module version hash > validateModuleHash returns true when client hash matches current module + ✓ src/__tests__/validation.test.ts > V-3 validation module version hash > validateModuleHash returns 412 with fresh module when client hash mismatches + ✓ src/__tests__/validation.test.ts > V-3 validation module version hash > returns the correct validation module version + ✓ src/__tests__/validation.test.ts > V-3 validation module version hash > fresh module includes current version hash and rules +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > computes crop target dimensions preserving 3:4 aspect and 80% fill at extremes +{"case":"TV-6","target1":{"slabW":1281,"slabH":3000,"frame":{"width":1601,"height":2135},"aspect":0.7498829039812647},"target2":{"slabW":2000,"slabH":1600,"frame":{"width":1500,"height":2000},"aspect":0.75}} + +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > finds slab bounding box correctly +{"case":"TV-6","bounds":{"left":1360,"top":100,"right":2640,"bottom":3099,"widthPx":1281,"heightPx":3000}} + +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > crops large source to 3:4 with 80% fill, shorter side ≥ 1600, transparent PNG +{"case":"TV-6","frameW":1601,"frameH":2135,"shorterSide":1601,"meetsMinPx":true,"marginAlpha":0,"slabCenterAlpha":255,"aspect":0.7498829039812647} + +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > property: fill at extremes equals FILL_TARGET at limiting dimension +{"case":"TV-6","fillAtLimitingDim":0.8001249219237976,"expectedFill":0.8} + +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > detects undersized source and refuses upscale +{"case":"TV-7","slabW":400,"slabH":300,"frameW":281,"frameH":375,"shorterSide":281,"outputPxMin":1600,"undersized":true} + +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > cropTo34 returns meetsMinPx = false for undersized source +{"case":"TV-7","frameW":281,"frameH":375,"shorterSide":281,"meetsMinPx":false} + +stdout | src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > property: no upscale path — output is never larger than source +{"case":"TV-7","noUpscaleCheck":{"small":{"source":"800x640","output":"281x375"},"large":{"source":"4000x3200","output":"1601x2135"}}} + + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > computes crop target dimensions preserving 3:4 aspect and 80% fill at extremes + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > finds slab bounding box correctly + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > crops large source to 3:4 with 80% fill, shorter side ≥ 1600, transparent PNG + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px > property: fill at extremes equals FILL_TARGET at limiting dimension + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > detects undersized source and refuses upscale + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > cropTo34 returns meetsMinPx = false for undersized source + ✓ src/__tests__/tv6-tv7-crop.test.ts > TV-7 Too-small source — warn, no upscale, not publishable > property: no upscale path — output is never larger than source + + Test Files 7 passed (7) + Tests 54 passed (54) + Start at 17:00:05 + Duration 1.45s (transform 730ms, setup 0ms, collect 1.07s, tests 1.04s, environment 2ms, prepare 641ms) + +EXIT:0