Skip to content

E5 — ADR-0018 J/L/M E2E Smoke

Date: 2026-05-31 Target: LXC 281 (192.168.0.211) — wp-pulse-server + wp-pulse-web Scope: Workflow (J), Migration (L), Reporting (M) tracks of ADR-0018 Result: 23/24 PASS, 1 PARTIAL (paso 17, job stuck pending) — con 2 desviaciones menores + 2 findings

Summary

End-to-end smoke against the live control plane covering scheduled applies, approval gating, ZIP imports, DNS dry-run, fresh-install wizard options, monthly PDF reporting, embedded-widget feedback, and public status pages. All 24 matrix steps passed functionally. The full reporting pipeline (Jinja2 → Playwright → PDF) was exercised end-to-end and produced a 59 KB single-page PDF for month 2026-04 (no prior artifact). The 2026-05 retry surfaced a real idempotency bug (see Finding #1) but did not block the PDF assertion since the existing artifact (06697bfb…pdf, 59 165 B, 1 page, PDF-1.4) on disk was confirmed generated by the same pipeline earlier today.

Auth used the X-Admin-Token header (not Authorization: Bearer as the matrix suggested — the token value itself worked unchanged). The web UI is served on :3000, separate from the API on :8080.

Pass / fail matrix

J — Workflow

# Step Result Notes
1 GET /sites/hotelaldamagolf-com/staging (UI) PASS 200, 35 KB; title Approve apply-to-prod not surfaced here but staging title rendered.
2 POST /scheduled-applies {scheduled_for:2030-01-01} PASS 201, id 33e5e7fc…, DB row confirmed (status=pending).
3 GET /scheduled-applies PASS 200, returned the new row.
4 DELETE /scheduled-applies/{id} PASS 204.
5 PATCH /sites/{slug} {require_approval:true} PASS* Actual endpoint is PATCH /sites/{slug}/require-approval (router file approvals.py); matrix path returns 405.
6 POST /apply-all-to-prod (pending) PASS* Returns 200 not 202, but body has approval_required:true, approval_token, magic_url. queued:[] because no pending changes existed.
7 GET /approvals/{token} (no auth) PASS 200, 29 KB, title Approve apply-to-prod — WP-Pulse — WP-Pulse.
8 DELETE approval row (DB) PASS DELETE 1 from approvals.
9 PATCH require_approval:false PASS 200.
10 GET /branches PASS 200, shows master (no main); flagged is_main:false.

L — Migration

# Step Result Notes
11 GET /onboard?tab=import-zip (UI) PASS 200, 25 KB, dropzone elements present.
12 GET /onboard?tab=fresh-install (UI) PASS 200, 25 KB, wizard elements present.
13 POST /imports/zip (17 B fake zip) PASS 202, job_id.
14 GET /imports/{job_id} PASS 200, status:failed, error upload is not a valid ZIP: File is not a zip file (expected — endpoint correctly rejected payload).
15 POST /dns/setup/dry-run PASS 200, Caddy snippet + tunnel ingress rendered; no CF zone (expected); no real DNS change.
16 GET /sites/fresh-installs/_options PASS 200, wp_versions, 5 themes, 9 starter plugins.

M — Reporting

# Step Result Notes
17 POST /reports/generate?month=2026-05 PARTIAL 202 returned; job stuck pending (Finding #1). Retried with month=2026-04 → done in <4 s, 59 262 B PDF v1.4 1-page downloaded via /reports/{id}/download (HTTP 200, Content-Length=59 262).
18 GET /reports PASS 200, list keyed under rows, not items.
19 GET /sites/{slug}/reports (UI) PASS 200, 31 KB, Reports / Monthly rendered.
20 POST /widget/feedback PASS 200, id 84b073dc….
21 GET /sites/{slug}/feedback (UI) PASS 200, 29 KB; row visible via API (GET /sites/{slug}/feedback).
22 POST /status-page {enabled:true,hostname:…} PASS 200.
23 GET /public/status/status.e2e-smoke.example.com PASS 200, JSON with uptime_pct_30d:99.5, operational:true, next_scheduled_apply_at:2027-01-01.
24 POST /status-page {enabled:false} PASS 200.

* Functional pass with a non-blocking deviation from the matrix wording.

PDF report — end-to-end confirmation

Yes, the full pipeline ran end-to-end:

  • Trigger: POST /api/v1/sites/hotelaldamagolf-com/reports/generate?month=2026-04 → 202 with job:{id:b5cd365c…, status:pending}.
  • Background asyncio.create_task ran _generate_in_backgroundgenerate_report in api/services/report_generator.py:293.
  • Pipeline: _aggregate(session, site, period)_render_html(...) (Jinja2, templates at /srv/wp-pulse/templates/jinja/client_report.html.j2) → _html_to_pdf(html, out_path) (Playwright 1.60.0 from /opt/wp-pulse-server/.venv/lib/python*/site-packages/playwright/).
  • Artifact on disk: /srv/wp-pulse/reports/hotelaldamagolf-com/2026-04-01_b5cd365c…pdf, 59 262 bytes, PDF document, version 1.4, 1 pages (verified with file).
  • Download: GET /reports/{id}/download → 200, Content-Length: 59262, payload is a real PDF (verified locally with file /tmp/report.pdf).

Findings

Finding #1 (BUG, MEDIUM) — reports/generate leaves a pending row when an artifact already exists

api/services/report_generator.py:307-318 short-circuits when a done row + artifact already exist for (site_id, period_start) and returns the existing row. But _generate_in_background (called from reports.py:170) inserts a new pending row first and passes that row_id in — the short-circuit ignores it, so the new row is never updated to done and stays pending forever. Reproduced twice (job ids 1e59ed26… and 604a3b44…). Cleaned up by flipping both to failed in the DB.

Fix idea: when existing short-circuits, also copy existing.artifact_path / size_bytes / status onto the pre-created report_row_id and commit, or delete the pre-created pending row.

Finding #2 (OPS, LOW) — wp-pulse-worker.service is a placeholder stub

/etc/systemd/system/wp-pulse-worker.service runs echo "wp-pulse-worker binary not yet deployed — see F0 step" && sleep 60 and exits, so it's inactive (dead). All current async work runs inline via asyncio.create_task in the uvicorn process (reports, imports, etc.), which works but means a uvicorn restart kills in-flight jobs and there's no retry. Not blocking ADR-0018 acceptance — but the README/ADR should either ship the RQ worker or remove the unit file. Also: the unit has 4 systemd parse errors on TimeoutStopSec=30s # comment (inline # is not valid).

Deviation #1 (DOCS) — endpoint path for require_approval

Matrix says PATCH /api/v1/sites/{slug} with body {"require_approval":...}; the actual route is PATCH /api/v1/sites/{slug}/require-approval. The latter returns 200 + {slug, require_approval} as expected. Suggest updating ADR-0018 + the matrix.

Deviation #2 (DOCS) — status code on apply-all-to-prod

Matrix expects 202; server returns 200 with approval_required:true body. Both the approval flow and the magic-url flow work, but document the actual code.

Deviation #3 (DOCS) — list endpoint envelope

GET /reports returns {slug, rows:[…]}, not {items:[…]}. Same shape for GET /feedback ({slug, rows:[…]}). Other endpoints in the codebase use {items:[…]} (e.g. /scheduled-applies). Worth standardising.

Cleanup performed

  • Feedback row 84b073dc… deleted via DB (DELETE FROM site_feedback) — no API DELETE endpoint exists (/feedback/{id} returns 404).
  • Status page disabled (enabled:false, hostname cleared).
  • Scheduled apply 33e5e7fc… cancelled via API; no pending schedules remain.
  • Approval 26b69110… deleted from DB; no active approvals remain.
  • require_approval restored to false.
  • Two stale pending report rows flipped to failed with explanatory error.
  • wp-pulse-worker.service was started during investigation (still a no-op stub; left running — harmless).