Skip to content

E2 — Reversibility E2E Smoke (ADR-0015)

  • Target: LXC 281 (192.168.0.211:8080) via X-Admin-Token
  • Site: hotelaldamagolf-com
  • Date: 2026-05-31
  • Mode: Preview/promote with benign quarantine of fake plugin paths; no real customer plugin or DB mutation

Summary (< 250 words)

All five ADR-0015 reversibility features are functional on LXC 281. C1 (pre-apply auto-snapshot) captures a SiteVersion row with source="pre_apply" and links it to the resulting pushes_v2.pre_apply_version_id. C2 (sticky toast surface) is correct on the API side — GET /api/v1/fleet/recent-pushes returns every window-relevant push with pre_apply_version_id and an is_rollback flag — and the SPA bundle contains the JustApplied/Revert/Undo strings, confirming the toast component ships. C3/C4 (per-file rollback) correctly accepts the paths subset filter and (per code) leaves the original push DONE while creating a sibling rollback push tagged partial rollback of <id>; full rollbacks flip the original to ROLLED_BACK. C5 (time-window revert) returns 202 with both rolled_back_push_ids and the earliest loaded_version_id, schedules the restore via the version-load worker, and stamps the original push as ROLLED_BACK.

Two bugs found: (1) non-atomic apply-all-to-prod — a transient validation failure in the files category does NOT roll back the pre-apply snapshot or prevent the quarantine push from being queued, producing orphaned ~200 MB snapshots and ghost pushes. (2) Stray .htaccess file at wp-content/.htaccess is being written by an out-of-band process (likely the .htaccess_rules pending-DB writer) but never staged for push, blocking any subsequent apply-all-to-prod with a 409 until manually deleted from the worktree.

Pass/Fail matrix

# Feature Verdict Evidence
C1 Pre-apply auto-snapshot PASS SiteVersion row c7ffb3f0-72b8-4062-bc4c-d40495ae9dfb with source="pre_apply", name="Pre-apply 2026-05-31 16:56:59 UTC". Push 6226aa6d2712 carries pre_apply_version_id pointing at that UUID. version_id on disk: v_preapply_89d4416fe9a7.
C2 Sticky "Just applied" toast PASS GET /api/v1/fleet/recent-pushes?minutes=15 returns both test pushes with pre_apply_version_id populated and is_rollback=false. SPA HTML at https://wp-pulse-app.monxas.casa/sites/hotelaldamagolf-com matches JustApplied|Revert|Undo (component bundled).
C3 Inline diff before revert PASS (API surface only) The diff payload is sourced from push.files_changed which GET /pushes/{id} returns: 1 entry {path:"mu-plugins/wpp-quarantine.php", action:"write", size_bytes:951}. UI rendering not exercised in this smoke.
C4 Per-file revert PASS POST /pushes/64bb23f40a0f/rollback {"paths":["mu-plugins/wpp-quarantine.php"]} → 201 with new push ff35954fed7e. Log tail: partial rollback of 64bb23f40a0f; 1 restore(s). Original push stays DONE per spec (subset rollback). Worktree mu-plugin file content reverted to pre-test state (smoke-e6/x.php); smoke-rev-c1/smoke.php removed.
C5 Time-window revert PASS Fresh push 9c044a211bf7 (pre_apply v_preapply_a28357dfd446) → POST /revert-window {"minutes":1} → 202 with rolled_back_push_ids:["9c044a211bf7"], rollback_push_ids:["cafe7a23ff2f"], loaded_version_id:"v_preapply_a28357dfd446". Original flipped to ROLLED_BACK; rollback push cafe7a23ff2f reached DONE.

Bugs

B1 — apply-all-to-prod is not atomic across categories (P2)

First POST /apply-all-to-prod {"categories":["files","quarantine"]} returned HTTP 400 ('.htaccess' is outside themes/plugins/mu-plugins) because a stray .htaccess was present in the worktree at the time of the files validation pass. However, the same call had already:

  1. Captured a pre-apply snapshot (v_preapply_3d0bf75001db, 212 MB on disk).
  2. Queued a quarantine push (64bb23f40a0f) that subsequently completed DONE.

The 400 response gave no indication that side effects had occurred — the caller would naturally retry, producing a second snapshot (v_preapply_89d4416fe9a7) and a second quarantine push (6226aa6d2712), both also DONE. End result: two orphaned ~200 MB snapshots and two pushes for what the operator considered one apply.

Root cause (read from api/routers/pending.py:586-686): the snapshot is created before the per-category loops, and each category loop calls its own helper (start_push for files, _push_worktree_quarantine_to_prod for quarantine) independently. There is no try/except that unwinds the snapshot or earlier category pushes when a later category raises.

Recommended fix: wrap the per-category dispatch in a savepoint and either (a) defer snapshot creation until at least one category passes its validation pre-check, or (b) on HTTP exception from any sub-push, delete the orphaned SiteVersion row + on-disk version tarball. Option (a) is cheaper.

B2 — Out-of-band .htaccess writer breaks subsequent apply-all (P3)

Between the C1 apply and the C5 fresh apply (~3 min apart) something wrote wp-content/.htaccess (76 bytes, single Deny from 192.0.2.50 block timestamped 2026-05-31T16:59:28.364410+00:00). This file is not staged into the push, but it shows up as an untracked file in git status and triggers _push_worktree_quarantine_to_prod's "worktree has unrelated pending changes" 409 in any later apply.

It is presumably the htaccess_rules pending-DB feature writing the file into the worktree as a preview side effect — the C5 final pending-changes did show htaccess_rules.added: 2 pending. The bug is that this preview writer touches the worktree without registering its file in the push path or excluding it from the porcelain stray-file check.

Workaround used during this run: SSH'd in and rm'd the stray .htaccess. Recommended fix: either add .htaccess to the quarantine push's exclude list (parallel to QUARANTINE_RELPATH exclusion in apply-all-to-prod's files category), OR have the htaccess_rules writer commit/stash its output in a dedicated worktree state that the push helpers explicitly tolerate.

Residual state

quarantined_plugins == prod_quarantined_plugins == ["smoke-e6/x.php"] after the run. This is a pre-existing test residue from a prior smoke session (push history shows pushes touching mu-plugins/wpp-quarantine.php going back to 2026-05-31T08:30). Our test added smoke-rev-c1/smoke.php and smoke-rev-c5/c5.php and both were correctly removed by their respective reverts. The residual smoke-e6/x.php should be cleaned up out of band; it was not introduced by E2.

pending-changes.total_count == 3 at end of run, decomposing into 2 htaccess rules + 1 2FA nudge in the DB category — also pre-existing, not from E2.

Verdict

5 / 5 reversibility features behave as specified. Both bugs are non-blocking for the reversibility contract itself (the snapshots get taken, the rollbacks land, the time-window revert wires the version load) but B1 should be addressed before exposing apply-all-to-prod to less-technical operators, since it currently leaks disk on a transient failure.