#!/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) and (.restore_url | test("/wp-admin/revision\\.php\\?revision=[0-9]+$"))' >/dev/null \ || fail "confirmed layout change (templates, restore_url): $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" # Review M-9: without the Theme Builder rights the refusal is forbidden whatever the ID (like tb-list). [ "$(error_code "$(ability lit/tb-activate "$(jq -cn --argjson id "$TPL" '{template_id: $id, template_rev: "abcdef012345", enabled: true, confirm: true}')")")" = "forbidden" ] \ || 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)"]}')")")" = "forbidden" ] \ || 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