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 withjob:{id:b5cd365c…, status:pending}. - Background
asyncio.create_taskran_generate_in_background→generate_reportinapi/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 withfile). - Download:
GET /reports/{id}/download→ 200,Content-Length: 59262, payload is a real PDF (verified locally withfile /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_approvalrestored tofalse.- Two stale
pendingreport rows flipped tofailedwith explanatory error. wp-pulse-worker.servicewas started during investigation (still a no-op stub; left running — harmless).