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:
@@ -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.
|
||||
Executable
+541
@@ -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
|
||||
Reference in New Issue
Block a user