chore(reset)!: strip storefront → network-service seed (v0.9.0)
ci / check (push) Has been cancelled
ci / check (push) Has been cancelled
Pivot ecomm from a generic multi-tenant Shopify-alternative storefront to the commitment-commerce + cross-maker verified referral NETWORK direction (ecomm-content/rfcs/network_strategy.md; OHM identity/relationality super-drafts). Phase A of the reset — clear the decks: - Strip the storefront surfaces with no carry-forward: the storefronts domain, the products import/export/image HTTP spine (service/imagefetch/repo + endpoints), the SPA frontend, and the Playwright e2e suite. - Keep the reusable core: /healthz + /api/auth/* (accounts); the catalog-normalization library (codec/dialect/models/serialize/validate/diff/hosted — importable, no HTTP yet); platform/* infra. Migrations untouched (append-only). - Reduce check.sh to backend-only (import-linter + pytest); trim dev.sh and the unused GCS overlay env. Repoint app.json/README/CLAUDE.md; bump VERSION 0.8.0 -> 0.9.0. check.sh green: import-linter (main > domains > platform) KEPT, pytest passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
INFO: Started server process [39018]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
INFO: Uvicorn running on http://127.0.0.1:8765 (Press CTRL+C to quit)
|
||||
INFO: 127.0.0.1:58472 - "GET /healthz HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58474 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256953768-0@example.com | Your ecomm code: 580511
|
||||
Your ecomm one-time code is 580511.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58476 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58476 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58474 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 2, "dialect": "canonical", "duration_ms": 4, "errors": 0, "event": "import_draft_created", "row_count": 3, "storefront_id": 1, "unchanged": 0, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58476 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58476 - "GET /api/products/imports/drafts/1 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /api/products/imports/drafts/1/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"added": 2, "duration_ms": 7, "errored": 0, "event": "import_run_completed", "run_id": 1, "storefront_id": 1, "updated": 0}
|
||||
INFO: 127.0.0.1:58476 - "POST /api/products/imports/drafts/1/confirm HTTP/1.1" 201 Created
|
||||
INFO:ecomm.telemetry: {"duration_ms": 2, "event": "image_phase_completed", "failed": 0, "fetched": 0, "rejected": 0, "run_id": 1}
|
||||
INFO: 127.0.0.1:58476 - "GET /api/products/imports/runs/1 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58476 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58474 - "GET /api/products/export?status=all HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"duration_ms": 4, "event": "catalog_exported", "product_count": 2, "status_filter": "all", "storefront_id": 1}
|
||||
INFO: 127.0.0.1:58474 - "GET /api/products/export?status=active HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"duration_ms": 8, "event": "catalog_exported", "product_count": 2, "status_filter": "active", "storefront_id": 1}
|
||||
INFO: 127.0.0.1:58479 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58479 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58479 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58479 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58479 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58479 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256954851-1@example.com | Your ecomm code: 391458
|
||||
Your ecomm one-time code is 391458.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58481 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58481 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58479 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 3, "dialect": "canonical", "duration_ms": 10, "errors": 0, "event": "import_draft_created", "row_count": 3, "storefront_id": 2, "unchanged": 0, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58481 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58481 - "GET /api/products/imports/drafts/2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /api/products/imports/drafts/2/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"added": 3, "duration_ms": 13, "errored": 0, "event": "import_run_completed", "run_id": 2, "storefront_id": 2, "updated": 0}
|
||||
INFO: 127.0.0.1:58481 - "POST /api/products/imports/drafts/2/confirm HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58481 - "GET /api/products/imports/runs/2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"duration_ms": 154, "event": "image_phase_completed", "failed": 1, "fetched": 1, "rejected": 1, "run_id": 2}
|
||||
INFO: 127.0.0.1:58481 - "GET /api/products/imports/runs/2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58479 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58481 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58489 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58487 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58489 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58489 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256957956-2@example.com | Your ecomm code: 342006
|
||||
Your ecomm one-time code is 342006.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58487 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58487 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58489 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 2, "dialect": "canonical", "duration_ms": 4, "errors": 0, "event": "import_draft_created", "row_count": 3, "storefront_id": 3, "unchanged": 0, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58487 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58487 - "GET /api/products/imports/drafts/3 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /api/products/imports/drafts/3/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "DELETE /api/products/imports/drafts/3 HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58489 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58487 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58494 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58492 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58494 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58494 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256958663-3@example.com | Your ecomm code: 411253
|
||||
Your ecomm one-time code is 411253.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58492 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58492 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58494 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 2, "dialect": "canonical", "duration_ms": 6, "errors": 1, "event": "import_draft_created", "row_count": 3, "storefront_id": 4, "unchanged": 0, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58492 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58492 - "GET /api/products/imports/drafts/4 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /api/products/imports/drafts/4/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /api/products/imports/drafts/4/records?limit=100&offset=0&kind=error HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"added": 2, "duration_ms": 11, "errored": 1, "event": "import_run_completed", "run_id": 3, "storefront_id": 4, "updated": 0}
|
||||
INFO: 127.0.0.1:58492 - "POST /api/products/imports/drafts/4/confirm HTTP/1.1" 201 Created
|
||||
INFO:ecomm.telemetry: {"duration_ms": 2, "event": "image_phase_completed", "failed": 0, "fetched": 0, "rejected": 0, "run_id": 3}
|
||||
INFO: 127.0.0.1:58492 - "GET /api/products/imports/runs/3 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58494 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58492 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58499 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58497 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58499 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58499 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256959226-4@example.com | Your ecomm code: 672118
|
||||
Your ecomm one-time code is 672118.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58497 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58497 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58499 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "POST /api/products/imports HTTP/1.1" 400 Bad Request
|
||||
INFO: 127.0.0.1:58499 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58497 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58503 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58501 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58503 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58503 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256959685-5@example.com | Your ecomm code: 541296
|
||||
Your ecomm one-time code is 541296.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58501 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58501 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58503 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 2, "dialect": "canonical", "duration_ms": 5, "errors": 0, "event": "import_draft_created", "row_count": 3, "storefront_id": 6, "unchanged": 0, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58501 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58501 - "GET /api/products/imports/drafts/5 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /api/products/imports/drafts/5/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"added": 2, "duration_ms": 5, "errored": 0, "event": "import_run_completed", "run_id": 4, "storefront_id": 6, "updated": 0}
|
||||
INFO: 127.0.0.1:58501 - "POST /api/products/imports/drafts/5/confirm HTTP/1.1" 201 Created
|
||||
INFO:ecomm.telemetry: {"duration_ms": 2, "event": "image_phase_completed", "failed": 0, "fetched": 0, "rejected": 0, "run_id": 4}
|
||||
INFO: 127.0.0.1:58501 - "GET /api/products/imports/runs/4 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58501 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58503 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58507 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58505 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58507 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58507 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256960220-6@example.com | Your ecomm code: 765123
|
||||
Your ecomm one-time code is 765123.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58505 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58505 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58507 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 1, "dialect": "shopify", "duration_ms": 4, "errors": 0, "event": "import_draft_created", "row_count": 1, "storefront_id": 7, "unchanged": 0, "unknown_columns_count": 4, "updates": 0}
|
||||
INFO: 127.0.0.1:58505 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58505 - "GET /api/products/imports/drafts/6 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58505 - "GET /api/products/imports/drafts/6/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"added": 1, "duration_ms": 7, "errored": 0, "event": "import_run_completed", "run_id": 5, "storefront_id": 7, "updated": 0}
|
||||
INFO: 127.0.0.1:58505 - "POST /api/products/imports/drafts/6/confirm HTTP/1.1" 201 Created
|
||||
INFO:ecomm.telemetry: {"duration_ms": 1, "event": "image_phase_completed", "failed": 0, "fetched": 0, "rejected": 0, "run_id": 5}
|
||||
INFO: 127.0.0.1:58505 - "GET /api/products/imports/runs/5 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /assets/index-BXAA3-2r.js HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58512 - "GET /assets/index-DKKZujRA.css HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
||||
INFO: 127.0.0.1:58510 - "GET /fonts/inter-v20-latin-regular.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58512 - "GET /brand/mark-mono-gold.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /brand/mark-tile.svg HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58512 - "GET /fonts/space-grotesk-v22-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /fonts/space-grotesk-v22-latin-700.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /fonts/inter-v20-latin-500.woff2 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.mailer: LogMailer -> merchant1781256960905-7@example.com | Your ecomm code: 873468
|
||||
Your ecomm one-time code is 873468.
|
||||
It is valid for 10 minutes.
|
||||
If you didn't request this, ignore this message.
|
||||
INFO: 127.0.0.1:58510 - "POST /api/auth/request-code HTTP/1.1" 204 No Content
|
||||
INFO: 127.0.0.1:58510 - "POST /api/auth/verify HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /fonts/inter-v20-latin-600.woff2 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "POST /api/storefronts HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58512 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"adds": 2, "dialect": "canonical", "duration_ms": 2, "errors": 0, "event": "import_draft_created", "row_count": 3, "storefront_id": 8, "unchanged": 0, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58510 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/imports/drafts/7 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/imports/drafts/7/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"added": 2, "duration_ms": 4, "errored": 0, "event": "import_run_completed", "run_id": 6, "storefront_id": 8, "updated": 0}
|
||||
INFO: 127.0.0.1:58510 - "POST /api/products/imports/drafts/7/confirm HTTP/1.1" 201 Created
|
||||
INFO:ecomm.telemetry: {"duration_ms": 1, "event": "image_phase_completed", "failed": 0, "fetched": 0, "rejected": 0, "run_id": 6}
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/imports/runs/6 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58512 - "GET /api/products/imports/runs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/summary HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/export?status=all HTTP/1.1" 200 OK
|
||||
INFO:ecomm.telemetry: {"duration_ms": 4, "event": "catalog_exported", "product_count": 2, "status_filter": "all", "storefront_id": 8}
|
||||
INFO:ecomm.telemetry: {"adds": 0, "dialect": "canonical", "duration_ms": 6, "errors": 0, "event": "import_draft_created", "row_count": 3, "storefront_id": 8, "unchanged": 2, "unknown_columns_count": 0, "updates": 0}
|
||||
INFO: 127.0.0.1:58510 - "POST /api/products/imports HTTP/1.1" 201 Created
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/imports/drafts/8 HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:58510 - "GET /api/products/imports/drafts/8/records?limit=100&offset=0 HTTP/1.1" 200 OK
|
||||
@@ -1,5 +0,0 @@
|
||||
node_modules/
|
||||
.backend.log
|
||||
.objectstore/
|
||||
test-results/
|
||||
playwright-report/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 492 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 B |
@@ -1,4 +0,0 @@
|
||||
Handle,Title,Vendor,Tags,Status,Published,Option1 Name,Option1 Value,Variant SKU,Variant Price,Variant Inventory Qty
|
||||
moon-mug,Moon Mug,Wiggle Goods,"kitchen, mugs",active,TRUE,,,WG-MUG-001,18.00,40
|
||||
star-tee,Star Tee,Wiggle Goods,apparel,active,TRUE,Size,S,WG-TEE-S,24.00,12
|
||||
star-tee,,,,,,,M,WG-TEE-M,24.00,18
|
||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 184 B |
@@ -1,2 +0,0 @@
|
||||
Handle,Vendor
|
||||
mug,Acme
|
||||
|
@@ -1,4 +0,0 @@
|
||||
Handle,Title,Variant Price
|
||||
good-mug,Good Mug,10.00
|
||||
bad-tee,Bad Tee,not-a-price
|
||||
also-good,Also Good,5.00
|
||||
|
@@ -1,2 +0,0 @@
|
||||
Handle,Title,Body (HTML),Vendor,Type,Tags,Published,Status,Variant SKU,Variant Grams,Variant Inventory Qty,Variant Price,Variant Compare At Price,Gift Card,SEO Title,Cost per item
|
||||
shopify-mug,Shopify Mug,<p>Imported from Shopify.</p>,Acme,Drinkware,"mugs",TRUE,active,SM-001,300,25,14.00,20.00,FALSE,Shopify Mug | Acme,7.00
|
||||
|
@@ -1,4 +0,0 @@
|
||||
Handle,Title,Image Src
|
||||
good-lamp,Good Lamp,http://127.0.0.1:8799/good.png
|
||||
tiny-lamp,Tiny Lamp,http://127.0.0.1:8799/tiny.png
|
||||
gone-lamp,Gone Lamp,http://127.0.0.1:8799/missing.png
|
||||
|
-120
@@ -1,120 +0,0 @@
|
||||
// Shared E2E helpers — the sign-up journey (SD-0001 §5.1–§5.4) and products-page
|
||||
// navigation (SD-0002 §5.2). Selectors are role/label-based against the real screens
|
||||
// (Landing.tsx, SignIn.tsx, CreateStorefront.tsx, Admin.tsx, ProductsPage.tsx).
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const LOG = join(__dirname, ".backend.log");
|
||||
let seq = 0;
|
||||
|
||||
// The storefront name every helper-created merchant uses; tests assert against it.
|
||||
export const STOREFRONT_NAME = "E2E Test Goods";
|
||||
|
||||
export function freshEmail(): string {
|
||||
return `merchant${Date.now()}-${seq++}@example.com`;
|
||||
}
|
||||
|
||||
// LogMailer's exact line (backend/app/platform/mailer.py):
|
||||
// INFO:ecomm.mailer: LogMailer -> <to> | Your ecomm code: <6 digits>\n<body>
|
||||
// The first 6-digit group after the marker is the code.
|
||||
async function codeFor(email: string): Promise<string> {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const log = await readFile(LOG, "utf8").catch(() => "");
|
||||
const at = log.lastIndexOf(`LogMailer -> ${email}`);
|
||||
if (at >= 0) {
|
||||
const m = log.slice(at).match(/\b(\d{6})\b/);
|
||||
if (m) return m[1];
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
throw new Error(`no code logged for ${email}`);
|
||||
}
|
||||
|
||||
export async function signUpWithStorefront(page: Page, email = freshEmail()): Promise<string> {
|
||||
// Landing → the sign-up door.
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "Create your storefront →" }).click();
|
||||
|
||||
// Sign-in step 1: request the one-time code.
|
||||
await page.getByLabel("Email").fill(email);
|
||||
await page.getByRole("button", { name: "Send code" }).click();
|
||||
|
||||
// Sign-in step 2: read the code from the backend log and verify it.
|
||||
await expect(page.getByRole("heading", { name: "Check your email" })).toBeVisible();
|
||||
const code = await codeFor(email);
|
||||
await page.getByLabel("One-time code").fill(code);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Create-storefront screen (a new account has none yet).
|
||||
await expect(page.getByRole("heading", { name: "Create your storefront" })).toBeVisible();
|
||||
await page.getByLabel("Storefront name").fill(STOREFRONT_NAME);
|
||||
await page.getByRole("button", { name: "Create storefront", exact: true }).click();
|
||||
|
||||
// Admin shell: nav strip + the storefront identity in the topbar.
|
||||
await expect(page.getByRole("navigation", { name: "Admin sections" })).toBeVisible();
|
||||
await expect(page.locator(".storeid__name")).toHaveText(STOREFRONT_NAME);
|
||||
return email;
|
||||
}
|
||||
|
||||
export async function gotoProducts(page: Page) {
|
||||
await page
|
||||
.getByRole("navigation", { name: "Admin sections" })
|
||||
.getByRole("link", { name: "Products" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Products" })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function uploadFixture(page: Page, fixture: string) {
|
||||
// Two "Import products" links render on the empty products page (header + empty
|
||||
// state) — same destination, so take the first.
|
||||
await page.getByRole("link", { name: "Import products" }).first().click();
|
||||
await expect(page.getByRole("heading", { name: "Import products" })).toBeVisible();
|
||||
await page.locator('input[type="file"]').setInputFiles(join(__dirname, "fixtures", fixture));
|
||||
}
|
||||
|
||||
// Import good.csv and confirm it, leaving a 2-product catalog. Returns nothing;
|
||||
// callers continue from the run-detail screen.
|
||||
export async function importGoodCsv(page: Page) {
|
||||
await uploadFixture(page, "good.csv");
|
||||
await expect(page.getByRole("heading", { name: "Import preview — good.csv" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Import 2 products" }).click();
|
||||
await expect(page.getByRole("heading", { level: 1, name: "good.csv" })).toBeVisible();
|
||||
}
|
||||
|
||||
// Import with-images.csv (3 products, each with one Image Src) and confirm it.
|
||||
// Mirrors importGoodCsv; callers continue from the run-detail screen, where the
|
||||
// background image fetch progresses fetching_images → terminal.
|
||||
export async function importWithImages(page: Page) {
|
||||
await uploadFixture(page, "with-images.csv");
|
||||
await expect(page.getByRole("heading", { name: "Import preview — with-images.csv" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Import 3 products" }).click();
|
||||
await expect(page.getByRole("heading", { level: 1, name: "with-images.csv" })).toBeVisible();
|
||||
}
|
||||
|
||||
// Click an Export status option and capture the downloaded CSV's text + path.
|
||||
export async function exportCatalog(page: Page, label: string): Promise<{ text: string; path: string }> {
|
||||
// Open the <details> menu, then click the status option, capturing the download.
|
||||
// The menu is a native <details> that *toggles* on each summary click and is
|
||||
// NOT closed by clicking a download <a> (a download doesn't navigate). So on a
|
||||
// second export the menu may already be open — deterministically open it
|
||||
// rather than blind-toggling, and wait for the status link to be visible.
|
||||
const details = page.locator("details.products__export");
|
||||
if (!(await details.evaluate((el: HTMLDetailsElement) => el.open))) {
|
||||
await details.locator("> summary").click();
|
||||
}
|
||||
const link = page.getByRole("link", { name: label, exact: true });
|
||||
await expect(link).toBeVisible();
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
link.click(),
|
||||
]);
|
||||
const stream = await download.createReadStream();
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const c of stream) chunks.push(c as Buffer);
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
const path = join(tmpdir(), `export-${Date.now()}.csv`);
|
||||
await writeFile(path, text, "utf8");
|
||||
return { text, path };
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createServer } from "node:http";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const dir = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "images");
|
||||
createServer(async (req, res) => {
|
||||
const name = (req.url || "").replace(/^\//, "").split("?")[0];
|
||||
if (name === "good.png" || name === "tiny.png") {
|
||||
try {
|
||||
const buf = await readFile(join(dir, name));
|
||||
res.writeHead(200, { "Content-Type": "image/png" });
|
||||
res.end(buf);
|
||||
return;
|
||||
} catch { /* fallthrough */ }
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}).listen(8799, "127.0.0.1");
|
||||
Generated
-76
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"name": "wiggleverse-ecomm-e2e",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wiggleverse-ecomm-e2e",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "wiggleverse-ecomm-e2e",
|
||||
"private": true,
|
||||
"scripts": { "test": "playwright test" },
|
||||
"devDependencies": { "@playwright/test": "^1.48.0" }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
timeout: 60_000,
|
||||
retries: 0,
|
||||
// One shared backend + log file; parallel sign-ins would interleave codes.
|
||||
workers: 1,
|
||||
use: { baseURL: "http://localhost:8765" },
|
||||
webServer: {
|
||||
command: "bash ./serve.sh",
|
||||
url: "http://localhost:8765/healthz",
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# E2E server: fresh ecomm_e2e database, LogMailer (codes land in .backend.log),
|
||||
# backend on :8765 serving the built SPA (the deployed topology, SD-0001 §6.2).
|
||||
set -euo pipefail
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
if [ ! -f "$repo_root/frontend/dist/index.html" ]; then
|
||||
( cd "$repo_root/frontend" && npm run build )
|
||||
fi
|
||||
|
||||
"$repo_root/.venv/bin/python" - <<'PY'
|
||||
import psycopg
|
||||
admin = psycopg.connect("postgresql://ecomm:ecomm@localhost:5432/postgres", autocommit=True)
|
||||
admin.execute("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='ecomm_e2e' AND pid <> pg_backend_pid()")
|
||||
admin.execute("DROP DATABASE IF EXISTS ecomm_e2e")
|
||||
admin.execute("CREATE DATABASE ecomm_e2e")
|
||||
PY
|
||||
|
||||
export ECOMM_DATABASE_URL="postgresql://ecomm:ecomm@localhost:5432/ecomm_e2e"
|
||||
export ECOMM_MAILER=log
|
||||
|
||||
# Image pipeline (SLICE-7): local objectstore + a fixture image host on :8799.
|
||||
# ECOMM_IMAGE_FETCH_ALLOW_PRIVATE lets the SSRF guard reach 127.0.0.1 — TEST-ONLY,
|
||||
# never set in the deployed overlay. The &-backgrounded node host is a child of
|
||||
# this serve process; Playwright tears the whole webServer group down between runs.
|
||||
export ECOMM_OBJECTSTORE_KIND=local
|
||||
export ECOMM_OBJECTSTORE_DIR="$repo_root/e2e/.objectstore"
|
||||
export ECOMM_IMAGE_FETCH_ALLOW_PRIVATE=1
|
||||
rm -rf "$repo_root/e2e/.objectstore"
|
||||
node "$repo_root/e2e/image-host.mjs" &
|
||||
|
||||
cd "$repo_root/backend"
|
||||
exec "$repo_root/.venv/bin/python" -m uvicorn app.main:app --port 8765 \
|
||||
> "$repo_root/e2e/.backend.log" 2>&1
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// DoD scenario e2e_export_download (SD-0002 §6.8): export downloads a canonical
|
||||
// CSV and the status filter is respected.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exportCatalog, gotoProducts, importGoodCsv, signUpWithStorefront } from "../helpers";
|
||||
|
||||
test("e2e_export_download", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
await importGoodCsv(page);
|
||||
await gotoProducts(page);
|
||||
|
||||
// Export "All products" → a canonical CSV with both handles.
|
||||
const all = await exportCatalog(page, "All products");
|
||||
expect(all.text.split("\n")[0]).toContain("Handle,");
|
||||
expect(all.text).toContain("moon-mug");
|
||||
expect(all.text).toContain("star-tee");
|
||||
|
||||
// good.csv's products are active → "Active" exports both, "Draft" is empty/disabled-path.
|
||||
const active = await exportCatalog(page, "Active");
|
||||
expect(active.text).toContain("moon-mug");
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { gotoProducts, importWithImages, signUpWithStorefront } from "../helpers";
|
||||
|
||||
test("e2e_image_outcomes: per-image outcomes, problem rows, notice band", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
await importWithImages(page);
|
||||
// Run detail polls fetching_images → terminal; wait for the outcome summary.
|
||||
await expect(page.getByText("1 fetched · 1 rejected · 1 failed")).toBeVisible({ timeout: 30_000 });
|
||||
// Problem images listed in the outcomes table.
|
||||
await expect(page.getByText("tiny-lamp")).toBeVisible();
|
||||
await expect(page.getByText("gone-lamp")).toBeVisible();
|
||||
// Products page surfaces the aggregate notice.
|
||||
await gotoProducts(page);
|
||||
await expect(page.getByText(/products have image problems/)).toBeVisible();
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
// DoD scenario e2e_import_cancel_no_trace (SD-0002 §6.8): cancelling at the
|
||||
// preview gate (PUC-3a) leaves no trace — no products, no run in the history.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers";
|
||||
|
||||
test("e2e_import_cancel_no_trace", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
await uploadFixture(page, "good.csv");
|
||||
|
||||
// The preview gate is up.
|
||||
await expect(page.getByRole("heading", { name: "Import preview — good.csv" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "2 to add" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Lands back on Products — still the empty state, no run recorded.
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Products" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("No products yet. Bulk import is how product data gets in."),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("No imports yet.")).toBeVisible();
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
// DoD scenario e2e_import_errors_actionable (SD-0002 §6.8): row-level errors are
|
||||
// actionable — line, column, message — at the preview gate AND on the run report
|
||||
// card (PUC-5), while the good rows still import.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers";
|
||||
|
||||
test("e2e_import_errors_actionable", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
await uploadFixture(page, "mixed-errors.csv");
|
||||
|
||||
// Preview tiles: two good products, one errored row.
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Import preview — mixed-errors.csv" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "2 to add" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "1 errors" }).click();
|
||||
|
||||
// The error table names the line, the column, and the problem (actionable, PUC-5).
|
||||
const previewRow = page.locator(".errortable tbody tr");
|
||||
await expect(previewRow).toHaveCount(1);
|
||||
await expect(previewRow.locator("td").nth(0)).toHaveText("3");
|
||||
await expect(previewRow.locator("td").nth(1)).toHaveText("Variant Price");
|
||||
await expect(previewRow.locator("td").nth(2)).toContainText("is not a price");
|
||||
|
||||
// Good rows still import.
|
||||
await page.getByRole("button", { name: "Import 2 products" }).click();
|
||||
|
||||
// Run detail: counts include the errored row, and the same error row persists.
|
||||
await expect(page.getByRole("heading", { level: 1, name: "mixed-errors.csv" })).toBeVisible();
|
||||
await expect(page.getByText("2 added · 0 updated · 1 rows in error")).toBeVisible();
|
||||
const runRow = page.locator(".errortable tbody tr");
|
||||
await expect(runRow).toHaveCount(1);
|
||||
await expect(runRow.locator("td").nth(0)).toHaveText("3");
|
||||
await expect(runRow.locator("td").nth(1)).toHaveText("Variant Price");
|
||||
await expect(runRow.locator("td").nth(2)).toContainText("is not a price");
|
||||
|
||||
// The catalog gained the two good products.
|
||||
await gotoProducts(page);
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Products · 2" })).toBeVisible();
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
// DoD scenario e2e_import_file_rejected (SD-0002 §6.8): a file-level rejection
|
||||
// (PUC-5a) renders in place on the upload screen, the picker stays live for a
|
||||
// retry, and nothing is recorded — no products, no run.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers";
|
||||
|
||||
test("e2e_import_file_rejected", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
await uploadFixture(page, "missing-title.csv");
|
||||
|
||||
// The rejection renders in place, naming the missing column.
|
||||
await expect(page.getByText("That file can't be imported")).toBeVisible();
|
||||
await expect(page.getByText("missing the required column 'Title'")).toBeVisible();
|
||||
|
||||
// The picker is live again for a retry.
|
||||
await expect(page.locator('input[type="file"]')).toBeEnabled();
|
||||
|
||||
// No trace: still the empty catalog, and no run recorded.
|
||||
await gotoProducts(page);
|
||||
await expect(
|
||||
page.getByText("No products yet. Bulk import is how product data gets in."),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("No imports yet.")).toBeVisible();
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// DoD scenario e2e_import_preview_confirm (SD-0002 §6.8): the happy path end to
|
||||
// end — sign up, empty catalog, upload good.csv, preview gate (PUC-2/3), confirm,
|
||||
// run report card, and the catalog + history reflecting the import. Subsumes the
|
||||
// Task-15 harness smoke (sign-up journey + products empty state).
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { gotoProducts, signUpWithStorefront, STOREFRONT_NAME, uploadFixture } from "../helpers";
|
||||
|
||||
test("e2e_import_preview_confirm", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
|
||||
// Admin topbar: storefront identity + the signed-in account chip (ex-smoke).
|
||||
await expect(page.locator(".storeid__name")).toHaveText(STOREFRONT_NAME);
|
||||
await expect(page.getByRole("button", { name: "Sign out" })).toBeVisible();
|
||||
|
||||
await gotoProducts(page);
|
||||
|
||||
// Empty state + the import affordances (SD-0002 §5.2, ex-smoke).
|
||||
await expect(
|
||||
page.getByText("No products yet. Bulk import is how product data gets in."),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Download sample CSV" })).toBeVisible();
|
||||
|
||||
await uploadFixture(page, "good.csv");
|
||||
|
||||
// Preview (§5.4): summary tiles + the file's name in the heading.
|
||||
await expect(page.getByRole("heading", { name: "Import preview — good.csv" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "2 to add" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "0 errors" })).toBeVisible();
|
||||
|
||||
// Drill in: open star-tee's diff row and see field-level detail (the S-variant SKU).
|
||||
const starTee = page.locator(".difflist__item", { hasText: "star-tee" });
|
||||
await starTee.locator("summary").click();
|
||||
await expect(starTee.getByText("WG-TEE-S")).toBeVisible();
|
||||
|
||||
// Consent gate (PUC-3): confirm the import.
|
||||
await page.getByRole("button", { name: "Import 2 products" }).click();
|
||||
|
||||
// Run detail (§5.5): report card with the file name, counts, and status.
|
||||
await expect(page.getByRole("heading", { level: 1, name: "good.csv" })).toBeVisible();
|
||||
await expect(page.getByText("2 added · 0 updated · 0 rows in error")).toBeVisible();
|
||||
await expect(page.getByText("Complete", { exact: true })).toBeVisible();
|
||||
|
||||
// Back on Products: the catalog count and exactly one history row for this run.
|
||||
await gotoProducts(page);
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Products · 2" })).toBeVisible();
|
||||
const rows = page.locator(".datatable tbody tr");
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first().getByRole("link", { name: "good.csv" })).toBeVisible();
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
// DoD scenario e2e_import_shopify_dialect (SD-0002 §6.8, SLICE-8): a Shopify
|
||||
// product CSV imports directly (PUC-6). Upload an unmodified-shape Shopify export;
|
||||
// the preview recognizes the dialect ("Shopify product CSV — mapped"), lists the
|
||||
// columns with no canonical home as not-imported, and the import confirms and
|
||||
// completes — the run report card carrying the same dialect label.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers";
|
||||
|
||||
test("e2e_import_shopify_dialect", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
|
||||
await uploadFixture(page, "shopify-export.csv");
|
||||
|
||||
// Preview (§5.4): the file name, the recognized dialect, and the not-imported band.
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Import preview — shopify-export.csv" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Shopify product CSV — mapped")).toBeVisible();
|
||||
// The not-imported band lists Shopify columns with no canonical home. Assert a
|
||||
// column that never appears in canonical diff detail, so the match is unambiguous.
|
||||
await expect(page.getByText("Columns not imported")).toBeVisible();
|
||||
await expect(page.getByText("Variant Compare At Price")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "1 to add" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "0 errors" })).toBeVisible();
|
||||
|
||||
// Consent gate (PUC-3): confirm the import.
|
||||
await page.getByRole("button", { name: "Import 1 products" }).click();
|
||||
|
||||
// Run detail (§5.5): report card carries the same dialect label and completes.
|
||||
await expect(page.getByRole("heading", { level: 1, name: "shopify-export.csv" })).toBeVisible();
|
||||
await expect(page.getByText("Shopify product CSV — mapped")).toBeVisible();
|
||||
await expect(page.getByText("1 added · 0 updated · 0 rows in error")).toBeVisible();
|
||||
await expect(page.getByText("Complete", { exact: true })).toBeVisible();
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
// DoD scenario e2e_roundtrip_noop (SD-0002 §6.8, PUC-10): exporting then
|
||||
// re-importing the unmodified file previews as all-unchanged with the import
|
||||
// action disabled and the "Nothing to change" note.
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exportCatalog, gotoProducts, importGoodCsv, signUpWithStorefront } from "../helpers";
|
||||
|
||||
test("e2e_roundtrip_noop", async ({ page }) => {
|
||||
await signUpWithStorefront(page);
|
||||
await gotoProducts(page);
|
||||
await importGoodCsv(page);
|
||||
await gotoProducts(page);
|
||||
|
||||
// Export the catalog, then re-import the unmodified file.
|
||||
const { path } = await exportCatalog(page, "All products");
|
||||
await page.getByRole("link", { name: "Import products" }).first().click();
|
||||
await expect(page.getByRole("heading", { name: "Import products" })).toBeVisible();
|
||||
await page.locator('input[type="file"]').setInputFiles(path);
|
||||
|
||||
// Preview: everything unchanged, nothing to add/update (PUC-10).
|
||||
await expect(page.getByRole("button", { name: "2 unchanged" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "0 to add" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "0 to update" })).toBeVisible();
|
||||
|
||||
// The import action is disabled, with the no-op note.
|
||||
const importBtn = page.getByRole("button", { name: /^Import 0 products$/ });
|
||||
await expect(importBtn).toBeDisabled();
|
||||
await expect(
|
||||
page.getByText("Nothing to change — your catalog already matches this file"),
|
||||
).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user