diff --git a/docs/superpowers/notes/2026-09-27-etappe-3b-abnahme.md b/docs/superpowers/notes/2026-09-27-etappe-3b-abnahme.md new file mode 100644 index 0000000..f0eb17e --- /dev/null +++ b/docs/superpowers/notes/2026-09-27-etappe-3b-abnahme.md @@ -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. diff --git a/tests/Integration/tb-smoke.sh b/tests/Integration/tb-smoke.sh new file mode 100755 index 0000000..6660f87 --- /dev/null +++ b/tests/Integration/tb-smoke.sh @@ -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 …"): 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" == ""* ]] || 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