#!/usr/bin/env bash # End-to-end checks of the Pods profile of the compatibility layer (plan 2026-11-06-kompatibilitaetsschicht, 1.25): # status, check route /pods-check, tab "Compatibility", release (refused for a tested version, simulated for an # untested one) and the lock by a red stored report. Runs INSIDE the DDEV web container: # cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-compat-smoke.sh # Temporarily activates the installed Pods plugin (restored afterwards); removes the lit_mcp_pods_* options, the # fixture pod and its entry again. The minimum version of the profile is not changeable from outside, so the lock is # triggered the way production does it: a red check report for the current state (lit_mcp_pods_check_report). set -euo pipefail cd /var/www/html # shellcheck source=lib.sh source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1) PODS_WAS_ACTIVE=no wp plugin is-active pods && PODS_WAS_ACTIVE=yes CREATED_POSTS=() BASE=http://localhost OPTIONS=(lit_mcp_pods_check_report lit_mcp_pods_releases lit_mcp_pods_check_lock) restore() { for id in "${CREATED_POSTS[@]}"; do wp post delete "$id" --force >/dev/null 2>&1 || true done wp option delete "${OPTIONS[@]}" >/dev/null 2>&1 || true wp eval-file "$PLUGIN/tests/Integration/pods-fixture.php" delete >/dev/null 2>&1 || true [ "$PODS_WAS_ACTIVE" = "no" ] && wp plugin deactivate pods >/dev/null 2>&1 || true cleanup_app_passwords_for "$ADMIN_LOGIN" } trap 'rc=$?; restore; guard_exit "$rc" || exit $?' EXIT pods_component() { tool lit-site-status '{}' | jq -c '.components[] | select(.id == "pods")'; } stored_option() { wp option get "$1" --format=json 2>/dev/null || true; } # Runs the admin-post handler of the tab (pods-compat-release.php); prints the notice of its redirect. handler() { wp eval-file "$PLUGIN/tests/Integration/pods-compat-release.php" "$1" "$2" 2>/dev/null | tail -1; } # Stores a red report for the current Pods state (what a failed admin check leaves behind). write_report() { # $1 = red|green RESULT="$1" wp eval ' $profile = LIT\MCP\Integrations\Pods\PodsCompatibility::profile(); $problems = getenv("RESULT") === "red" ? ["Smoke: Pods-Aufruf fehlt"] : []; LIT\MCP\Compatibility\CheckReportStore::forProfile($profile)->save(new LIT\MCP\Compatibility\CheckReport( "smoke-report", gmdate("c"), 1, (string) PODS_VERSION, LIT\MCP\Plugin::VERSION, "", [new LIT\MCP\Compatibility\CheckStep("manifest", $problems, false, "Pods-Funktionen")] ));' >/dev/null || fail "could not write the $1 report" } wp plugin activate pods >/dev/null wp eval-file "$PLUGIN/tests/Integration/pods-fixture.php" create >/dev/null connect "$ADMIN_LOGIN" # 1. Pods active: version, no problems pods_component | jq -e '.active == true and .version == "3.3.9.2" and .problems == []' >/dev/null \ || fail "site status must show Pods 3.3.9.2 active without problems: $(pods_component)" pass "lit-site-status: Pods 3.3.9.2 active, no problems" # 2. Check route (application password instead of the cookie nonce, as in divi-release-smoke.sh) APP_PASSWORD=$(wp user application-password create "$ADMIN_LOGIN" lit-mcp-integration-test --porcelain) 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/pods-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" REPORT=$(stored_option lit_mcp_pods_check_report) echo "$REPORT" | jq -e '(.steps | length) >= 1 and all(.steps[]; .problems == [] and .skipped == false) and .version == "3.3.9.2" and (has("divi_version") | not)' >/dev/null || fail "the stored report must be green with key version: $REPORT" [ -z "$(stored_option lit_mcp_pods_check_lock)" ] || fail "the check lock must be released" # The behaviour probe is part of the report (green, not skipped, its label is the real probe, not "not probed") ... echo "$REPORT" | jq -e '[.steps[] | select(.key == "probe")] | length == 1 and (.[0] | .problems == [] and .skipped == false and (.label | contains("nicht geprüft") | not))' >/dev/null || fail "the report must contain the green probe step: $REPORT" # ... and leaves no entry behind (it ran on lit_person; any status, any marked entry). LEFT=$(wp post list --post_type=any --post_status=any --meta_key=_lit_mcp_check_entry --format=count) [ "$LEFT" = "0" ] || fail "the probe left $LEFT entries behind" [ "$(wp post list --post_type=lit_person --post_status=any --format=count)" = "0" ] || fail "the probe left a lit_person entry" 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/pods-check") [ "$UNAUTH" = "401" ] || [ "$UNAUTH" = "403" ] || fail "anonymous check must be refused, got $UNAUTH" pass "/pods-check: 303 to the tab, green report with key version and probe step, no probe entry left, lock released, anonymous refused" # 3. The tab renders the Pods section and, as 3.3.9.2 is tested, offers no release TAB=$(wp --user="$ADMIN_LOGIN" eval ' require_once ABSPATH . "wp-admin/includes/template.php"; $_GET["tab"] = "compatibility"; LIT\MCP\Plugin::settingsPage()->render();') echo "$TAB" | grep -q 'id="lit-mcp-compat-pods"' || fail "tab Compatibility must have a Pods section" echo "$TAB" | grep -q '3\.3\.9\.2' || fail "the Pods section must show the version" echo "$TAB" | grep -q 'freigegeben' || fail "the Pods section must show the state" echo "$TAB" | grep -q 'lit-mcp-compat-check-pods' || fail "the Pods section must offer the check" echo "$TAB" | grep -q 'lit-mcp-compat-release-pods' && fail "no release button for a tested version" pass "tab Compatibility: Pods section with version and state, check button, no release button" # 3b. Orphan cleanup: an old marked probe entry (a dead check) is deleted on the tab's profile-load hook, a young one and # an unmarked entry with the same title are not. ORPHANS=$(wp --user="$ADMIN_LOGIN" eval ' $make = static fn (string $date, bool $marked): int => (int) wp_insert_post([ "post_type" => "lit_person", "post_status" => "draft", "post_title" => LIT\MCP\Compatibility\ThrowawayPost::TITLE . " smoke", "post_date" => get_date_from_gmt($date), "post_date_gmt" => $date, "meta_input" => $marked ? [LIT\MCP\Compatibility\ThrowawayPost::MARKER => "1"] : [], ]); $old = $make(gmdate("Y-m-d H:i:s", time() - 3600), true); $young = $make(gmdate("Y-m-d H:i:s"), true); $unmarked = $make(gmdate("Y-m-d H:i:s", time() - 3600), false); do_action(LIT\MCP\Admin\CompatibilityTab::LOAD_ACTION, "pods"); echo json_encode([$old, $young, $unmarked, get_post_status($old), get_post_status($young), get_post_status($unmarked)]);') OLD_ID=$(echo "$ORPHANS" | jq -r '.[0]'); YOUNG_ID=$(echo "$ORPHANS" | jq -r '.[1]'); UNMARKED_ID=$(echo "$ORPHANS" | jq -r '.[2]') CREATED_POSTS+=("$OLD_ID" "$YOUNG_ID" "$UNMARKED_ID") echo "$ORPHANS" | jq -e '.[3] == false and .[4] == "draft" and .[5] == "draft"' >/dev/null \ || fail "orphan cleanup must delete only the old marked entry: $ORPHANS" pass "orphan cleanup: old marked probe entry deleted, young and unmarked ones kept" # 4. Release: refused for the tested version; for an untested one (simulated profile) it is only documented [ "$(handler release real)" = "compat_release_refused" ] || fail "releasing a tested version must be refused" [ -z "$(stored_option lit_mcp_pods_releases)" ] || fail "a refused release must write nothing" [ "$(handler release untested)" = "compat_released" ] || fail "releasing an untested version after a green report must work" stored_option lit_mcp_pods_releases | jq -e 'length == 1 and .[0].version == "3.3.9.2" and (.[0] | has("divi_version") | not)' >/dev/null \ || fail "the release must be stored with key version: $(stored_option lit_mcp_pods_releases)" pods_component | jq -e '.problems == []' >/dev/null || fail "a release must not change the Pods status" tool lit-content-types '{"post_type":"lit_person"}' | jq -e '.post_types[0].fields[] | select(.key == "role") | .status == "writable"' >/dev/null \ || fail "a release must not lock or free anything" [ "$(handler withdraw untested)" = "compat_withdrawn" ] || fail "withdrawing the release must work" stored_option lit_mcp_pods_releases | jq -e 'length == 0' >/dev/null || fail "the withdrawn release must be gone" pass "release: refused for tested 3.3.9.2; simulated untested one stored and withdrawn, no lock effect" # 5. Lock: a red report for the current state locks Pods (all of its capabilities); a green one frees it again PERSON=$(tool lit-content-create '{"post_type":"lit_person","title":"Smoke Sperre","fields":{"role":"Leitung"}}') PERSON_ID=$(echo "$PERSON" | jq -r '.changed[0].id // empty') [ -n "$PERSON_ID" ] || fail "fixture entry: $PERSON" CREATED_POSTS+=("$PERSON_ID") REV=$(tool lit-content-get "{\"post_id\":$PERSON_ID}" | jq -r '.rev') write_report red pods_component | jq -e '(.problems | length) >= 1 and (.problems | any(contains("Prüfung")))' >/dev/null \ || fail "a red report must show as a problem: $(pods_component)" tool lit-content-types '{"post_type":"lit_person"}' | jq -e '[.post_types[0].fields[] | select(.key == "role")] | all(.status == "readonly")' >/dev/null \ || fail "locked: a Pods field must be read-only: $(tool lit-content-types '{"post_type":"lit_person"}')" BLOCKED=$(tool lit-content-update "{\"post_id\":$PERSON_ID,\"rev\":\"$REV\",\"fields\":{\"role\":\"Gesperrt\"}}") echo "$BLOCKED" | jq -e 'has("__error")' >/dev/null || fail "locked: a Pods field must not be writable: $BLOCKED" [ "$(wp post meta get "$PERSON_ID" role)" = "Leitung" ] || fail "locked: the value must be unchanged" pass "red report: status shows the problem, Pods field read-only, update refused ($(error_code "$BLOCKED"))" wp option delete lit_mcp_pods_check_report >/dev/null pods_component | jq -e '.problems == []' >/dev/null || fail "without the report Pods must be free: $(pods_component)" tool lit-content-types '{"post_type":"lit_person"}' | jq -e '[.post_types[0].fields[] | select(.key == "role")] | all(.status == "writable")' >/dev/null \ || fail "free again: the Pods field must be writable" FREE=$(tool lit-content-update "{\"post_id\":$PERSON_ID,\"rev\":\"$REV\",\"fields\":{\"role\":\"Frei\"}}") echo "$FREE" | jq -e '.ok == true' >/dev/null || fail "free again: update must work: $FREE" pass "report removed: status clean, field writable, update works" # 6. Nothing is left behind restore for option in "${OPTIONS[@]}"; do [ -z "$(stored_option "$option")" ] || fail "option $option is left behind" done [ "$(wp plugin is-active pods && echo yes || echo no)" = "$PODS_WAS_ACTIVE" ] || fail "Pods state not restored" [ "$(wp post list --post_type=lit_person --post_status=any --format=count 2>/dev/null || echo 0)" = "0" ] || fail "fixture entry left" pass "cleanup: options, entry, fixture pod gone; Pods state restored" finish