Files
WP-LIT-MCP/tests/Integration/fastest-cache-smoke.sh
T
Jan-Niclas LoosenandClaude Opus 5.5 bee9da3a86 feat(cache): WP Fastest Cache statt WP Super Cache
Die Cache-Integration war für WP Super Cache gebaut, eingesetzt wird aber
WP Fastest Cache. Profil (wpfc), Listener, Drosselung, lit/cache-clear und
Leeren-Probe laufen jetzt über die öffentlichen WPFC-Funktionen
wpfc_clear_post_cache_by_id/wpfc_clear_all_cache; die Probe leert nur die
Wegwerf-Seite (singleDeleteCache ohne Elternseiten) und prüft, dass WPFC
den Hook noch verarbeitet. Smoke fastest-cache-smoke.sh ersetzt den WPSC-Smoke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:57:55 +01:00

268 lines
16 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# End-to-end checks of the WP Fastest Cache profile of the compatibility layer: status, check route /wpfc-check with
# the purge probe (cache off and on), the purge after a write of this plugin without wp_update_post (terms), the
# whole-cache purge after a setting, its throttle, the trailing purge by cron and lit/cache-clear. Runs INSIDE the
# DDEV web container:
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/fastest-cache-smoke.sh
#
# SIDE EFFECTS, all undone by the EXIT trap and proven at the end (verify_restored): WP Fastest Cache is activated if
# it was not, its option WpFastestCache is switched (only wpFastestCacheStatus, nothing that writes .htaccess) and set
# back byte for byte, a test post and two categories are created and deleted. Cache files of the site are emptied by
# the whole-cache purges – that is what the run checks; WPFC rebuilds them on the next requests.
set -euo pipefail
cd /var/www/html
# shellcheck source=lib.sh
source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh
ROOT=/var/www/html
CACHE_ALL=$ROOT/wp-content/cache/all
HTACCESS=$ROOT/.htaccess
CRON_PATTERN='lit_mcp_wpfc'
OPTION_PATTERN='lit_mcp_wpfc'
TRAILING_HOOK=lit_mcp_wpfc_trailing_purge
PURGED_AT=lit_mcp_wpfc_purged_at
BASE=http://localhost
CACHE_ON='{"wpFastestCacheStatus":"on"}'
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
PREFIX=$(wp db prefix)
# --- Guards and snapshot of everything the run can change ----------------------------------------------------------
wp plugin is-installed wp-fastest-cache || fail "WP Fastest Cache is not installed"
sums() { sha256sum "$@" 2>/dev/null | awk '{print $1}'; }
WPFC_WAS_ACTIVE=no; wp plugin is-active wp-fastest-cache 2>/dev/null && WPFC_WAS_ACTIVE=yes
WPFC_OPTION_BEFORE=$(wp option get WpFastestCache 2>/dev/null || echo __absent__)
HTACCESS_BEFORE=absent; [ -e "$HTACCESS" ] && HTACCESS_BEFORE=$(sums "$HTACCESS")
list_hooks() { wp cron event list --fields=hook --format=csv | tail -n +2 | grep -E "$CRON_PATTERN" | sort -u || true; }
list_options() { wp db query "SELECT option_name FROM ${PREFIX}options WHERE option_name REGEXP '$OPTION_PATTERN' ORDER BY option_name" --skip-column-names; }
HOOKS_BEFORE=$(list_hooks)
OPTIONS_BEFORE=$(list_options)
ACTIVE_PLUGINS_BEFORE=$(wp option get active_plugins --format=json)
ORIGINAL_DESCRIPTION=$(wp option get blogdescription)
CREATED_POSTS=()
CREATED_TERMS=()
restore() {
set +e
local id term hook option
for id in "${CREATED_POSTS[@]}"; do wp post delete "$id" --force >/dev/null 2>&1; done
for term in "${CREATED_TERMS[@]}"; do wp term delete category "$term" >/dev/null 2>&1; done
wp option update blogdescription "$ORIGINAL_DESCRIPTION" >/dev/null 2>&1
wp transient delete "$PURGED_AT" >/dev/null 2>&1
cleanup_app_passwords_for "$ADMIN_LOGIN"
if [ "$WPFC_OPTION_BEFORE" = __absent__ ]; then
wp option delete WpFastestCache >/dev/null 2>&1
else
wp option update WpFastestCache "$WPFC_OPTION_BEFORE" >/dev/null 2>&1
fi
[ "$WPFC_WAS_ACTIVE" = no ] && wp plugin deactivate wp-fastest-cache >/dev/null 2>&1
for hook in $(list_hooks); do
grep -qx "$hook" <<<"$HOOKS_BEFORE" || wp cron event delete "$hook" >/dev/null 2>&1
done
for option in $(list_options); do
grep -qx "$option" <<<"$OPTIONS_BEFORE" || wp option delete "$option" >/dev/null 2>&1
done
set -e
}
# Prints the proof; returns non-zero on any difference (used in the trap, so it must not exit).
verify_restored() {
local ok=0 htaccess_after=absent
[ "$(wp option get WpFastestCache 2>/dev/null || echo __absent__)" = "$WPFC_OPTION_BEFORE" ] \
|| { echo "FAIL: option WpFastestCache differs"; ok=1; }
[ -e "$HTACCESS" ] && htaccess_after=$(sums "$HTACCESS")
[ "$htaccess_after" = "$HTACCESS_BEFORE" ] || { echo "FAIL: .htaccess changed ($HTACCESS_BEFORE -> $htaccess_after)"; ok=1; }
[ "$(wp option get active_plugins --format=json)" = "$ACTIVE_PLUGINS_BEFORE" ] || { echo "FAIL: active_plugins differs"; ok=1; }
[ "$(list_hooks)" = "$HOOKS_BEFORE" ] || { echo "FAIL: cron events differ: $(list_hooks | tr '\n' ' ')"; ok=1; }
[ "$(list_options)" = "$OPTIONS_BEFORE" ] || { echo "FAIL: options differ: $(list_options | tr '\n' ' ')"; ok=1; }
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || { echo "FAIL: transient $PURGED_AT left"; ok=1; }
[ "$(wp option get blogdescription)" = "$ORIGINAL_DESCRIPTION" ] || { echo "FAIL: blogdescription not restored"; ok=1; }
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] \
|| { echo "FAIL: a probe page is left"; ok=1; }
[ "$(wp post list --post_type=post --post_status=any --name=lit-wpfc-smoke --format=count)" = 0 ] \
|| { echo "FAIL: the test post is left"; ok=1; }
[ "$ok" = 0 ] && pass "restored: WP Fastest Cache active=$WPFC_WAS_ACTIVE with its option as before, .htaccess untouched"
return "$ok"
}
on_exit() {
local rc=$1
restore
verify_restored || rc=1
guard_exit "$rc" || exit $?
exit "$rc"
}
trap 'on_exit $?' EXIT
trap 'exit 130' INT TERM HUP
# --- Helpers ---------------------------------------------------------------------------------------------------------
wpfc_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "wpfc")'; }
stored_option() { wp option get "$1" --format=json 2>/dev/null || true; }
event_count() { wp cron event list --hook="$TRAILING_HOOK" --format=count 2>/dev/null || echo 0; }
# The cache file of the test post where WP Fastest Cache keeps it (a purge moves the directory to cache/tmpWpfc).
cache_file() { local f="$CACHE_ALL${POST_PATH%/}/index.html"; [ -f "$f" ] && echo "$f" || true; }
wait_for() { # $1 = description, rest = command; polls up to 15 s
local what=$1 i; shift
for i in $(seq 1 15); do "$@" && return 0; sleep 1; done
fail "timeout: $what"
}
cache_file_exists() { [ -n "$(cache_file)" ]; }
cache_file_gone() { [ -z "$(cache_file)" ]; }
fetch_post() {
local code
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $HOST" "$BASE$POST_PATH")
[ "$code" = 200 ] || fail "GET $POST_PATH answered $code"
}
# Requests the test post until WP Fastest Cache has written its file.
fill_cache() {
local i
for i in $(seq 1 10); do
fetch_post
cache_file_exists && return 0
sleep 1
done
echo "--- end of the response of $POST_PATH and the cache tree:"
curl -s -H "Host: $HOST" "$BASE$POST_PATH" | tail -c 300
find "$CACHE_ALL" -maxdepth 4 | head -20
fail "timeout: cache file of the test post"
}
# --- 0. Active, cache switched off -----------------------------------------------------------------------------------
[ "$WPFC_WAS_ACTIVE" = yes ] || wp plugin activate wp-fastest-cache >/dev/null 2>&1 || fail "could not activate WP Fastest Cache"
wp option update WpFastestCache '{}' >/dev/null
connect "$ADMIN_LOGIN"
# 1. Status
wpfc_component | jq -e '.active == true and .version == "1.5.5" and .problems == []' >/dev/null \
|| fail "site status must show WP Fastest Cache 1.5.5 active without problems: $(wpfc_component)"
pass "lit-site-status: WP Fastest Cache 1.5.5 active, no problems"
# 2. Check route with the cache switched off: the probe step is green and says "not checked"
APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain)
run_check() {
local headers
headers=$(curl -s -o /dev/null -D - -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' -u "$ADMIN_LOGIN:$APP_PASSWORD" "$BASE/wp-json/lit-mcp/v1/wpfc-check")
echo "$headers" | grep -qi '^HTTP/[0-9.]* 303' || fail "check route must answer 303: $headers"
echo "$headers" | grep -qi '^location:.*tab=compatibility.*lit_mcp_notice=compat_checked' || fail "redirect to the tab: $headers"
}
run_check
REPORT=$(stored_option lit_mcp_wpfc_check_report)
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("Cache ist ausgeschaltet, nicht geprüft")))' >/dev/null \
|| fail "cache off: the report must be green and the probe must say it did not check: $REPORT"
pass "/wpfc-check with the cache off: green, probe step says 'Cache ist ausgeschaltet, nicht geprüft'"
# 3. Cache on; the check route runs the real purge probe
wp option update WpFastestCache "$CACHE_ON" >/dev/null
run_check
REPORT=$(stored_option lit_mcp_wpfc_check_report)
echo "$REPORT" | jq -e '(.steps | length) == 2 and all(.steps[]; .problems == [] and .skipped == false)
and ([.steps[] | select(.key == "probe")] | length == 1 and (.[0].label | contains("nicht geprüft") | not))
and .version == "1.5.5"' >/dev/null || fail "cache on: the report must be green with a real probe step: $REPORT"
[ -z "$(stored_option lit_mcp_wpfc_check_lock)" ] || fail "the check lock must be released"
[ "$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count)" = 0 ] || fail "the probe left a page behind"
[ "$(find "$CACHE_ALL" -maxdepth 3 -path '*lit-mcp-check-*' -print -quit 2>/dev/null)" = "" ] || fail "the probe left a cache directory behind"
UNAUTH=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "$BASE/wp-json/lit-mcp/v1/wpfc-check")
[ "$UNAUTH" = 401 ] || [ "$UNAUTH" = 403 ] || fail "anonymous check must be refused, got $UNAUTH"
pass "/wpfc-check with the cache on: green with the purge probe, no page or cache directory left, anonymous refused"
# 4. A published test post, fetched until it is in the cache
CAT_A=$(wp term create category "LIT WPFC Smoke A" --porcelain); CREATED_TERMS+=("$CAT_A")
CAT_B=$(wp term create category "LIT WPFC Smoke B" --porcelain); CREATED_TERMS+=("$CAT_B")
POST_ID=$(wp post create --post_type=post --post_status=publish --post_title="LIT WPFC Smoke" --post_name=lit-wpfc-smoke \
--post_content="Testbeitrag fuer den Fastest-Cache-Smoke." --porcelain)
CREATED_POSTS+=("$POST_ID")
POST_PATH=$(wp eval "echo wp_parse_url(get_permalink($POST_ID), PHP_URL_PATH);")
fill_cache
pass "test post $POST_ID cached: $(cache_file | sed "s#$ROOT/##")"
# 5. Control: a term change outside this plugin does not touch the cache (WP Fastest Cache never sees it) ...
wp post term set "$POST_ID" category "$CAT_A" >/dev/null
sleep 2
cache_file_exists || fail "control: WP Fastest Cache itself purged on a plain term change; the next check proves nothing"
# ... but the same kind of change through lit-content-update (no wp_update_post) empties the cache of the post.
REV=$(tool lit-content-get "{\"post_id\":$POST_ID}" | jq -r '.rev')
UPDATED=$(tool lit-content-update "{\"post_id\":$POST_ID,\"rev\":\"$REV\",\"terms\":{\"category\":[$CAT_B]}}")
echo "$UPDATED" | jq -e '.ok == true' >/dev/null || fail "lit-content-update (terms only): $UPDATED"
wait_for "the cache file of the post is gone after lit-content-update" cache_file_gone
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
pass "terms-only lit-content-update empties the cache of the post (a plain term change does not), no trailing event"
# 6. Whole-cache purge after a setting; the second one inside 30 s is throttled and leaves exactly one cron event
fill_cache
[ -z "$(wp transient get "$PURGED_AT" 2>/dev/null || true)" ] || fail "transient $PURGED_AT set before the first whole purge"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 1"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 1"
wait_for "the cache is empty after a setting change" cache_file_gone
wait_for "the purge time is stored" bash -c "[ -n \"\$(wp transient get $PURGED_AT 2>/dev/null)\" ]"
LAST=$(wp transient get "$PURGED_AT")
[ "$(event_count)" = 0 ] || fail "the first whole purge must not schedule a trailing purge"
pass "setting change empties the whole cache (purge time stored, no trailing event)"
fill_cache
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 2"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 2"
wait_for "one trailing purge is scheduled" bash -c "[ \"\$(wp cron event list --hook=$TRAILING_HOOK --format=count)\" = 1 ]"
sleep 2
cache_file_exists || fail "throttled: the second whole purge ran inside the lock time"
ability lit/wp-settings-update '{"settings":{"blogdescription":"LIT WPFC Smoke 3"}}' | jq -e '.ok == true' >/dev/null || fail "settings update 3"
sleep 3
[ "$(event_count)" = 1 ] || fail "exactly one trailing event must stay scheduled, found $(event_count)"
cache_file_exists || fail "throttled: the third whole purge ran inside the lock time"
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "throttled purges must not move the purge time"
pass "second and third whole purge inside 30 s: throttled (file stays, purge time unchanged), exactly one $TRAILING_HOOK event"
# 7. Run the trailing purge once the lock is over: the cache is emptied and nothing is scheduled any more
WAIT=$((LAST + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
wp cron event run "$TRAILING_HOOK" >/dev/null 2>&1 || fail "wp cron event run $TRAILING_HOOK"
cache_file_gone || fail "the trailing purge did not empty the cache"
[ "$(event_count)" = 0 ] || fail "no trailing event may be left after it ran"
[ "$(wp transient get "$PURGED_AT")" -gt "$LAST" ] || fail "the trailing purge must store a new purge time"
pass "trailing purge by cron: cache emptied, event gone, purge time renewed"
# 8. lit/cache-clear: the dry run changes nothing, confirm empties the post's page / the whole cache
fill_cache
for ARGS in "{\"scope\":\"post\",\"post_id\":$POST_ID}" '{"scope":"all"}'; do
DRY=$(ability lit/cache-clear "$ARGS")
[ "$(error_code "$DRY")" = confirm_required ] || fail "lit/cache-clear dry run $ARGS must ask for confirm: $DRY"
echo "$DRY" | jq -e '(.__error | fromjson | .cache_enabled) == true' >/dev/null || fail "dry run must report the cache as on: $DRY"
done
sleep 2
cache_file_exists || fail "the dry runs of lit/cache-clear emptied the cache"
[ "$(event_count)" = 0 ] || fail "a dry run must not schedule anything"
pass "lit/cache-clear without confirm: asks for confirmation, reports the cache as on, cache file stays"
CLEARED=$(ability lit/cache-clear "{\"scope\":\"post\",\"post_id\":$POST_ID,\"confirm\":true}")
echo "$CLEARED" | jq -e '.ok == true and .outcome == "purged" and .log_id != null' >/dev/null || fail "lit/cache-clear post: $CLEARED"
wait_for "the cache file of the post is gone after lit/cache-clear scope post" cache_file_gone
[ "$(event_count)" = 0 ] || fail "a single-post purge must not schedule a trailing purge"
pass "lit/cache-clear scope post with confirm: the page of the post is emptied"
# The whole-cache purge needs the lock time of the earlier steps to be over.
WAIT=$(($(wp transient get "$PURGED_AT") + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
fill_cache
CLEARED=$(ability lit/cache-clear '{"scope":"all","confirm":true}')
echo "$CLEARED" | jq -e '.ok == true and .outcome == "purged" and .log_id != null' >/dev/null || fail "lit/cache-clear all: $CLEARED"
wait_for "the cache is empty after lit/cache-clear scope all" cache_file_gone
sleep 2
# Its own change log entry must not purge a second time (that would be throttled and leave a trailing event).
[ "$(event_count)" = 0 ] || fail "the change log entry of lit/cache-clear triggered a second purge"
LAST=$(wp transient get "$PURGED_AT")
pass "lit/cache-clear scope all with confirm: whole cache emptied once, own log entry purges nothing"
fill_cache
THROTTLED=$(ability lit/cache-clear '{"scope":"all","confirm":true}')
echo "$THROTTLED" | jq -e '.ok == true and .outcome == "throttled"' >/dev/null || fail "lit/cache-clear all inside the lock time: $THROTTLED"
[ "$(event_count)" = 1 ] || fail "a throttled lit/cache-clear must schedule exactly one trailing purge"
cache_file_exists || fail "throttled: the cache file must stay"
[ "$(wp transient get "$PURGED_AT")" = "$LAST" ] || fail "a throttled purge must not move the purge time"
WAIT=$((LAST + 32 - $(date +%s))); [ "$WAIT" -gt 0 ] && sleep "$WAIT"
wp cron event run "$TRAILING_HOOK" >/dev/null 2>&1 || fail "wp cron event run $TRAILING_HOOK"
cache_file_gone || fail "the trailing purge did not empty the cache"
pass "lit/cache-clear scope all inside 30 s: throttled, one trailing event, cron run empties the cache"
# 9. Everything back as before (the EXIT trap would do it too; here it is part of the verdict)
restore
verify_restored || fail "the state before the run was not restored"
pass "cleanup: test post, categories, options, transient and cron events gone"
finish