test: Etappe 3b end-to-end smoke and acceptance checklist

tb-smoke.sh checks the Theme Builder tools, Pods field warnings (own
temporary pod, ruling Q6), find-similar, make-global, P13 and P10 through
the OAuth endpoint. The only enabled template targets a temporary draft
page; cleanup goes by exact IDs and the last section verifies the Theme
Builder list, enabled states and options equal the state before the run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-27 22:37:12 +02:00
co-authored by Claude Opus 5.5
parent 3db96a3f5b
commit 0b7a1c28ec
2 changed files with 686 additions and 0 deletions
@@ -0,0 +1,145 @@
# Etappe 3b – Manuelle Abnahme (Theme Builder, ähnliche Abschnitte, globale Module umwandeln)
Automatisch geprüft sind bereits:
- alle Unit-Tests (`composer test`) und Lint (`composer lint`);
- `tb-smoke.sh` (neu) und die sechs bisherigen Integrationsskripte `divi-library-smoke.sh`,
`divi-write-smoke.sh`, `divi-read-smoke.sh`, `mcp-smoke.sh`, `wp-basis-smoke.sh` und `media-admin-smoke.sh`
(alle in `tests/Integration/`).
`tb-smoke.sh` prüft über den OAuth-Endpunkt, nur mit temporären Daten, die es danach wieder löscht. Die einzige
Vorlage, die es aktiviert, gilt nur für eine temporäre Entwurfsseite; Besucher sehen davon nichts. Geprüft werden:
- Werkzeugliste: weiterhin 22 direkte Tools; `lit/tb-list`, `lit/tb-create-template`, `lit/tb-assign`,
`lit/tb-activate`, `lit/library-find-similar` und `lit/library-make-global` indirekt und erreichbar;
`lit-site-status` ohne Befund bei Divi;
- neue Vorlage: deaktiviert, in der Liste des Theme Builders, Inhalt-Layout über Divis eigene Funktionen mit dem
Inhalt des Plugins, wird nicht angezeigt; die Divi-Werkzeuge ändern das Layout einer deaktivierten Vorlage
sofort, „Inhalt posten“ (`post-content`) ist im Inhalt-Layout erlaubt;
- `lit/tb-assign`: bei einer deaktivierten Vorlage sofort gespeichert, fehlendes `exclude_from` lässt die
Ausnahmen stehen, `[]` entfernt sie, eine veraltete `template_rev` wird abgelehnt;
- `lit/tb-activate`: erst Probelauf, dann Aktivieren; die Entwurfsseite zeigt die Vorlage in der Vorschau, die
Startseite und Besucher sehen nichts; Änderungen am Layout (auch Löschen) und an den Bedingungen einer aktiven
Vorlage erst nach Probelauf; Deaktivieren; die Standardvorlage wird abgelehnt;
- ein ungespeicherter Entwurf im Theme Builder sperrt Anlegen, Zuordnen und Aktivieren; Deaktivieren geht
trotzdem, mit Warnung;
- eine Vorlage, die ein veraltetes Theme-Builder-Fenster aus der Liste entfernt hat: erscheint unter
`detached`, Aktivieren und Zuordnen ohne `reattach` werden abgelehnt, mit `reattach: true` ist sie wieder in
der Liste, deaktiviert und ohne „ungenutzt“-Markierung;
- ein Redakteur ohne Theme-Builder-Recht: `lit/tb-list` und Anlegen abgelehnt, Vorlagen und Layouts gelten als
nicht vorhanden; Gruppe „Divi Theme Builder“ auf „nur lesen“ bzw. deaktiviert wirkt auch auf die
Divi-Werkzeuge;
- Pods, mit einem eigenen temporären Pod: Divi bietet die Bedingung „Alle …“ für den neuen Inhaltstyp an; im
Inhalt-Layout erzeugt das Beziehungsfeld eine Warnung, das Auswahlfeld einen Hinweis, das Textfeld nichts;
- `lit/library-find-similar` (gleich und nur gleich aufgebaut) und `lit/library-make-global` (Umwandeln,
dann Ersetzen nach Probelauf; ein nur ähnlicher Abschnitt wird nicht ersetzt);
- P13 (keine Bibliotheks-IDs ohne Bibliothekszugang) und P10 (`post_link_url_page` nicht auf Beiträge);
- am Ende: Liste des Theme Builders und Aktiv-Zustand jeder Vorlage wie vorher, Optionen wiederhergestellt,
keine Beiträge, Metadaten, Begriffe, Protokollzeilen, Pods, Konten oder Rollen des Laufs übrig.
Die folgenden Punkte brauchen den echten Theme Builder bzw. Visual Builder und einen verbundenen KI-Assistenten.
Abweichungen als kurze Notiz zurückmelden; sie werden Folgeaufgaben.
Wichtig: Eine aktive Vorlage ändert sofort, was Besucher sehen. Für die Abnahme Vorlagen nur für Entwurfsseiten
oder für die „[Fixture] Personen“ aktivieren und im selben Schritt wieder deaktivieren. Die „[Fixture]“- und
„[Test]“-Inhalte nicht ändern.
## Vorbereitung
- [ ] DDEV läuft, Divi 5.2.1 ist aktiv, Pods ist aktiv, LIT MCP zeigt Version 0.5.0 (Plugins-Seite).
- [ ] Divi → Theme Builder öffnen und den Stand notieren: welche Vorlagen es gibt und welche aktiv sind
(Schalter in der Vorlage). Ist dort etwas ungespeichert, speichern oder verwerfen. Danach das Fenster
schließen – ein offenes Theme-Builder-Fenster kann beim späteren Speichern Vorlagen des Plugins wieder
entfernen.
- [ ] Unter Einstellungen → LIT MCP → Tools stehen die Gruppen „Divi“, „Divi-Bibliothek“ und „Divi Theme Builder“
auf ihrem Standard (nicht „nur lesen“, nicht deaktiviert); in der Gruppe „Divi Theme Builder“ stehen
`lit/tb-list`, `lit/tb-create-template`, `lit/tb-assign` und `lit/tb-activate`.
- [ ] Nach einem Lauf von `tb-smoke.sh` ist nichts übrig: unter Seiten, Divi → Divi-Bibliothek und Divi → Theme
Builder nach „LIT Smoke“ suchen – keine Treffer; unter Pods Admin → Pods verwalten kein Pod „lit_smk_…“.
- [ ] Den KI-Assistenten neu verbinden, damit er die neuen Tools und Hinweise lädt. In einer neuen Unterhaltung
nennt der Hinweis des Servers den Theme Builder.
- [ ] Eine Entwurfsseite „Abnahme 3b – Vorlage“ anlegen (nicht veröffentlichen). Sie ist das Ziel der
Seiten-Vorlage unten.
## 1. Vom Plugin angelegte Vorlage im Theme Builder und Visual Builder
- [ ] Den KI-Assistenten bitten: „Lege im Theme Builder eine Vorlage ‚Abnahme 3b – Seite‘ für die Seite
‚Abnahme 3b – Vorlage‘ an: globale Kopfzeile, im Inhalt eine Überschrift mit dem Seitentitel, ein Text und
der Seiteninhalt, Fußzeile ausgeblendet.“ Erwartet: er nimmt `lit/tb-create-template` und sagt, dass die
Vorlage deaktiviert ist und erst mit `lit/tb-activate` wirkt.
- [ ] Divi → Theme Builder öffnen: die Vorlage steht in der Liste, deaktiviert, Bedingung „Seiten: Abnahme 3b –
Vorlage“ (bzw. die Seiten-ID), Kopfzeile = globale Kopfzeile, Fußzeile ausgeblendet.
- [ ] Das Inhalt-Layout im Visual Builder öffnen: Überschrift mit dynamischem Inhalt „Titel“, Text und das
Modul „Inhalt posten“ sind vorhanden und bearbeitbar; der Builder meldet keine Fehler. Ohne Speichern
schließen.
- [ ] Im Theme Builder eine harmlose Änderung an einer anderen, deaktivierten Vorlage machen (z. B. Titel) und
„Änderungen speichern“: die neue Vorlage bleibt in der Liste. Die Änderung danach zurücknehmen.
- [ ] Den Assistenten bitten, die Vorlage aufzulisten: `lit/tb-list` zeigt sie deaktiviert, mit Layout-IDs und
`template_rev`.
## 2. Aktivieren mit Probelauf
- [ ] Den Assistenten bitten, die Vorlage zu aktivieren. Erwartet: er zeigt zuerst den Probelauf (Bedingungen,
Layouts, Überschneidungen mit anderen aktiven Vorlagen) und fragt nach, bevor er etwas speichert.
- [ ] Nach der Nachfrage ablehnen: im Theme Builder ist die Vorlage weiter deaktiviert.
- [ ] Erneut bitten und zustimmen: im Theme Builder ist sie aktiv. Die Vorschau der Entwurfsseite zeigt die
Vorlage (Überschrift mit dem Seitentitel, Text, Seiteninhalt, keine Fußzeile). Die Startseite und andere
Seiten sehen aus wie vorher.
- [ ] Den Assistenten bitten, im Inhalt-Layout den Text zu ändern: er meldet, dass die Vorlage aktiv ist, zeigt
den Probelauf und fragt nach. Nach Zustimmung zeigt die Vorschau den neuen Text; unter „Revisionen“ des
Layouts steht der alte Stand.
- [ ] Den Assistenten bitten, die Bedingungen der aktiven Vorlage zu ändern (z. B. eine Ausnahme hinzufügen):
Probelauf mit vorher/nachher, erst nach Zustimmung gespeichert.
- [ ] Den Assistenten bitten, die Vorlage wieder zu deaktivieren (Probelauf, Zustimmung): im Theme Builder ist
sie deaktiviert, die Vorschau der Entwurfsseite zeigt wieder das normale Seitenlayout.
- [ ] Unter Einstellungen → LIT MCP → Protokoll stehen Anlegen, Aktivieren, Layout-Änderung, Bedingungen und
Deaktivieren.
## 3. Bedingungen für einen Pods-Inhaltstyp
- [ ] Den KI-Assistenten bitten: „Lege eine Vorlage für alle [Fixture] Personen an: globale Kopfzeile, im Inhalt
Name, Funktion, Stufe und Mentor, Fußzeile wie bisher.“ Erwartet: er liest vorher die Felder
(`lit/divi-dynamic-sources` für `lit_fixture_person`), legt die Vorlage deaktiviert an und weist darauf hin,
dass Divi beim Mentor nur die ID zeigt und bei der Stufe den gespeicherten Wert statt der Beschriftung.
- [ ] Divi → Theme Builder: die Vorlage hat die Bedingung „Alle [Fixture] Personen“ und ist deaktiviert.
- [ ] Das Inhalt-Layout im Visual Builder öffnen: die Module zeigen dynamischen Inhalt „Benutzerdefiniertes
Feld“ mit „role“, „level“ und „mentor“ und lassen sich im Dialog bearbeiten. Ohne Speichern schließen.
- [ ] Den Assistenten bitten, die Bedingung auf genau eine Person zu ändern (Erika Muster), dann zu aktivieren
(Probelauf, Zustimmung): die Seite dieser Person zeigt Name und Funktion („Geschäftsführung“), die Stufe als
„senior“; die Seite von Max Beispiel sieht aus wie vorher. Danach sofort wieder deaktivieren lassen.
## 4. Ähnliche Abschnitte zu globalen Modulen machen
- [ ] Zwei Entwurfsseiten „Abnahme 3b – A“ und „Abnahme 3b – B“ mit demselben Abschnitt bauen (im Visual Builder
den Abschnitt von A kopieren und in B einfügen), dazu eine Seite „Abnahme 3b – C“ mit demselben Abschnitt,
aber einem geänderten Text.
- [ ] Den Assistenten bitten, wiederkehrende Abschnitte zu finden und zu globalen Modulen zu machen. Erwartet:
`lit/library-find-similar` nennt B als gleich und C als nur gleich aufgebaut; er schlägt vor, den Abschnitt
von A umzuwandeln, und fragt nach.
- [ ] Nach Zustimmung: `lit/library-make-global` legt einen globalen Eintrag an und setzt ihn auf Seite A als
Instanz ein; für Seite B zeigt er zuerst den Probelauf und ersetzt erst nach Zustimmung. Seite C bleibt
unverändert.
- [ ] Divi → Divi-Bibliothek: der Eintrag ist ein globaler Abschnitt.
- [ ] Seiten A und B im Visual Builder öffnen: der Abschnitt ist als globales Modul gekennzeichnet; eine
Änderung daran im Builder erscheint nach dem Speichern auf beiden Seiten. Unter „Revisionen“ von B steht der
Stand vor dem Ersetzen.
- [ ] Den Assistenten bitten, in eine andere Entwurfsseite einen neuen Abschnitt einzufügen, der wie der
Bibliotheksabschnitt aufgebaut ist: die Antwort weist auf den Bibliothekseintrag hin.
## 5. Rechte
- [ ] Optional mit einem Redakteurs-Konto (Rolle unter Einstellungen → LIT MCP → Zugriff freigeben, den
Assistenten in einem privaten Browserfenster mit diesem Konto verbinden): der Assistent sieht keine Vorlagen
und erklärt, dass die Berechtigung für den Theme Builder fehlt. Danach die Freigabe zurücknehmen und wieder
als Administrator verbinden.
- [ ] Gruppe „Divi Theme Builder“ auf „nur lesen“: `lit/tb-list` geht, Anlegen und Aktivieren fehlen, Änderungen
an Theme-Builder-Layouts mit den Divi-Werkzeugen werden abgelehnt. Danach auf den Standard zurückstellen.
## Aufräumen
- [ ] Divi → Theme Builder: die Vorlagen „Abnahme 3b …“ und die Personen-Vorlage löschen und „Änderungen
speichern“. Danach dieselben Vorlagen aktiv wie in der Vorbereitung notiert.
- [ ] Die Seiten „Abnahme 3b …“ und den globalen Eintrag in der Divi-Bibliothek löschen (auch aus dem Papierkorb).
- [ ] Optional: die Einträge der Abnahme im Protokoll-Tab über „Protokoll leeren“ entfernen.
- [ ] Ob Pods wieder deaktiviert werden soll, entscheidet der Benutzer; die Fixture-Daten („[Fixture] Personen“)
bleiben stehen, bis ihr Aufräumen laut `tests/Fixtures/divi/stage3b/README.md` gewünscht ist.
+541
View File
@@ -0,0 +1,541 @@
#!/usr/bin/env bash
# End-to-end checks of Etappe 3b (Divi Theme Builder, Pods fields, find-similar, make-global, P10, P13) through the
# OAuth-protected endpoint. Runs INSIDE the DDEV web container (Divi 5 must be the active theme):
# cd /home/janniclas/Projekte/WordPress && timeout 900 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/tb-smoke.sh
# REQUIRE_PODS=1 makes the Pods section mandatory (else it is skipped with SKIP when Pods is inactive).
#
# Creates only temporary data of this run (titles "LIT Smoke <run> …"): draft pages, a post, Theme Builder templates
# and layouts, library entries, its OWN temporary Pods pod (ruling Q6 – lit_person and lit_fixture_person are never
# touched), a role cloned from "editor" (no edit_theme_options, so no Theme Builder) with one user, and it changes the
# options lit_mcp_allowed_roles/lit_mcp_exposure for a while. Visible site: the only template this script ever
# enables has exactly one condition – a temporary DRAFT page, which visitors cannot open (it is rendered through a
# logged-in preview). The EXIT trap removes, by exact ID: the created templates from the live Theme Builder list,
# all created posts (with revisions, meta and term relationships), a Theme Builder draft it may have created,
# change-log rows, the pod, the user with its app passwords and sessions and the role; it restores every option it
# touched (lit_mcp_*, Divi's Theme Builder backup option, rewrite_rules; absent stays absent) and clears Divi's
# caches. The last section checks that the Theme Builder list and every template's enabled state equal the state
# before the run and that nothing of the run is left. [Fixture]/[Test] content is only read.
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)
STAGE3=$PLUGIN/tests/Fixtures/divi/stage3
RUN="$(date +%s)-$$"
PREFIX="LIT Smoke $RUN"
DIGITS=$(printf '%s' "$RUN" | tr -dc '0-9')
# Per-run names: cleanup never touches an account, role or pod that existed before this run.
EDITOR_LOGIN="lit-tb-editor-$RUN"
EDITOR_ROLE="lit_tb_editor_${RUN//-/_}"
POD="lit_smk_${DIGITS: -8}"
EDITOR_CREATED=0
ROLE_CREATED=0
POD_CREATED=0
POD_POSTS=()
CREATED_POSTS=()
CREATED_TEMPLATES=()
DRAFT_TB=""
SESSION_COOKIE=""
# State before the run (restored by cleanup, compared at the end).
option_json() { wp option get "$1" --format=json 2>/dev/null || echo '__none__'; }
ORIGINAL_ROLES=$(option_json lit_mcp_allowed_roles)
ORIGINAL_EXPOSURE=$(option_json lit_mcp_exposure)
ORIGINAL_REWRITE=$(option_json rewrite_rules)
TB_ID=$(wp eval 'echo (int) et_theme_builder_get_theme_builder_post_id(true, false);')
BACKUP_OPTION="et_tb_templates_backup_$TB_ID"
ORIGINAL_BACKUP=$(option_json "$BACKUP_OPTION")
# The live Theme Builder list with every template's _et_enabled, exactly as stored.
tb_state() {
LIT_TB="$TB_ID" wp eval '
$tb = (int) getenv("LIT_TB");
$rows = get_post_meta($tb, "_et_template");
$enabled = [];
foreach ($rows as $id) { $enabled[(string) $id] = get_post_meta((int) $id, "_et_enabled", true); }
echo wp_json_encode(["list" => $rows, "enabled" => $enabled]);'
}
TB_BEFORE=$(tb_state)
# Divi's front-end feature caches (created by any page view, e.g. this script's renders): absent stays absent.
divi_caches() {
wp eval 'global $wpdb; echo implode(" ", $wpdb->get_col("SELECT option_name FROM $wpdb->options WHERE option_name LIKE \"\\_et\\_builder\\_%\\_feature\\_cache\" ORDER BY option_name"));'
}
DIVI_CACHES_BEFORE=" $(divi_caches) "
restore_option() { # $1 = option, $2 = original JSON or __none__
if [ "$2" = "__none__" ]; then
wp option delete "$1" >/dev/null 2>&1 || true
else
wp option update "$1" "$2" --format=json >/dev/null 2>&1 || true
fi
}
# Every created post (and those a stopped run could not record: title prefix of this run) – idempotent.
cleanup() {
local found id name
found=$(LIT_PREFIX="$PREFIX " wp eval '
global $wpdb;
echo implode(" ", $wpdb->get_col($wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE post_title LIKE %s AND post_type IN (%s, %s, %s, %s, %s, %s, %s)",
$wpdb->esc_like((string) getenv("LIT_PREFIX")) . "%",
"page", "post", "et_pb_layout", "et_template", "et_header_layout", "et_body_layout", "et_footer_layout"
)));' 2>/dev/null || true)
for id in $found; do CREATED_POSTS+=("$id"); done
# 1. Off the live list first (nothing of this run may stay visible or listed), then disabled.
for id in "${CREATED_TEMPLATES[@]}"; do
LIT_ID="$id" LIT_TB="$TB_ID" wp eval '
$id = (string) getenv("LIT_ID");
delete_post_meta((int) getenv("LIT_TB"), "_et_template", $id);
update_post_meta((int) $id, "_et_enabled", "0");' >/dev/null 2>&1 || true
done
if [ -n "$DRAFT_TB" ]; then wp post delete "$DRAFT_TB" --force >/dev/null 2>&1 || true; DRAFT_TB=""; fi
# 2. Change-log rows: every row of the temporary user and every row about a post of this run.
LIT_LOGIN="$EDITOR_LOGIN" LIT_IDS="${CREATED_POSTS[*]} ${CREATED_TEMPLATES[*]}" wp eval '
global $wpdb;
$table = $wpdb->prefix . "lit_mcp_log";
$u = get_user_by("login", getenv("LIT_LOGIN"));
if ($u) { $wpdb->delete($table, ["user_id" => $u->ID]); }
$types = ["page", "post", "et_pb_layout", "et_template", "et_header_layout", "et_body_layout", "et_footer_layout"];
foreach (array_unique(array_filter(explode(" ", (string) getenv("LIT_IDS")))) as $id) {
foreach ($types as $type) { $wpdb->delete($table, ["object_type" => $type, "object_id" => $id]); }
}' >/dev/null 2>&1 || true
# 3. Posts with revisions and meta; term relationships also for types a taxonomy is not registered for.
LIT_IDS="${CREATED_POSTS[*]} ${CREATED_TEMPLATES[*]}" wp eval '
global $wpdb;
foreach (array_unique(array_filter(array_map("intval", explode(" ", (string) getenv("LIT_IDS"))))) as $id) {
if (get_post($id)) { wp_delete_post($id, true); }
$wpdb->delete($wpdb->term_relationships, ["object_id" => $id]);
}' >/dev/null 2>&1 || true
CREATED_POSTS=()
CREATED_TEMPLATES=()
# 4. The temporary pod (definition, fields, content) and anything of it left by ID.
if [ "$POD_CREATED" = 1 ]; then
LIT_POD="$POD" wp eval '
if (function_exists("pods_api") && pods_api()->load_pod(["name" => getenv("LIT_POD")])) {
pods_api()->delete_pod(["name" => getenv("LIT_POD")], false, true);
}' >/dev/null 2>&1 || true
LIT_POD="$POD" LIT_IDS="${POD_POSTS[*]}" wp eval '
global $wpdb;
$ids = array_merge(array_map("intval", array_filter(explode(" ", (string) getenv("LIT_IDS")))),
array_map("intval", $wpdb->get_col($wpdb->prepare("SELECT ID FROM $wpdb->posts WHERE post_type = %s", getenv("LIT_POD")))));
foreach (array_unique($ids) as $id) { if ($id > 0 && get_post($id)) { wp_delete_post($id, true); } }' >/dev/null 2>&1 || true
POD_CREATED=0
fi
# 5. Accounts, options, caches.
cleanup_app_passwords_for "$ADMIN_LOGIN"
if [ "$EDITOR_CREATED" = 1 ]; then
cleanup_app_passwords_for "$EDITOR_LOGIN"
wp user delete "$EDITOR_LOGIN" --yes >/dev/null 2>&1 || true
EDITOR_CREATED=0
fi
if [ "$ROLE_CREATED" = 1 ]; then
wp role delete "$EDITOR_ROLE" >/dev/null 2>&1 || true
ROLE_CREATED=0
fi
restore_option lit_mcp_allowed_roles "$ORIGINAL_ROLES"
restore_option lit_mcp_exposure "$ORIGINAL_EXPOSURE"
restore_option "$BACKUP_OPTION" "$ORIGINAL_BACKUP"
restore_option rewrite_rules "$ORIGINAL_REWRITE"
for name in $(divi_caches 2>/dev/null || true); do
if [[ "$DIVI_CACHES_BEFORE" != *" $name "* ]]; then wp option delete "$name" >/dev/null 2>&1 || true; fi
done
wp eval 'if (function_exists("et_theme_builder_clear_wp_cache")) { et_theme_builder_clear_wp_cache("all"); }
if (class_exists("ET_Core_PageResource")) { ET_Core_PageResource::remove_static_resources("all", "all", true); }' \
>/dev/null 2>&1 || true
}
trap 'rc=$?; cleanup; guard_exit "$rc" || exit $?' EXIT
has() { grep -qF -- "$2" <<<"$1"; } # $1 = haystack, $2 = needle (no pipe: pipefail + grep -q would race)
create_post() { # $1 = post type, $2 = title suffix, $3 = status, $4 = markup (optional) -> ID
LIT_TYPE="$1" LIT_TITLE="$PREFIX $2" LIT_STATUS="$3" LIT_CONTENT="${4:-}" wp --user="$ADMIN_LOGIN" eval '
$id = wp_insert_post(["post_type" => getenv("LIT_TYPE"), "post_status" => getenv("LIT_STATUS"),
"post_title" => getenv("LIT_TITLE"), "post_content" => wp_slash((string) getenv("LIT_CONTENT"))]);
update_post_meta($id, "_et_pb_use_builder", "on");
update_post_meta($id, "_et_pb_use_divi_5", "on");
echo $id;'
}
content_of() { wp eval "echo (string) get_post($1)->post_content;"; }
content_hash() { wp eval "echo hash('sha256', (string) get_post($1)->post_content);"; }
rev_of() { tool lit-divi-outline "{\"post_id\":$1}" | jq -r '.tree_rev'; }
enabled_of() { wp post meta get "$1" _et_enabled; }
listed() { tb_state | jq -e --arg t "$1" '.list | index($t) != null' >/dev/null; } # on the live Theme Builder list
tb_list() { # $1 = arguments JSON (default {})
local args='{}'
if [ $# -gt 0 ]; then args=$1; fi
ability lit/tb-list "$args"
}
template_rev() { tb_list "{\"template_id\":$1}" | jq -r '.templates[0].template_rev // .detached[0].template_rev'; }
activate() { # $1 = template ID, $2 = enabled (true|false), $3 = confirm (true|false)
ability lit/tb-activate "$(jq -cn --argjson id "$1" --arg rev "$(template_rev "$1")" --argjson e "$2" --argjson c "$3" \
'{template_id: $id, template_rev: $rev, enabled: $e} + (if $c then {confirm: true} else {} end)')"
}
details() { echo "$1" | jq -r '.__error' | jq -c '.'; } # the details of a tool error (confirm_required etc.)
# Front-end HTML: $1 = post ID; $2 = "preview" renders it for the logged-in temporary editor (drafts), else anonymous.
render_of() {
local cookie=()
if [ "${2:-}" = preview ]; then cookie=(-H "Cookie: $SESSION_COOKIE"); fi
curl -s --max-time 30 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "${cookie[@]}" "http://localhost/?page_id=$1"
}
render_home() { curl -s --max-time 30 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' "http://localhost/"; }
[ "$(wp eval 'echo get_template();')" = "Divi" ] || fail "Divi must be the active theme"
[ "$TB_ID" -gt 0 ] || fail "setup: no live Theme Builder post (save the Theme Builder once)"
[ "$(wp post list --post_type=et_theme_builder --post_status=auto-draft --format=count)" = "0" ] \
|| fail "setup: the Theme Builder has unsaved changes – save or discard them first"
echo "$TB_BEFORE" | jq -e '[.enabled[]] | all(. != "1")' >/dev/null \
|| fail "setup: a Theme Builder template is enabled – this local site must show no active template before the run"
if wp user get "$EDITOR_LOGIN" >/dev/null 2>&1 || wp role exists "$EDITOR_ROLE" >/dev/null 2>&1; then
fail "user $EDITOR_LOGIN or role $EDITOR_ROLE exists already"
fi
# Temporary editor (per-run role cloned from editor: no edit_theme_options, Divi's "Theme Builder" option off).
wp role create "$EDITOR_ROLE" "LIT Smoke Theme Builder" --clone=editor >/dev/null
ROLE_CREATED=1
wp option update lit_mcp_allowed_roles "[\"administrator\",\"$EDITOR_ROLE\"]" --format=json >/dev/null
wp user create "$EDITOR_LOGIN" "$EDITOR_LOGIN@example.invalid" --role="$EDITOR_ROLE" >/dev/null
EDITOR_CREATED=1
[ "$(LIT_LOGIN="$EDITOR_LOGIN" wp eval 'echo user_can(get_user_by("login", getenv("LIT_LOGIN")), "edit_theme_options") ? "yes" : "no";')" = "no" ] \
|| fail "setup: the temporary editor must lack edit_theme_options"
# Its logged-in cookie previews the temporary draft page (session stored on the temporary user only).
SESSION_COOKIE=$(LIT_LOGIN="$EDITOR_LOGIN" wp eval '
$u = get_user_by("login", getenv("LIT_LOGIN"));
$expires = time() + 1800;
$token = WP_Session_Tokens::get_instance($u->ID)->create($expires);
echo LOGGED_IN_COOKIE . "=" . wp_generate_auth_cookie($u->ID, $expires, "logged_in", $token);')
connect "$ADMIN_LOGIN"
# 1. Exposure: still 22 direct lit tools; the six new abilities are indirect and reachable; no TB compat problem
LIST=$(rpc tools/list '{}')
DIRECT='["lit-content-create","lit-content-get","lit-content-query","lit-content-types","lit-content-update","lit-divi-catalog",
"lit-divi-delete","lit-divi-design-system","lit-divi-insert","lit-divi-move","lit-divi-node-get","lit-divi-outline","lit-divi-update",
"lit-guide","lit-library-insert","lit-media-download","lit-media-preview","lit-media-search","lit-media-upload-from-url",
"lit-page-render-summary","lit-post-publish","lit-site-status"]'
echo "$LIST" | jq -e --argjson want "$DIRECT" '[.result.tools[].name | select(startswith("lit-"))] | sort == ($want | sort)' >/dev/null \
|| fail "direct lit tools: $(echo "$LIST" | jq -c '[.result.tools[].name]')"
for a in lit/tb-list lit/tb-create-template lit/tb-assign lit/tb-activate lit/library-find-similar lit/library-make-global; do
if echo "$LIST" | jq -e --arg t "lit-${a#lit/}" '.result.tools[] | select(.name == $t)' >/dev/null; then fail "$a must be indirect"; fi
tool mcp-adapter-get-ability-info "$(jq -cn --arg a "$a" '{ability_name: $a}')" | jq -e 'has("__error") | not' >/dev/null \
|| fail "$a is not reachable"
done
STATUS=$(tool lit-site-status '{}')
echo "$STATUS" | jq -e '.components[] | select(.id == "divi") | .active == true and .problems == []' >/dev/null \
|| fail "Divi compatibility findings: $STATUS"
pass "exposure: 22 direct tools, the six new abilities indirect and reachable; no Divi/Theme Builder compatibility problem"
# 2. Create a template: disabled, on the live list, body layout by Divi with the plugin's content, invisible
PAGE=$(create_post page "Entwurf" draft '')
CREATED_POSTS+=("$PAGE")
[ "$(wp post get "$PAGE" --field=post_status)" = "draft" ] || fail "setup: the target page must be a draft"
MARKER="LIT-Smoke-Marker-$RUN"
BASE_PREVIEW=$(render_of "$PAGE" preview)
has "$BASE_PREVIEW" "$PREFIX Entwurf" || fail "setup: the preview does not show the draft page: $(head -c 300 <<<"$BASE_PREVIEW")"
CREATED=$(ability lit/tb-create-template "$(jq -cn --arg t "$PREFIX Vorlage" --arg m "$MARKER" --argjson p "$PAGE" '{
title: $t,
header: {source: "global"},
body: {source: "new", nodes: [{type: "section", modules: [{type: "heading", text: "{{post:title}}"}, {type: "text", content: $m}]}]},
footer: {source: "hidden"},
use_on: ["singular:post_type:page:id:\($p)"],
exclude_from: ["search"]}')")
TPL=$(echo "$CREATED" | jq -r '.template_id // empty')
[ -n "$TPL" ] || fail "tb-create-template: $CREATED"
CREATED_TEMPLATES+=("$TPL")
BODY=$(echo "$CREATED" | jq -r '.layouts.body.id')
CREATED_POSTS+=("$BODY")
echo "$CREATED" | jq -e '.enabled == false and .layouts.footer.enabled == false and .layouts.header.source == "global"
and .layouts.body.source == "new"' >/dev/null || fail "tb-create-template result: $CREATED"
[ "$(enabled_of "$TPL")" = "0" ] || fail "the new template is enabled"
[ "$(wp post meta get "$TPL" _et_use_on)" = "singular:post_type:page:id:$PAGE" ] || fail "conditions: $(wp post meta get "$TPL" _et_use_on)"
listed "$TPL" || fail "template $TPL is not on the live Theme Builder list"
[ "$(wp post get "$BODY" --field=post_type)" = "et_body_layout" ] || fail "body layout type"
[ "$(wp post meta get "$BODY" _et_pb_use_divi_5)" = "on" ] || fail "body layout without the Divi 5 flag"
BODY_CONTENT=$(content_of "$BODY")
[[ "$BODY_CONTENT" == "<!-- wp:divi/placeholder -->"* ]] || fail "body layout content: $BODY_CONTENT"
has "$BODY_CONTENT" "$MARKER" || fail "body layout lacks the marker"
tb_list "{\"template_id\":$TPL}" | jq -e '.templates[0] | .enabled == false and .live == false and .attached == true' >/dev/null \
|| fail "tb-list does not show the new template disabled and attached"
if has "$(render_of "$PAGE" preview)" "$MARKER"; then fail "a disabled template is rendered"; fi
pass "tb-create-template: disabled, on the live list, body layout by Divi with the plugin's content, not rendered"
# 3. Divi tools on the body of a disabled template save at once; post-content is allowed in the body
UPD=$(tool lit-divi-insert "$(jq -cn --argjson id "$BODY" --arg rev "$(rev_of "$BODY")" \
'{post_id: $id, tree_rev: $rev, parent: "s0.r0.c0", nodes: [{type: "post-content"}]}')")
echo "$UPD" | jq -e '.ok == true and .templates == []' >/dev/null || fail "insert into the body of a disabled template: $UPD"
has "$(content_of "$BODY")" "wp:divi/post-content" || fail "post-content not stored in the body"
pass "Divi tools write the body of a disabled template without confirm; post-content allowed in the body"
# 4. tb-assign while disabled (Q3): an omitted exclude_from stays, conditions still only the draft page
ASSIGN=$(ability lit/tb-assign "$(jq -cn --argjson id "$TPL" --arg rev "$(template_rev "$TPL")" --argjson p "$PAGE" \
'{template_id: $id, template_rev: $rev, use_on: ["singular:post_type:page:id:\($p)"]}')")
echo "$ASSIGN" | jq -e '.ok == true and .live == false and .previous.exclude_from == ["search"]' >/dev/null || fail "tb-assign: $ASSIGN"
[ "$(wp post meta get "$TPL" _et_exclude_from)" = "search" ] || fail "an omitted exclude_from changed the exceptions"
[ "$(wp post meta get "$TPL" _et_use_on)" = "singular:post_type:page:id:$PAGE" ] || fail "use_on after tb-assign"
[ "$(error_code "$(ability lit/tb-assign "$(jq -cn --argjson id "$TPL" --argjson p "$PAGE" \
'{template_id: $id, template_rev: "abcdef012345", use_on: ["singular:post_type:page:id:\($p)"]}')")")" = "rev_mismatch" ] \
|| fail "tb-assign with a stale template_rev"
pass "tb-assign on a disabled template: saved at once, omitted exclude_from kept (Q3), stale template_rev refused"
# 5. Activation: dry run, then confirm; the draft page shows the template, visitors see nothing new
DRY=$(activate "$TPL" true false)
[ "$(error_code "$DRY")" = "confirm_required" ] || fail "activation without confirm: $DRY"
details "$DRY" | jq -e --arg p "$PAGE" '.use_on | length == 1 and (.[0] | contains($p))' >/dev/null || fail "activation dry run: $DRY"
[ "$(enabled_of "$TPL")" = "0" ] || fail "the dry run enabled the template"
ON=$(activate "$TPL" true true)
echo "$ON" | jq -e '.enabled == true and .live == true' >/dev/null || fail "activation: $ON"
[ "$(enabled_of "$TPL")" = "1" ] || fail "template not enabled"
PREVIEW=$(render_of "$PAGE" preview)
has "$PREVIEW" "$MARKER" || fail "the active template is not rendered for its draft page"
if has "$(render_of "$PAGE")" "$MARKER"; then fail "a visitor sees the draft page's template"; fi
if has "$(render_home)" "$MARKER"; then fail "the template shows on the home page"; fi
pass "tb-activate: dry run first, then enabled; only the draft page (preview) shows it, visitors see nothing"
# 6. Live template: layout changes and condition changes are a dry run first (Q9), Q3 on the live template
HASH=$(content_hash "$BODY")
CHANGE=$(jq -cn --arg m "$MARKER-geändert" '[{id: "s0.r0.c0.m1", set: {content: $m}}]')
LIVE=$(tool lit-divi-update "$(jq -cn --argjson id "$BODY" --arg rev "$(rev_of "$BODY")" --argjson c "$CHANGE" '{post_id: $id, tree_rev: $rev, changes: $c}')")
[ "$(error_code "$LIVE")" = "confirm_required" ] || fail "visible layout changed without confirm: $LIVE"
details "$LIVE" | jq -e --argjson t "$TPL" '.templates | map(.id) | index($t) != null' >/dev/null || fail "dry run lacks the template: $LIVE"
[ "$(content_hash "$BODY")" = "$HASH" ] || fail "the dry run wrote the layout"
DEL=$(tool lit-divi-delete "$(jq -cn --argjson id "$BODY" --arg rev "$(rev_of "$BODY")" '{post_id: $id, tree_rev: $rev, ids: ["s0.r0.c0.m0"]}')")
[ "$(error_code "$DEL")" = "confirm_required" ] || fail "visible layout: delete without confirm: $DEL"
[ "$(content_hash "$BODY")" = "$HASH" ] || fail "the delete dry run wrote the layout"
DONE=$(tool lit-divi-update "$(jq -cn --argjson id "$BODY" --arg rev "$(rev_of "$BODY")" --argjson c "$CHANGE" \
'{post_id: $id, tree_rev: $rev, confirm: true, changes: $c}')")
echo "$DONE" | jq -e --argjson t "$TPL" '.ok == true and (.templates | map(.id) | index($t) != null)' >/dev/null \
|| fail "confirmed layout change: $DONE"
has "$(render_of "$PAGE" preview)" "$MARKER-geändert" || fail "the changed layout is not rendered"
REASSIGN=$(jq -cn --argjson id "$TPL" --arg rev "$(template_rev "$TPL")" --argjson p "$PAGE" \
'{template_id: $id, template_rev: $rev, use_on: ["singular:post_type:page:id:\($p)"], exclude_from: []}')
ADRY=$(ability lit/tb-assign "$REASSIGN")
[ "$(error_code "$ADRY")" = "confirm_required" ] || fail "conditions of an active template changed without confirm: $ADRY"
details "$ADRY" | jq -e '(.before.exclude_from | length) == 1 and .after.exclude_from == [] and (.after.use_on | length) == 1' >/dev/null \
|| fail "tb-assign dry run before/after: $ADRY"
[ "$(wp post meta get "$TPL" _et_exclude_from)" = "search" ] || fail "the dry run changed the exceptions"
ACONF=$(ability lit/tb-assign "$(echo "$REASSIGN" | jq -c '. + {confirm: true}')")
echo "$ACONF" | jq -e '.ok == true and .live == true and .previous.exclude_from == ["search"]' >/dev/null || fail "confirmed tb-assign: $ACONF"
[ -z "$(wp post meta get "$TPL" _et_exclude_from 2>/dev/null || true)" ] || fail "exclude_from [] did not clear the exceptions"
[ "$(wp post meta get "$TPL" _et_use_on)" = "singular:post_type:page:id:$PAGE" ] || fail "use_on of the live template changed"
pass "live template: layout update/delete and conditions only after a dry run; confirmed change rendered; [] clears exceptions"
# 7. Theme Builder draft: blocks create/assign/enable, the off-switch still works (Q20); default template refused
DEFAULT=$(tb_list | jq -r '[.templates[] | select(.default)][0].id // empty')
if [ -n "$DEFAULT" ]; then
DEF_ENABLED=$(tb_list "{\"template_id\":$DEFAULT}" | jq -r '.templates[0].enabled')
[ "$(error_code "$(activate "$DEFAULT" "$([ "$DEF_ENABLED" = true ] && echo false || echo true)" false)")" = "unsupported" ] \
|| fail "the default template is not refused"
fi
TPL_REV=$(template_rev "$TPL")
DRAFT_TB=$(wp eval 'echo (int) wp_insert_post(["post_type" => "et_theme_builder", "post_status" => "auto-draft", "post_title" => "Theme Builder"]);')
[ "$DRAFT_TB" -gt 0 ] || fail "setup: Theme Builder draft"
[ "$(error_code "$(ability lit/tb-create-template "$(jq -cn --arg t "$PREFIX Gesperrt" '{title: $t}')")")" = "unsupported" ] \
|| fail "tb-create-template despite a Theme Builder draft"
[ "$(error_code "$(ability lit/tb-assign "$(jq -cn --argjson id "$TPL" --arg rev "$TPL_REV" --argjson p "$PAGE" \
'{template_id: $id, template_rev: $rev, use_on: ["singular:post_type:page:id:\($p)"], confirm: true}')")")" = "unsupported" ] \
|| fail "tb-assign despite a Theme Builder draft"
OFF_DRY=$(ability lit/tb-activate "$(jq -cn --argjson id "$TPL" --arg rev "$TPL_REV" '{template_id: $id, template_rev: $rev, enabled: false}')")
[ "$(error_code "$OFF_DRY")" = "confirm_required" ] || fail "deactivation without confirm: $OFF_DRY"
[ "$(enabled_of "$TPL")" = "1" ] || fail "the deactivation dry run switched"
OFF=$(ability lit/tb-activate "$(jq -cn --argjson id "$TPL" --arg rev "$TPL_REV" '{template_id: $id, template_rev: $rev, enabled: false, confirm: true}')")
echo "$OFF" | jq -e '.enabled == false and .live == false and (.warnings | map(contains("Entwurf")) | any)' >/dev/null \
|| fail "off-switch with a Theme Builder draft: $OFF"
[ "$(enabled_of "$TPL")" = "0" ] || fail "template still enabled"
[ "$(error_code "$(activate "$TPL" true true)")" = "unsupported" ] || fail "tb-activate (on) despite a Theme Builder draft"
[ "$(enabled_of "$TPL")" = "0" ] || fail "enabled despite a Theme Builder draft"
wp post delete "$DRAFT_TB" --force >/dev/null
DRAFT_TB=""
if has "$(render_of "$PAGE" preview)" "$MARKER"; then fail "a deactivated template is still rendered"; fi
pass "Theme Builder draft blocks create, assign and enable; the off-switch works with a warning; default template refused"
# 8. Detached template (a stale Theme Builder save removed it): listed, refused, reattached disabled on request
LIT_TB="$TB_ID" LIT_ID="$TPL" wp eval '
delete_post_meta((int) getenv("LIT_TB"), "_et_template", (string) getenv("LIT_ID"));
update_post_meta((int) getenv("LIT_ID"), "_et_theme_builder_marked_as_unused", "2026-09-27 00:00:00");' >/dev/null
if listed "$TPL"; then fail "setup: template still listed"; fi
tb_list | jq -e --argjson t "$TPL" '(.detached | map(.id) | index($t) != null) and (.templates | map(.id) | index($t) == null)' >/dev/null \
|| fail "detached template not listed under detached"
[ "$(error_code "$(activate "$TPL" true true)")" = "validation_failed" ] || fail "a detached template was activated"
[ "$(error_code "$(ability lit/tb-assign "$(jq -cn --argjson id "$TPL" --arg rev "$(template_rev "$TPL")" --argjson p "$PAGE" \
'{template_id: $id, template_rev: $rev, use_on: ["singular:post_type:page:id:\($p)"]}')")")" = "validation_failed" ] \
|| fail "tb-assign on a detached template without reattach"
RE=$(ability lit/tb-assign "$(jq -cn --argjson id "$TPL" --arg rev "$(template_rev "$TPL")" --argjson p "$PAGE" \
'{template_id: $id, template_rev: $rev, use_on: ["singular:post_type:page:id:\($p)"], reattach: true}')")
echo "$RE" | jq -e '.ok == true and .live == false' >/dev/null || fail "reattach: $RE"
listed "$TPL" || fail "not reattached"
[ "$(enabled_of "$TPL")" = "0" ] || fail "reattached enabled"
[ -z "$(wp post meta get "$TPL" _et_theme_builder_marked_as_unused 2>/dev/null || true)" ] || fail "unused mark kept"
pass "stale-editor detach: listed as detached, activate/assign refused, reattach puts it back disabled and unmarked"
# 9. Rights: an editor without the Theme Builder; group "nur lesen" and "deaktiviert" gate the Divi tools
connect "$EDITOR_LOGIN"
[ "$(error_code "$(ability lit/tb-list '{}')")" = "forbidden" ] || fail "editor lists the Theme Builder"
[ "$(error_code "$(ability lit/tb-create-template "$(jq -cn --arg t "$PREFIX Redakteur" '{title: $t}')")")" = "forbidden" ] \
|| fail "editor creates a template"
[ "$(error_code "$(ability lit/tb-activate "$(jq -cn --argjson id "$TPL" '{template_id: $id, template_rev: "abcdef012345", enabled: true, confirm: true}')")")" = "not_found" ] \
|| fail "editor reaches the template"
[ "$(error_code "$(ability lit/tb-assign "$(jq -cn --argjson id "$TPL" --argjson p "$PAGE" \
'{template_id: $id, template_rev: "abcdef012345", use_on: ["singular:post_type:page:id:\($p)"]}')")")" = "not_found" ] \
|| fail "editor reaches the template's conditions"
[ "$(error_code "$(tool lit-divi-outline "{\"post_id\":$BODY}")")" = "not_found" ] || fail "editor reads a Theme Builder layout"
[ "$(enabled_of "$TPL")" = "0" ] || fail "editor refusal changed the template"
connect "$ADMIN_LOGIN"
wp option update lit_mcp_exposure '{"@readonly:divi-theme-builder":true}' --format=json >/dev/null
connect "$ADMIN_LOGIN"
[ "$(error_code "$(tool lit-divi-update "$(jq -cn --argjson id "$BODY" --arg rev "$(rev_of "$BODY")" \
'{post_id: $id, tree_rev: $rev, changes: [{id: "s0.r0.c0.m1", label: "Nur lesen"}]}')")")" = "forbidden" ] \
|| fail "group read-only: Theme Builder layout written"
[ -n "$(template_rev "$TPL")" ] || fail "group read-only: tb-list must stay readable"
RO=$(activate "$TPL" true true)
has "$RO" "Ability 'lit/tb-activate' not found" || fail "group read-only: tb-activate still executable: $RO"
[ "$(enabled_of "$TPL")" = "0" ] || fail "group read-only: template enabled"
wp option update lit_mcp_exposure '{"@group:divi-theme-builder":"disabled"}' --format=json >/dev/null
connect "$ADMIN_LOGIN"
[ "$(error_code "$(tool lit-divi-outline "{\"post_id\":$BODY}")")" = "not_found" ] || fail "group disabled: Theme Builder layout readable"
GONE=$(ability lit/tb-list '{}')
echo "$GONE" | jq -e 'has("__error")' >/dev/null && has "$GONE" 'not found' || fail "group disabled: tb-list still executable: $GONE"
restore_option lit_mcp_exposure "$ORIGINAL_EXPOSURE"
connect "$ADMIN_LOGIN"
pass "editor without the Theme Builder: forbidden/not_found everywhere; group read-only and disabled gate the Divi tools"
# 10. Pods fields in a Theme Builder body – with a temporary pod of this run (ruling Q6)
if wp plugin is-active pods >/dev/null 2>&1; then
if [ "$(LIT_POD="$POD" wp eval 'echo pods_api()->load_pod(["name" => getenv("LIT_POD")]) ? "yes" : "no";')" != "no" ] \
|| [ "$POD" = lit_person ] || [ "$POD" = lit_fixture_person ]; then
fail "pod $POD exists already"
fi
POD_CREATED=1
POD_IDS=$(LIT_POD="$POD" LIT_LABEL="$PREFIX Personen" wp eval '
$api = pods_api();
$pod = getenv("LIT_POD");
$ids = [(int) $api->save_pod(["name" => $pod, "label" => getenv("LIT_LABEL"), "label_singular" => getenv("LIT_LABEL"),
"type" => "post_type", "storage" => "meta", "public" => 1, "show_ui" => 1])];
foreach ([
["name" => "role", "label" => "Funktion", "type" => "text"],
["name" => "level", "label" => "Stufe", "type" => "pick", "pick_object" => "custom-simple", "pick_custom" => "junior|Junior\nsenior|Senior"],
["name" => "mentor", "label" => "Mentor", "type" => "pick", "pick_object" => "post_type", "pick_val" => $pod, "pick_format_type" => "single"],
] as $field) {
$ids[] = (int) $api->save_field(array_merge(["pod" => $pod], $field));
}
echo implode(" ", $ids);')
for id in $POD_IDS; do POD_POSTS+=("$id"); done
[ "$(echo "$POD_IDS" | wc -w)" = 4 ] || fail "setup: temporary pod $POD: $POD_IDS"
CONDS=$(tb_list '{"conditions":true}')
echo "$CONDS" | jq -e --arg c "singular:post_type:$POD:all" '.conditions | map(.id) | index($c) != null' >/dev/null \
|| fail "Divi offers no condition for the temporary pod"
PODS=$(ability lit/tb-create-template "$(jq -cn --arg t "$PREFIX Person-Vorlage" --arg pod "$POD" '{
title: $t, header: {source: "default"}, footer: {source: "default"},
body: {source: "new", nodes: [{type: "section", modules: [
{type: "heading", text: "{{post:title}}"}, {type: "text", content: "{{field:role}}"},
{type: "text", content: "{{field:level}}"}, {type: "text", content: "{{field:mentor}}"}]}]},
use_on: ["singular:post_type:\($pod):all"]}')")
PTPL=$(echo "$PODS" | jq -r '.template_id // empty')
[ -n "$PTPL" ] || fail "Pods template: $PODS"
CREATED_TEMPLATES+=("$PTPL")
CREATED_POSTS+=("$(echo "$PODS" | jq -r '.layouts.body.id')")
[ "$(enabled_of "$PTPL")" = "0" ] || fail "the Pods template is enabled"
echo "$PODS" | jq -e '[.warnings[] | select(contains("„mentor“"))] | length == 1' >/dev/null || fail "no warning for mentor: $PODS"
echo "$PODS" | jq -e '[.warnings[] | select(contains("„level“"))] | length == 1' >/dev/null || fail "no note for level: $PODS"
echo "$PODS" | jq -e '[.warnings[] | select(contains("„role“"))] | length == 0' >/dev/null || fail "role wrongly warned: $PODS"
PBODY=$(echo "$PODS" | jq -r '.layouts.body.id')
PUPD=$(tool lit-divi-insert "$(jq -cn --argjson id "$PBODY" --arg rev "$(rev_of "$PBODY")" \
'{post_id: $id, tree_rev: $rev, parent: "s0.r0.c0", nodes: [{type: "text", content: "{{field:mentor}}"}]}')")
echo "$PUPD" | jq -e '.ok == true and ([.warnings[] | select(contains("„mentor“"))] | length >= 1)' >/dev/null \
|| fail "Divi tool on the Pods body without the mentor warning: $PUPD"
pass "Pods (temporary pod $POD): condition offered; relation field warned, choice field noted, text field clean; template disabled"
elif [ "${REQUIRE_PODS:-0}" = 1 ]; then
fail "Pods is not active (REQUIRE_PODS=1)"
else
echo "SKIP - Pods section (Pods inactive)"
fi
# 11. find-similar and make-global (mode 1, mode 2: dry run, confirm, identical only)
SECTION=$(tr -d '\r' < "$STAGE3/library--library-abschnitt.html")
VARIANT=${SECTION/Library-Blurb\"/Library-Blurb anders\"}
[ "$VARIANT" != "$SECTION" ] || fail "setup: variant equals the section"
A=$(create_post page "Ähnlich A" draft "$SECTION")
B=$(create_post page "Ähnlich B" draft "$SECTION")
C=$(create_post page "Ähnlich C" draft "$VARIANT")
CREATED_POSTS+=("$A" "$B" "$C")
SIM=$(ability lit/library-find-similar "$(jq -cn --argjson a "$A" '{post_id: $a, id: "s0", post_type: "page", limit: 200}')")
echo "$SIM" | jq -e --argjson b "$B" --argjson c "$C" '
([.matches[] | select(.post_id == $b and .id == "s0" and .match == "identical")] | length == 1) and
([.matches[] | select(.post_id == $c and .id == "s0" and .match == "structure")] | length == 1)' >/dev/null \
|| fail "find-similar: $SIM"
MG=$(ability lit/library-make-global "$(jq -cn --argjson a "$A" --arg rev "$(rev_of "$A")" --arg t "$PREFIX Global" \
'{post_id: $a, tree_rev: $rev, id: "s0", title: $t}')")
LIB=$(echo "$MG" | jq -r '.library_id // empty')
[ -n "$LIB" ] || fail "make-global: $MG"
CREATED_POSTS+=("$LIB")
[ "$(content_of "$LIB")" = "$SECTION" ] || fail "global entry bytes differ from the section"
[ "$(wp eval "echo implode(',', wp_get_object_terms($LIB, 'scope', ['fields' => 'slugs']));")" = "global" ] || fail "entry is not global"
has "$(content_of "$A")" "\"globalModule\":\"$LIB\",\"blockName\":\"divi/section\"" || fail "page A holds no instance"
BHASH=$(content_hash "$B")
CHASH=$(content_hash "$C")
BREVS=$(wp eval "echo count(wp_get_post_revisions($B));")
TARGETS=$(jq -cn --argjson lib "$LIB" --argjson b "$B" --arg rev "$(rev_of "$B")" '{library_id: $lib, targets: [{post_id: $b, tree_rev: $rev, ids: ["s0"]}]}')
MDRY=$(ability lit/library-make-global "$TARGETS")
[ "$(error_code "$MDRY")" = "confirm_required" ] || fail "replace without dry run: $MDRY"
details "$MDRY" | jq -e --argjson b "$B" '.affected | map(.post_id) == [$b]' >/dev/null || fail "make-global dry run: $MDRY"
[ "$(content_hash "$B")" = "$BHASH" ] || fail "the dry run changed page B"
BAD=$(ability lit/library-make-global "$(jq -cn --argjson lib "$LIB" --argjson c "$C" --arg rev "$(rev_of "$C")" \
'{library_id: $lib, confirm: true, targets: [{post_id: $c, tree_rev: $rev, ids: ["s0"]}]}')")
[ "$(error_code "$BAD")" = "validation_failed" ] || fail "a section that is not identical was replaced: $BAD"
[ "$(content_hash "$C")" = "$CHASH" ] || fail "the refused target C changed"
MCONF=$(ability lit/library-make-global "$(echo "$TARGETS" | jq -c '. + {confirm: true}')")
echo "$MCONF" | jq -e --argjson b "$B" '.pages[0].post_id == $b and .pages[0].revision_id > 0' >/dev/null || fail "replace occurrences: $MCONF"
has "$(content_of "$B")" "\"globalModule\":\"$LIB\"" || fail "page B holds no instance"
[ "$(wp eval "echo count(wp_get_post_revisions($B));")" -gt "$BREVS" ] || fail "replacing made no revision of page B"
[ "$(content_hash "$C")" = "$CHASH" ] || fail "page C changed"
pass "find-similar: identical and structure; make-global mode 1 (entry + instance), mode 2 dry run, confirm, identical only"
# 12. P13: with the library group disabled no library ID is named; P10: post_link_url_page refuses a post
wp option update lit_mcp_exposure '{"@group:divi-library":"disabled"}' --format=json >/dev/null
connect "$ADMIN_LOGIN"
if has "$(tool lit-divi-outline "{\"post_id\":$A}")" "$LIB"; then fail "outline names library ID $LIB with the library hidden"; fi
if has "$(tool lit-divi-node-get "{\"post_id\":$A,\"id\":\"g0\"}")" "$LIB"; then fail "node-get names library ID $LIB"; fi
if has "$(tool lit-page-render-summary "{\"post_id\":$A}")" "$LIB"; then fail "render summary names library ID $LIB"; fi
restore_option lit_mcp_exposure "$ORIGINAL_EXPOSURE"
connect "$ADMIN_LOGIN"
has "$(tool lit-divi-outline "{\"post_id\":$A}")" "$LIB" || fail "control: outline does not name $LIB with the library visible"
POST=$(create_post post "Beitrag" draft '')
CREATED_POSTS+=("$POST")
LINK="\$variable({\"type\":\"content\",\"value\":{\"name\":\"post_link_url_page\",\"settings\":{\"post_id\":\"$POST\"}}})\$"
PHASH=$(content_hash "$PAGE")
P10=$(tool lit-divi-insert "$(jq -cn --argjson id "$PAGE" --arg rev "$(rev_of "$PAGE")" --arg l "$LINK" \
'{post_id: $id, tree_rev: $rev, nodes: [{type: "section", modules: [{type: "button", text: "Mehr", url: $l}]}]}')")
[ "$(error_code "$P10")" = "validation_failed" ] || fail "post_link_url_page accepted a post: $P10"
[ "$(content_hash "$PAGE")" = "$PHASH" ] || fail "the refused link changed the page"
pass "P13: no library ID without library access; P10: post_link_url_page refuses a post as target"
# 13. Cleanup now and check it: Theme Builder list and enabled states as before, nothing of the run left
if has "$(render_home)" "$MARKER"; then fail "the home page shows the marker"; fi
KEEP_IDS="${CREATED_POSTS[*]} ${CREATED_TEMPLATES[*]} ${POD_POSTS[*]}"
cleanup
[ " $(divi_caches) " = "$DIVI_CACHES_BEFORE" ] || fail "Divi feature cache options differ: before$DIVI_CACHES_BEFORE, after $(divi_caches)"
[ "$(tb_state)" = "$TB_BEFORE" ] || fail "Theme Builder list/enabled states differ: before $TB_BEFORE, after $(tb_state)"
tb_state | jq -e '[.enabled[]] | all(. != "1")' >/dev/null || fail "a Theme Builder template is enabled after the run: $(tb_state)"
[ "$(option_json "$BACKUP_OPTION")" = "$ORIGINAL_BACKUP" ] || fail "Theme Builder backup option not restored"
[ "$(option_json lit_mcp_exposure)" = "$ORIGINAL_EXPOSURE" ] || fail "lit_mcp_exposure not restored"
[ "$(option_json lit_mcp_allowed_roles)" = "$ORIGINAL_ROLES" ] || fail "lit_mcp_allowed_roles not restored"
[ "$(option_json rewrite_rules)" = "$ORIGINAL_REWRITE" ] || fail "rewrite_rules not restored"
LEFT=$(LIT_IDS="$KEEP_IDS" LIT_PREFIX="$PREFIX" LIT_POD="$POD" LIT_LOGIN="$EDITOR_LOGIN" LIT_ROLE="$EDITOR_ROLE" wp eval '
global $wpdb;
$ids = array_values(array_unique(array_filter(array_map("intval", explode(" ", (string) getenv("LIT_IDS"))))));
$in = $ids === [] ? "0" : implode(",", $ids);
$left = [];
$left["posts"] = (int) $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE ID IN ($in) OR post_parent IN ($in)");
$left["meta"] = (int) $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->postmeta WHERE post_id IN ($in)");
$left["terms"] = (int) $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->term_relationships WHERE object_id IN ($in)");
$left["log"] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}lit_mcp_log WHERE object_id IN (\"" . implode("\",\"", $ids ?: [0]) . "\")");
$left["titles"] = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->posts WHERE post_title LIKE %s",
$wpdb->esc_like((string) getenv("LIT_PREFIX")) . "%"));
$left["pod"] = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = %s OR (post_type IN (\"_pods_pod\", \"_pods_field\") AND post_name = %s)",
getenv("LIT_POD"), getenv("LIT_POD")));
$left["user"] = get_user_by("login", getenv("LIT_LOGIN")) ? 1 : 0;
$left["role"] = wp_roles()->is_role(getenv("LIT_ROLE")) ? 1 : 0;
echo wp_json_encode(array_filter($left));')
[ "$LEFT" = "[]" ] || fail "left over after cleanup: $LEFT"
pass "cleanup: Theme Builder list as before, no template enabled, options restored, no post/meta/term/log/pod/user/role left"
finish