chore(reset)!: strip storefront → network-service seed (v0.9.0)
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:
2026-06-12 09:30:01 -07:00
parent e2d8b5caae
commit df92e3f94c
104 changed files with 353 additions and 8400 deletions
+245
View File
@@ -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
-5
View File
@@ -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

-4
View File
@@ -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
1 Handle Title Vendor Tags Status Published Option1 Name Option1 Value Variant SKU Variant Price Variant Inventory Qty
2 moon-mug Moon Mug Wiggle Goods kitchen, mugs active TRUE WG-MUG-001 18.00 40
3 star-tee Star Tee Wiggle Goods apparel active TRUE Size S WG-TEE-S 24.00 12
4 star-tee M WG-TEE-M 24.00 18
Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

-2
View File
@@ -1,2 +0,0 @@
Handle,Vendor
mug,Acme
1 Handle Vendor
2 mug Acme
-4
View File
@@ -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 Handle Title Variant Price
2 good-mug Good Mug 10.00
3 bad-tee Bad Tee not-a-price
4 also-good Also Good 5.00
-2
View File
@@ -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 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
2 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
-4
View File
@@ -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
1 Handle Title Image Src
2 good-lamp Good Lamp http://127.0.0.1:8799/good.png
3 tiny-lamp Tiny Lamp http://127.0.0.1:8799/tiny.png
4 gone-lamp Gone Lamp http://127.0.0.1:8799/missing.png
-120
View File
@@ -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 };
}
-19
View File
@@ -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");
-76
View File
@@ -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"
}
}
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"name": "wiggleverse-ecomm-e2e",
"private": true,
"scripts": { "test": "playwright test" },
"devDependencies": { "@playwright/test": "^1.48.0" }
}
-16
View File
@@ -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,
},
});
-34
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}
-21
View File
@@ -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");
});
-16
View File
@@ -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();
});
-23
View File
@@ -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();
});
-41
View File
@@ -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();
});
-25
View File
@@ -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();
});
-49
View File
@@ -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();
});
-35
View File
@@ -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();
});
-30
View File
@@ -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();
});