diff --git a/test-results/gate-b-2026-09-07-pass2/RESULTS.md b/test-results/gate-b-2026-09-07-pass2/RESULTS.md new file mode 100644 index 0000000..dde777f --- /dev/null +++ b/test-results/gate-b-2026-09-07-pass2/RESULTS.md @@ -0,0 +1,50 @@ +# Gate B pass2 results — 2026-09-07 + +Evidence collected on gate-b @ 302d694. + +--- + +## Results + +| Case | What this proves (mill-owner English) | Evidence class | Result | Evidence | Blocker | +|---|---|---|---|---|---| +| TV-1 | Green-sheet chroma-key keeps interior green patch, does not eat border wood | `proxy-synthetic` | pass | vitest.txt:13 | — | +| TV-2 | Black-sheet threshold keeps dark interior wood, flood-fill isolates border-only | `proxy-synthetic` | pass | vitest.txt:13 | — | +| TV-3 | bdft = sqft × thickness_in; 1 sqft @ 1" = 1 bdft; no divide-by-12 | `product-state` | blocked | — | No real slab fixtures or Ty walk on real wood | +| TV-3 | bdft identity holds on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:4–5,12 | — | +| TV-4 | Width samples at 6" stations report min, max, average perpendicular to confirmed axis | `proxy-synthetic` | pass | vitest.txt:14–15,20 | — | +| TV-5 | Angled-end slab: length axis follows prevailing length; widths stay perpendicular after confirm | `proxy-synthetic` | pass | vitest.txt:17–18,20 | — | +| TV-6 | 3:4 crop, slab centered, 80% fill, transparent PNG, shorter side ≥ 1600 when source allows | `product-state` | blocked | — | No real slab photos or Ty walk on real wood | +| TV-6 | Crop dimensions and fill ratio hold on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:80–90,101 | — | +| TV-7 | Undersized source warns, refuses upscale, photo not publishable until retake | `product-state` | blocked | — | No real slab photos or Ty walk on real wood | +| TV-7 | Undersized-source logic holds on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:92–99,101 | — | +| TV-8 | Pricing: bdft 0.96 × $12.50 → $12.00 recommended; override stored; no rule leaves empty | `proxy-synthetic` | pass | vitest.txt:11 | — | +| TV-9 | Thickness 1" rounds up to Woo range using synced attribute term, not hardcoded label | `proxy-synthetic` | pass | vitest.txt:10 | — | +| TV-10 | Rounding: 96.04" → 96"; 0.641 sqft → 0.64; 11.525 bdft → 11.53; 12.345 price → 12.35 | `product-state` | blocked | — | No real slab measurements or Ty walk on real wood | +| TV-10 | Rounding rules hold on synthetic fixture (proxy proof only) | `proxy-synthetic` | pass | vitest.txt:7–8,12 | — | +| V-1 | SKU charset `[A-Z0-9-]{3,24}` accepted; invalid chars/rejected length rejected | `proxy-synthetic` | pass | vitest.txt:21–50,79 | — | +| V-2 | Title/short_title/description maxLength enforced at field exit and submit | `proxy-synthetic` | pass | vitest.txt:52–68,79 | — | +| V-3 | Validation module hash: client cache, submit carry, mismatch returns 412 with fresh module | `proxy-synthetic` | pass | vitest.txt:70–77,79 | — | +| AES-1 | Credentials round-trip without ciphertext leak; settings GET never returns app password | `api-contract` | pass | pytest.txt:46 | — | +| AES-1 | Encrypt-then-decrypt round-trips for username, password, empty, unicode, special chars | `proxy-synthetic` | pass | pytest.txt:9–17 | — | +| AES-2 | Missing SLAB_AES_KEY env var: app refuses to start; no plaintext fallback | `proxy-synthetic` | pass | pytest.txt:23–24 | — | +| H-1 | `GET /api/health` returns 200 `{ "status":"ok" }` with `woo_reachable` false when not configured | `api-contract` | pass | pytest.txt:27–31 | — | +| D-1 | `POST /api/v1/slabs` accepts multipart, returns 201 with `status: calibrated` and `server_rev` | `api-contract` | fail | pytest.txt:32–39 | [#29](https://gitea.vps1.afterthedemo.com/Ty_Tech/SlabUploader/issues/29) | +| OPS-1 | `GET https://${APP_HOSTNAME}/api/health` returns ok after compose up with filled inventory | `deploy` | blocked | — | health.txt: slab.local unresolved, 443/18443 refuse, HTTP :18081 timed out | +| GC-5 | FastAPI refuses to start when WOO_BASE_URL missing or set to `http://` | `proxy-synthetic` | pass | pytest.txt:40–41 | — | + +--- + +## Counts + +**17 pass / 1 fail / 5 blocked** (23 total rows) + +--- + +## Unproven / Gate exit + +- **TV-3, TV-6, TV-7, TV-10 product-state**: No real slab fixtures, no Ty walk. Proxy-synthetic rows pass but do not satisfy Gate B exit. +- **OPS-1 deploy**: No compose-up, no Caddy, no inventory filled. Health endpoints all unreachable (health.txt). +- **Walkable UI / E2E**: No Playwright E2E tests exist yet; no browser session captured. +- **Real draft create (D-1)**: All 8 TestCreateSlab tests failed with `python-multipart` missing; no slab was created. +- **Gate B is not green. UAT is not ready.** Bots do not declare Gate B green. diff --git a/test-results/gate-b-2026-09-07-pass2/health.txt b/test-results/gate-b-2026-09-07-pass2/health.txt new file mode 100644 index 0000000..722ac24 --- /dev/null +++ b/test-results/gate-b-2026-09-07-pass2/health.txt @@ -0,0 +1,16 @@ +Mon Sep 7 18:02:48 UTC 2026 +=== HTTP LAN :18081 === +curl: (28) Connection timed out after 3002 milliseconds +HTTP_FAIL:28 + +=== HTTPS slab.local === +curl: (6) Could not resolve host: slab.local +HTTPS_DNS_FAIL:6 + +=== HTTPS 100.64.0.9:443 === +curl: (7) Failed to connect to 100.64.0.9 port 443 after 652 ms: Could not connect to server +HTTPS443_FAIL:7 + +=== HTTPS 100.64.0.9:18443 === +curl: (7) Failed to connect to 100.64.0.9 port 18443 after 147 ms: Could not connect to server +HTTPS18443_FAIL:7 diff --git a/test-results/gate-b-2026-09-07-pass2/pytest.txt b/test-results/gate-b-2026-09-07-pass2/pytest.txt new file mode 100644 index 0000000..829118c --- /dev/null +++ b/test-results/gate-b-2026-09-07-pass2/pytest.txt @@ -0,0 +1,1346 @@ +============================= 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/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_username PASSED [ 2%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_password PASSED [ 5%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_empty_string PASSED [ 7%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_unicode PASSED [ 10%] +tests/unit/test_aes.py::TestAESRoundTrip::test_roundtrip_special_chars PASSED [ 12%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_is_bytes PASSED [ 15%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_starts_with_nonce PASSED [ 17%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_different_from_plaintext PASSED [ 20%] +tests/unit/test_aes.py::TestAESRoundTrip::test_ciphertext_changes_each_call PASSED [ 23%] +tests/unit/test_aes.py::TestFreshNonce::test_nonce_differs_across_encryptions PASSED [ 25%] +tests/unit/test_aes.py::TestFreshNonce::test_multiple_encryptions_all_unique_nonces PASSED [ 28%] +tests/unit/test_aes.py::TestTamperedCiphertext::test_tampered_body_raises_authentication_error PASSED [ 30%] +tests/unit/test_aes.py::TestTamperedCiphertext::test_truncated_ciphertext_raises_error PASSED [ 33%] +tests/unit/test_aes.py::TestTamperedCiphertext::test_wrong_key_raises_error PASSED [ 35%] +tests/unit/test_aes.py::TestMissingKey::test_missing_key_refuses_startup PASSED [ 38%] +tests/unit/test_aes.py::TestMissingKey::test_too_short_key_refuses_startup PASSED [ 41%] +tests/unit/test_aes.py::TestMissingKey::test_get_aes_key_returns_bytes PASSED [ 43%] +tests/unit/test_aes.py::TestNoPlaintextFallback::test_decrypt_returns_decrypted_value PASSED [ 46%] +tests/contract/test_health.py::TestHealthEndpoint::test_returns_200 PASSED [ 48%] +tests/contract/test_health.py::TestHealthEndpoint::test_returns_ok_status PASSED [ 51%] +tests/contract/test_health.py::TestHealthEndpoint::test_woo_reachable_false_when_credentials_not_configured PASSED [ 53%] +tests/contract/test_health.py::TestHealthEndpoint::test_response_shape PASSED [ 56%] +tests/contract/test_health.py::TestHealthEndpoint::test_with_test_client_lifespan PASSED [ 58%] +tests/contract/test_slabs.py::TestCreateSlab::test_returns_201 FAILED [ 61%] +tests/contract/test_slabs.py::TestCreateSlab::test_returns_status_calibrated FAILED [ 64%] +tests/contract/test_slabs.py::TestCreateSlab::test_returns_server_rev FAILED [ 66%] +tests/contract/test_slabs.py::TestCreateSlab::test_response_shape FAILED [ 69%] +tests/contract/test_slabs.py::TestCreateSlab::test_multiple_photos FAILED [ 71%] +tests/contract/test_slabs.py::TestCreateSlab::test_missing_data_field_returns_400 FAILED [ 74%] +tests/contract/test_slabs.py::TestCreateSlab::test_duplicate_sku_returns_409 FAILED [ 76%] +tests/contract/test_slabs.py::TestCreateSlab::test_with_test_client_lifespan FAILED [ 79%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_refuses_http_url PASSED [ 82%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_refuses_missing_url PASSED [ 84%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlValidation::test_accepts_https_url PASSED [ 87%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_get_settings_returns_woo_base_url PASSED [ 89%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_rejects_woo_base_url PASSED [ 92%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_allows_other_fields PASSED [ 94%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_get_settings_never_returns_app_password PASSED [ 97%] +tests/contract/test_woo_base_url.py::TestWooBaseUrlReadOnly::test_put_settings_rejects_consumer_keys PASSED [100%] + +=================================== FAILURES =================================== +_______________________ TestCreateSlab.test_returns_201 ________________________ + +self = + + def test_returns_201(self) -> None: + """D-1: POST /api/v1/slabs returns 201 on valid multipart upload.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + data_json = json.dumps({ + "id": slab_id, + "sku": "BW-0042", + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:65: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 _________________ + +self = + + def test_returns_status_calibrated(self) -> None: + """D-1: Response contains status: calibrated.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + data_json = json.dumps({ + "id": slab_id, + "sku": "BW-0099", + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:122: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 ____________________ + +self = + + def test_returns_server_rev(self) -> None: + """D-1: Response contains server_rev.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + data_json = json.dumps({ + "id": slab_id, + "sku": "BW-0077", + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:181: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 ______________________ + +self = + + def test_response_shape(self) -> None: + """D-1: Response contains required fields.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + data_json = json.dumps({ + "id": slab_id, + "sku": "BW-0033", + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:241: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 ______________________ + +self = + + def test_multiple_photos(self) -> None: + """D-1: Multiple files[] and meta[] pairs are accepted.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + data_json = json.dumps({ + "id": slab_id, + "sku": "BW-0055", + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ("files[]", ("extra1.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:335: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 ______________ + +self = + + def test_missing_data_field_returns_400(self) -> None: + """D-1: Missing data field returns 400.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:367: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 _________________ + +self = + + def test_duplicate_sku_returns_409(self) -> None: + """D-1: Duplicate SKU returns 409.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + sku = "DUPLICATE-TEST" + data_json = json.dumps({ + "id": slab_id, + "sku": sku, + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: + # Create first slab +> response1 = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:427: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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 _________________ + +self = + + def test_with_test_client_lifespan(self) -> None: + """FUND-9: TestClient with app manages lifespan correctly for slabs.""" + settings = Settings( + aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", + woo_base_url="https://test.example.com", + ) + app = create_app(settings) + + slab_id = str(uuid.uuid4()) + data_json = json.dumps({ + "id": slab_id, + "sku": "BW-0011", + "length_in": 96.0, + "thickness_in": 1.5, + "sqft": 0.64, + "bdft": 0.96, + "width_min_in": 14.0, + "width_max_in": 22.0, + "width_avg_in": 18.2, + "species_id": 42, + "wood_category_ids": [15, 23], + "edge_type_term_id": 8, + "figure_term_ids": [3], + "grade_term_ids": [7], + "thickness_term_id": 12, + "moisture_term_id": 5, + "fig_tag_ids": [99], + "feat_tag_ids": [42], + "price_per_bdft": 12.50, + "price": 12.00, + "price_source": "recommendation", + "title": None, + "short_title": None, + "description": None, + "content_source": None, + "client_rev": 1, + }) + + with TestClient(app) as client: +> response = client.post( + "/api/v1/slabs", + files=[ + ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), + ], + data={ + "data": data_json, + "meta[]": json.dumps({ + "kind": "inventory", + "role": "topdown", + "seq": 1, + }), + }, + ) + +tests/contract/test_slabs.py:503: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.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() + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = + + async def _get_form( + self, + *, + max_files: int | float = 1000, + max_fields: int | float = 1000, + max_part_size: int = 1024 * 1024, + ) -> FormData: + if self._form is None: # pragma: no branch +> assert parse_options_header is not None, ( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + "The `python-multipart` library must be installed to use form parsing." + ) +E AssertionError: The `python-multipart` library must be installed to use form parsing. + +.venv/lib/python3.13/site-packages/starlette/requests.py:276: AssertionError +------------------------------ 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.52s =================== +EXIT:1 diff --git a/test-results/gate-b-2026-09-07-pass2/started.txt b/test-results/gate-b-2026-09-07-pass2/started.txt new file mode 100644 index 0000000..1b72723 --- /dev/null +++ b/test-results/gate-b-2026-09-07-pass2/started.txt @@ -0,0 +1,2 @@ +Mon Sep 7 18:02:14 UTC 2026 +302d694 diff --git a/test-results/gate-b-2026-09-07-pass2/vitest.txt b/test-results/gate-b-2026-09-07-pass2/vitest.txt new file mode 100644 index 0000000..cdbda6e --- /dev/null +++ b/test-results/gate-b-2026-09-07-pass2/vitest.txt @@ -0,0 +1,108 @@ + + RUN v2.1.9 /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend + +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__/tv9-thickness-band.test.ts (6 tests) 5ms + ✓ src/__tests__/tv8-pricing.test.ts (6 tests) 5ms + ✓ src/__tests__/tv3-tv10-units.test.ts (3 tests) 5ms + ✓ src/__tests__/tv1-tv2-mask.test.ts (5 tests) 38ms +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 (2 tests) 43ms +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 (25 tests) 29ms +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 (7 tests) 789ms + + Test Files 7 passed (7) + Tests 54 passed (54) + Start at 18:02:58 + Duration 1.29s (transform 558ms, setup 0ms, collect 966ms, tests 914ms, environment 1ms, prepare 574ms) + +EXIT:0