#!/usr/bin/env bash # End-to-end checks of Etappe 4b (assign presets to modules, overrides, mass assignment, detach, delete) through the # OAuth-protected endpoint against the real Divi. Runs INSIDE the DDEV web container: # cd /home/janniclas/Projekte/WordPress && timeout 900 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-presets-assign-smoke.sh # Runs only with the plugins postlos and asynclistry deactivated (Monolog 2 / deprecation output of the WP-CLI calls). # Creates presets named "LIT Smoke …" and two draft pages with one button each; the EXIT trap restores the # preset inventory taken before the first change and deletes the pages (with revisions) by exact ID. 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) RUN="$(date +%s)-$$" PREFIX="LIT Smoke $RUN" ORIGINAL_PRESETS=$(wp eval 'echo wp_json_encode(ET\Builder\Packages\GlobalData\GlobalPreset::get_data());' 2>/dev/null) PAGE1="" PAGE2="" NODE="s0.r0.c0.m0" CONTENT='' cleanup() { wp eval '$d = json_decode(getenv("LIT_PRESETS"), true); et_update_option(ET\Builder\Packages\GlobalData\GlobalPreset::option_name(), $d, false, "", "", true);' \ --user="$ADMIN_LOGIN" >/dev/null 2>&1 || echo "WARN: preset inventory not restored" for id in $PAGE1 $PAGE2; do wp post delete "$id" --force >/dev/null 2>&1 || true done # Change-log rows about the test pages. LIT_IDS="$PAGE1 $PAGE2" wp eval ' global $wpdb; $table = $wpdb->prefix . "lit_mcp_log"; foreach (array_filter(explode(" ", (string) getenv("LIT_IDS"))) as $id) { $wpdb->delete($table, ["object_type" => "page", "object_id" => $id]); }' >/dev/null 2>&1 || true cleanup_app_passwords_for "$ADMIN_LOGIN" } export LIT_PRESETS="$ORIGINAL_PRESETS" trap 'rc=$?; cleanup; guard_exit "$rc" || exit $?' EXIT connect "$ADMIN_LOGIN" create_page() { # $1 = title -> prints the ID of a draft Divi 5 page with one button without preset LIT_TITLE="$1" LIT_CONTENT="$CONTENT" wp --user="$ADMIN_LOGIN" eval ' $id = wp_insert_post(["post_type" => "page", "post_status" => "draft", "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;' 2>/dev/null } button_attrs() { # $1 = post ID -> attributes (JSON) of the first divi/button block as stored LIT_ID="$1" wp eval ' $find = static function (array $blocks) use (&$find) { foreach ($blocks as $b) { if ($b["blockName"] === "divi/button") { return $b["attrs"]; } $r = $find($b["innerBlocks"]); if ($r !== null) { return $r; } } return null; }; echo wp_json_encode($find(parse_blocks(get_post((int) getenv("LIT_ID"))->post_content)));' 2>/dev/null } revisions_of() { # $1 = post ID -> number of revisions LIT_ID="$1" wp eval 'echo count(wp_get_post_revisions((int) getenv("LIT_ID")));' 2>/dev/null } preset_stored() { # $1 = module|group, $2 = divi/, $3 = preset ID -> exit 0 when Divi stores it LIT_KIND="$1" LIT_SCOPE="$2" LIT_PID="$3" wp eval ' $d = ET\Builder\Packages\GlobalData\GlobalPreset::get_data(); exit(isset($d[getenv("LIT_KIND")][getenv("LIT_SCOPE")]["items"][getenv("LIT_PID")]) ? 0 : 1);' 2>/dev/null } preset_item() { # $1 = module preset ID -> the stored button preset item (JSON) LIT_PID="$1" wp eval ' $d = ET\Builder\Packages\GlobalData\GlobalPreset::get_data(); echo wp_json_encode($d["module"]["divi/button"]["items"][getenv("LIT_PID")] ?? null);' 2>/dev/null } nested_groups() { # $1 = module preset ID -> groupPresets (JSON) of that button preset LIT_PID="$1" wp eval ' $d = ET\Builder\Packages\GlobalData\GlobalPreset::get_data(); echo wp_json_encode($d["module"]["divi/button"]["items"][getenv("LIT_PID")]["groupPresets"] ?? new stdClass());' 2>/dev/null } timed() { # $1 = label; runs the rest of the arguments and prints the duration to stderr local label=$1 start end shift start=$(date +%s.%N) "$@" end=$(date +%s.%N) echo "time - $label: $(awk -v s="$start" -v e="$end" 'BEGIN { printf "%.2f", e - s }') s" >&2 } details() { # $1 = tool()/ability() result -> details object of an error, or the result itself echo "$1" | jq -c 'if has("__error") then ((.__error | fromjson?) // {}) else . end' } # Runs lit/divi-presets-set with dry run and confirm; sets RESULT. $1 = input JSON without confirm. presets_set() { local dry rev dry=$(ability lit/divi-presets-set "$1") [ "$(error_code "$dry")" = "confirm_required" ] || fail "presets-set dry run: $dry" rev=$(echo "$dry" | jq -r '.__error | fromjson | .design_rev') RESULT=$(ability lit/divi-presets-set "$(echo "$1" | jq -c --arg r "$rev" '. + {confirm: true, design_rev: $r}')") [ "$(echo "$RESULT" | jq -r '.ok')" = "true" ] || fail "presets-set confirm: $RESULT" } preset_id() { # $1 = module|group, $2 = short name ("button", "font"), $3 = preset name -> prints the ID local id id=$(ability lit/divi-preset-get "$(jq -cn --arg k "$1" --arg s "$2" --arg p "$3" '{($k): $s, preset: $p}')" | jq -r '.id // empty') [ -n "$id" ] || fail "preset $3 not found" echo "$id" } tree_rev() { # $1 = post ID tool lit-divi-outline "{\"post_id\":$1}" | jq -r '.tree_rev' } # 1. Two draft pages with one button without preset each. PAGE1=$(create_page "$PREFIX Seite 1") PAGE2=$(create_page "$PREFIX Seite 2") [[ $PAGE1 =~ ^[1-9][0-9]*$ && $PAGE2 =~ ^[1-9][0-9]*$ ]] || fail "pages not created" for id in "$PAGE1" "$PAGE2"; do button_attrs "$id" | jq -e '(.modulePreset // null) == null and (.groupPreset // null) == null' >/dev/null \ || fail "page $id starts with a preset" done pass "two draft pages with a button without preset ($PAGE1, $PAGE2)" # 2. Presets: two button module presets (A, B) and a font group preset (G); B nests G. presets_set "$(jq -cn --arg a "$PREFIX A" --arg b "$PREFIX B" --arg g "$PREFIX G" '{items: [ {module: "button", name: $a, attrs: {"button.decoration.background": {desktop: {color: "#123456"}}}}, {module: "button", name: $b, attrs: {"button.decoration.background": {desktop: {color: "#654321"}}}}, {group: "font", name: $g, source_module: "button", attrs: {"button.decoration.font.font": {desktop: {weight: "700"}}}}]}')" PRESET_A=$(preset_id module button "$PREFIX A") PRESET_B=$(preset_id module button "$PREFIX B") PRESET_G=$(preset_id group font "$PREFIX G") presets_set "$(jq -cn --arg b "$PREFIX B" --arg g "$PREFIX G" '{items: [ {module: "button", preset: $b, group_presets: {font: $g}}]}')" nested_groups "$PRESET_B" | jq -e --arg g "$PRESET_G" '."button.decoration.font".presetId == [$g]' >/dev/null \ || fail "preset B does not nest group preset G" pass "module presets A, B and group preset G created, B nests G" # 3. A stack and a group preset on page 1 through lit-divi-update. TREE=$(tree_rev "$PAGE1") UPDATE=$(tool lit-divi-update "$(jq -cn --argjson id "$PAGE1" --arg rev "$TREE" --arg n "$NODE" --arg a "$PREFIX A" \ --arg b "$PREFIX B" --arg g "$PREFIX G" '{post_id: $id, tree_rev: $rev, changes: [ {id: $n, preset: [$a, $b], group_presets: {font: $g}}]}')") echo "$UPDATE" | jq -e '.ok == true and (.tree_rev | length) > 0 and ((.warnings // []) | map(select(contains("überdecken"))) | length) == 0' \ >/dev/null || fail "update with stack and group preset: $UPDATE" button_attrs "$PAGE1" | jq -e --arg a "$PRESET_A" --arg b "$PRESET_B" --arg g "$PRESET_G" \ '.modulePreset == [$a, $b] and .groupPreset == {"button.decoration.font": {presetId: [$g], groupName: "divi/font"}}' >/dev/null \ || fail "stored modulePreset/groupPreset of page 1: $(button_attrs "$PAGE1")" OUTLINE=$(tool lit-divi-outline "{\"post_id\":$PAGE1}") echo "$OUTLINE" | jq -e --arg n "$NODE" --arg g "font: $PREFIX G" \ 'any(.nodes[]; .id == $n and ((.group_presets // []) | index($g) != null)) and (.outline | contains("Gruppen-Presets: " + $g))' \ >/dev/null || fail "outline lacks the group preset: $OUTLINE" pass "stack [A, B] and group preset stored; outline shows the group preset" # 4. Overrides: a differing local value is reported, clear_overrides removes it; a non-boolean value is refused. TREE=$(tree_rev "$PAGE2") UPDATE=$(tool lit-divi-update "$(jq -cn --argjson id "$PAGE2" --arg rev "$TREE" --arg n "$NODE" --arg a "$PREFIX A" \ '{post_id: $id, tree_rev: $rev, changes: [ {id: $n, preset: $a, attrs: {"button.decoration.background": {desktop: {color: "#abcdef"}}}}]}')") echo "$UPDATE" | jq -e --arg n "$NODE" --arg a "$PREFIX A" \ '.ok == true and ((.warnings // []) | map(select(contains($n) and contains("überdecken") and contains($a) and contains("clear_overrides"))) | length) == 1' \ >/dev/null || fail "no override warning: $UPDATE" button_attrs "$PAGE2" | jq -e --arg a "$PRESET_A" '.modulePreset == [$a] and .button.decoration.background != null' >/dev/null \ || fail "page 2 should keep its local background: $(button_attrs "$PAGE2")" TREE=$(echo "$UPDATE" | jq -r '.tree_rev') BAD=$(tool lit-divi-update "$(jq -cn --argjson id "$PAGE2" --arg rev "$TREE" --arg n "$NODE" \ '{post_id: $id, tree_rev: $rev, changes: [{id: $n, clear_overrides: "ja"}]}')") # The input schema (type boolean) refuses it before the tool runs, so the error carries no tool code. echo "$BAD" | jq -e 'has("__error") and (.__error | contains("clear_overrides"))' >/dev/null \ || fail "non-boolean clear_overrides: $BAD" CLEARED=$(tool lit-divi-update "$(jq -cn --argjson id "$PAGE2" --arg rev "$TREE" --arg n "$NODE" --arg a "$PREFIX A" \ '{post_id: $id, tree_rev: $rev, changes: [{id: $n, preset: $a, clear_overrides: true}]}')") echo "$CLEARED" | jq -e '.ok == true and ((.warnings // []) | map(select(contains("überdecken"))) | length) == 0' >/dev/null \ || fail "clear_overrides: $CLEARED" button_attrs "$PAGE2" | jq -e --arg a "$PRESET_A" '.modulePreset == [$a] and (.button.decoration.background // null) == null and .button.innerContent.desktop.value.text == "Los"' >/dev/null \ || fail "clear_overrides left or removed the wrong values: $(button_attrs "$PAGE2")" pass "override warning, clear_overrides removes exactly the covering value, non-boolean refused by the schema" # 5. Mass assignment over both pages: stack [B] and group preset G. ASSIGN=$(jq -cn --argjson p1 "$PAGE1" --argjson p2 "$PAGE2" --arg b "$PREFIX B" --arg g "$PREFIX G" \ '{module: "button", preset: $b, group_presets: {font: $g}, post_ids: [$p1, $p2]}') NONE=$(ability lit/divi-presets-assign "$(echo "$ASSIGN" | jq -c '. + {only_without_preset: true}')") details "$NONE" | jq -e '(.pages_count // 0) == 0' >/dev/null || fail "only_without_preset should match no page: $NONE" REVISIONS_BEFORE=$(revisions_of "$PAGE1") DRY=$(timed "assign dry run" ability lit/divi-presets-assign "$ASSIGN") [ "$(error_code "$DRY")" = "confirm_required" ] || fail "assign without confirm: $DRY" details "$DRY" | jq -e --argjson p1 "$PAGE1" --argjson p2 "$PAGE2" \ '.pages_count == 2 and .nodes_count == 2 and ([.pages[].post_id] | sort) == ([$p1, $p2] | sort) and ((.blocked_ids // []) | length) == 0' >/dev/null || fail "assign dry run: $DRY" button_attrs "$PAGE1" | jq -e --arg a "$PRESET_A" --arg b "$PRESET_B" '.modulePreset == [$a, $b]' >/dev/null \ || fail "dry run changed page 1" [ "$(revisions_of "$PAGE1")" = "$REVISIONS_BEFORE" ] || fail "dry run created a revision" ASSIGNED=$(timed "assign confirmed" ability lit/divi-presets-assign "$(echo "$ASSIGN" | jq -c '. + {confirm: true}')") echo "$ASSIGNED" | jq -e --argjson p1 "$PAGE1" --argjson p2 "$PAGE2" \ '.ok == true and (.saved | length) == 2 and .remaining == 0 and ([.saved[].post_id] | sort) == ([$p1, $p2] | sort)' \ >/dev/null || fail "assign with confirm: $ASSIGNED" for id in "$PAGE1" "$PAGE2"; do button_attrs "$id" | jq -e --arg b "$PRESET_B" --arg g "$PRESET_G" \ '.modulePreset == [$b] and .groupPreset == {"button.decoration.font": {presetId: [$g], groupName: "divi/font"}}' \ >/dev/null || fail "page $id after mass assignment: $(button_attrs "$id")" done REVISIONS_AFTER=$(revisions_of "$PAGE1") AGAIN=$(ability lit/divi-presets-assign "$ASSIGN") [ "$(error_code "$AGAIN")" != "confirm_required" ] || fail "second dry run still finds changes: $AGAIN" details "$AGAIN" | jq -e '(.pages_count // 0) == 0' >/dev/null || fail "second dry run lists pages: $AGAIN" [ "$(revisions_of "$PAGE1")" = "$REVISIONS_AFTER" ] || fail "second dry run created a revision" pass "mass assignment: dry run lists 2 pages, confirm saved 2 (remaining 0), second dry run finds nothing" # 6. detach of the group preset: it is in use, a stale design_rev is refused, then dry run, confirm, delete. GROUP_ITEM=$(jq -cn --arg g "$PRESET_G" '{items: [{group: "font", preset: $g}]}') [ "$(error_code "$(ability lit/divi-presets-delete "$GROUP_ITEM")")" = "in_use" ] || fail "delete of a used group preset" UNKNOWN_VARIABLE=$(ability lit/divi-design-detach '{"variable": "Irgendeine Variable"}') [ "$(error_code "$UNKNOWN_VARIABLE")" = "not_found" ] || fail "detach of an unknown variable: $UNKNOWN_VARIABLE" DETACH=$(jq -cn --arg g "$PRESET_G" '{preset: {group: "font", preset: $g}}') DRY=$(timed "detach dry run" ability lit/divi-design-detach "$DETACH") [ "$(error_code "$DRY")" = "confirm_required" ] || fail "detach without confirm: $DRY" DETACH_REV=$(details "$DRY" | jq -r '.design_rev') details "$DRY" | jq -e '.pages_count == 2 and (.diff | length) > 0' >/dev/null || fail "detach dry run: $DRY" STALE=$(ability lit/divi-design-detach "$(echo "$DETACH" | jq -c '. + {confirm: true, design_rev: "veraltet"}')") [ "$(error_code "$STALE")" = "rev_mismatch" ] || fail "stale design_rev: $STALE" nested_groups "$PRESET_B" | jq -e 'length > 0' >/dev/null || fail "stale detach changed preset B" button_attrs "$PAGE1" | jq -e '(.groupPreset // null) != null' >/dev/null || fail "stale detach changed page 1" pass "group preset is in use, unknown variable not found, stale design_rev refused without writing" echo "info - preset B before detach: $(preset_item "$PRESET_B")" >&2 DETACHED=$(timed "detach confirmed" ability lit/divi-design-detach "$(echo "$DETACH" | jq -c --arg r "$DETACH_REV" '. + {confirm: true, design_rev: $r}')") echo "$DETACHED" | jq -e '.ok == true and (.saved | length) == 2 and .remaining == 0' >/dev/null \ || fail "detach with confirm: $DETACHED" nested_groups "$PRESET_B" | jq -e 'length == 0' >/dev/null || fail "preset B still nests the group preset" echo "info - preset B after withoutNestedRef: $(preset_item "$PRESET_B")" >&2 for id in "$PAGE1" "$PAGE2"; do button_attrs "$id" | jq -e --arg b "$PRESET_B" '.modulePreset == [$b] and (.groupPreset // null) == null' >/dev/null \ || fail "page $id after detach: $(button_attrs "$id")" done pass "detach removed the nested reference (snapshot pipeline) and the page references" DRY=$(ability lit/divi-presets-delete "$GROUP_ITEM") [ "$(error_code "$DRY")" = "confirm_required" ] || fail "delete dry run after detach: $DRY" DELETE_REV=$(details "$DRY" | jq -r '.design_rev') DELETED=$(ability lit/divi-presets-delete "$(echo "$GROUP_ITEM" | jq -c --arg r "$DELETE_REV" '. + {confirm: true, design_rev: $r}')") echo "$DELETED" | jq -e '.ok == true' >/dev/null || fail "delete after detach: $DELETED" if preset_stored group divi/font "$PRESET_G"; then fail "group preset $PRESET_G is still stored"; fi pass "group preset deletable after detach" finish