Files
WP-LIT-MCP/tests/Integration/lib.sh
T
Jan-Niclas LoosenandClaude Opus 5.5 661d3945fd feat(cache): Verhaltensprobe und Smoke für WP Super Cache
SuperCacheProbe prüft beim Prüfen, ob wp_cache_post_edit die Cache-Datei einer Testseite entfernt; während der Probe ist nur der Einzelseiten-Purge erlaubt, der Pfad kommt aus home und es werden nur feste Dateinamen gelöscht. ProbeStep bündelt die Schrittlogik von Pods und WP Super Cache; ThrowawayPost hält Marker und Titel zentral und räumt auch private Probe-Seiten auf (WP Super Cache wie Pods beim Laden des Reiters). super-cache-smoke.sh prüft gegen das echte Plugin; lib.sh sperrt parallele Smoke-Läufe per flock, wp-config.php wird atomar wiederhergestellt. Doku: Host-Abhängigkeit des Einzel-Purges, WP-Cron für den Nachlauf, Komplett-Purge durch Divi beim Prüfen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:25:37 +01:00

150 lines
6.6 KiB
Bash
Raw 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
# Shared helpers for the MCP integration scripts (mcp-smoke.sh, wp-basis-smoke.sh,
# and Etappe 1b's smoke scripts). Source it from a script that runs INSIDE the DDEV
# web container with /var/www/html as its working directory (cd there before
# sourcing – HOST below shells out to `wp` immediately).
# Requires jq and curl (both present in the DDEV web image).
# Mutual exclusion of ALL smokes (every one sources this file inside the same web container): two at once would
# share one site state – WP Super Cache edits wp-config.php, every smoke empties the test application passwords of the
# admin (cleanup_app_passwords_for, harmless only because of this lock), Pods/Divi release options. The lock lives as
# long as the script (fd 9 closes with it); a smoke that runs a second one itself must not source this file twice.
exec 9>/tmp/lit-mcp-smoke.lock
flock -n 9 || { echo "FAIL: another smoke is running (lock /tmp/lit-mcp-smoke.lock)"; exit 1; }
PLUGIN=wp-content/plugins/WP-LIT-MCP
ENDPOINT=http://localhost/wp-json/mcp/mcp-oauth-server
# The site's own Host header (without scheme). A request that bypasses normal WP
# routing (e.g. the raw discovery rewrite target mcp-smoke.sh probes) needs this
# Host header, or WordPress 301s it to the canonical host before it ever reaches
# the code under test.
HOST=$(wp option get home | sed -E 's#^[a-zA-Z]+://##')
# Optional: the Host header MCP requests carry (default: curl's own, "localhost"). A cache plugin that keys its files
# by the request's host (WP Super Cache) needs the site's real host here (super-cache-smoke.sh sets it).
MCP_HOST=${MCP_HOST:-}
TOKEN=""
SESSION=""
INIT_RESPONSE=""
REQUEST_ID=100
fail() { echo "FAIL: $*"; exit 1; }
# A smoke script must reach its last line: finish() marks that and prints the verdict.
# guard_exit (called first in every EXIT trap) turns any other exit – including a silent
# stop that still returns 0 – into a failure, so a run can never pass without all checks.
SMOKE_FINISHED=0
finish() { SMOKE_FINISHED=1; echo "ALL CHECKS PASSED"; }
guard_exit() {
local rc=$1
if [ "$rc" -eq 0 ] && [ "$SMOKE_FINISHED" != 1 ]; then
echo "FAIL: script stopped before its last check (exit 0 without finish)"
return 1
fi
return "$rc"
}
pass() { echo "ok - $*"; }
# Removes every "lit-mcp-integration-test" application password of a user (of ANY run – safe only under the smoke
# lock above: no other smoke can be using one).
cleanup_app_passwords_for() { # $1 = user_login
local login="$1"
[ -n "$login" ] || return 0
local escaped=${login//\\/\\\\}
escaped=${escaped//\'/\\\'}
wp eval "
\$u = get_user_by('login', '${escaped}');
if (\$u) {
foreach (WP_Application_Passwords::get_user_application_passwords(\$u->ID) as \$pw) {
if ((\$pw['name'] ?? '') === 'lit-mcp-integration-test') {
WP_Application_Passwords::delete_application_password(\$u->ID, \$pw['uuid']);
}
}
}
" >/dev/null 2>&1 || true
}
# Mints an MCP access token for a user, exactly like mcp-oauth's TokenEndpoint does
# after a completed OAuth flow (mint-token.php). Also creates the throwaway
# "lit-mcp-integration-test" application password cleanup_app_passwords_for removes.
mint_token() { # $1 = user_login -> prints the token
wp eval-file "$PLUGIN/tests/Integration/mint-token.php" "$1"
}
# Raw JSON-RPC POST to the MCP endpoint; prints headers+body (-i). $3 (session id)
# is optional. Low-level building block: used directly where a script needs the
# response headers (e.g. to read Mcp-Session-Id off the initialize response) and by
# rpc() below for everything after that.
mcp() { # $1 = token, $2 = json body, $3 = session id (optional)
local session_header=()
[ -n "${3:-}" ] && session_header=(-H "Mcp-Session-Id: $3")
local host_header=()
[ -n "$MCP_HOST" ] && host_header=(-H "Host: $MCP_HOST")
curl -s --max-time 20 -i -X POST "$ENDPOINT" \
"${host_header[@]}" \
-H "Authorization: Bearer $1" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
"${session_header[@]}" -d "$2"
}
# Mints a token for the user and opens an MCP session. Sets TOKEN, SESSION and
# INIT_RESPONSE (raw initialize response incl. headers).
connect() { # $1 = user_login
TOKEN=$(mint_token "$1")
local init
init=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}')
SESSION=$(echo "$init" | grep -i '^mcp-session-id:' | awk '{print $2}' | tr -d '\r' || true)
INIT_RESPONSE="$init"
echo "$init" | grep -q '"protocolVersion"' || fail "initialize failed: $init"
}
# JSON-RPC call over the session connect() opened; prints the response body only.
rpc() { # $1 = method, $2 = params JSON
REQUEST_ID=$((REQUEST_ID + 1))
local body
body=$(jq -cn --arg m "$1" --argjson p "$2" --argjson id "$REQUEST_ID" '{jsonrpc: "2.0", id: $id, method: $m, params: $p}')
local session_header=()
[ -n "$SESSION" ] && session_header=(-H "Mcp-Session-Id: $SESSION")
local host_header=()
[ -n "$MCP_HOST" ] && host_header=(-H "Host: $MCP_HOST")
curl -s --max-time 60 -X POST "$ENDPOINT" \
"${host_header[@]}" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
"${session_header[@]}" -d "$body"
}
# Calls a tool; prints structuredContent, or {"__error": "<text>"} for isError results.
tool() { # $1 = tool name, $2 = arguments JSON
local response
response=$(rpc tools/call "$(jq -cn --arg n "$1" --argjson a "$2" '{name: $n, arguments: $a}')")
echo "$response" | jq -e '.result' >/dev/null 2>&1 || fail "$1: protocol error: $response"
if [ "$(echo "$response" | jq -r '.result.isError')" = "true" ]; then
echo "$response" | jq -c '{__error: .result.content[0].text}'
else
echo "$response" | jq -c '.result.structuredContent'
fi
}
# Runs an indirect ability through mcp-adapter-execute-ability; prints its result or {"__error": …}.
ability() { # $1 = ability name (lit/…), $2 = parameters JSON
local out
out=$(tool mcp-adapter-execute-ability "$(jq -cn --arg n "$1" --argjson p "$2" '{ability_name: $n, parameters: $p}')")
if echo "$out" | jq -e 'has("__error")' >/dev/null; then
echo "$out"
elif [ "$(echo "$out" | jq -r '.success')" = "true" ]; then
echo "$out" | jq -c '.data'
else
echo "$out" | jq -c '{__error: .error}'
fi
}
# Prints the lit error code (e.g. rev_mismatch) of a tool()/ability() result, or "".
error_code() { # $1 = result JSON
echo "$1" | jq -r '(.__error // "") | (try fromjson catch {}) | (.code // "")'
}