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>
150 lines
6.6 KiB
Bash
150 lines
6.6 KiB
Bash
#!/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 Fastest 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 Fastest Cache) needs the site's real host here (none of the current smokes needs 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 // "")'
|
||
}
|