feat: Divi hints point to the read tools; guides divi-structure/attrs/design-system; test: Divi read end-to-end

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 12:41:42 +02:00
co-authored by Claude Opus 5.5
parent b3cd0eee62
commit f433fef8f7
15 changed files with 326 additions and 19 deletions
+4 -3
View File
@@ -23,8 +23,8 @@ Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities,
Trefferliste. Geliefert werden nur Einträge, die der verbundene Nutzer selbst bearbeiten darf (fremde Trefferliste. Geliefert werden nur Einträge, die der verbundene Nutzer selbst bearbeiten darf (fremde
Entwürfe/private Inhalte ohne die passende Berechtigung erscheinen nicht). Entwürfe/private Inhalte ohne die passende Berechtigung erscheinen nicht).
- `lit-content-get`: alle Daten eines Eintrags inkl. Feldern, Begriffen, Beitragsbild und `rev`. - `lit-content-get`: alle Daten eines Eintrags inkl. Feldern, Begriffen, Beitragsbild und `rev`.
Bei Divi-Seiten fehlt `content`; Divi-Werkzeuge folgen in einer späteren Version, bis dahin werden Bei Divi-Seiten fehlt `content`; den Aufbau liest `lit-divi-outline` (siehe `divi-structure`), geändert
Divi-Inhalte im Divi-Builder bearbeitet (Titel, Auszug, Felder und Begriffe gehen weiter über wird er vorerst im Divi-Builder (Titel, Auszug, Felder und Begriffe gehen weiter über
`lit-content-update`). `lit-content-update`).
## Anlegen und Ändern ## Anlegen und Ändern
@@ -59,7 +59,8 @@ Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities,
## Sicher überarbeiten ## Sicher überarbeiten
1. `lit/post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`. 1. `lit/post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`.
2. Kopie mit `lit-content-update` (Divi-Inhalte bis zu den Divi-Werkzeugen im Divi-Builder) bearbeiten und dem Nutzer die Vorschau zeigen. 2. Kopie mit `lit-content-update` (Divi-Inhalte vorerst im Divi-Builder) bearbeiten und dem Nutzer die
Vorschau zeigen; `lit-page-render-summary` fasst den gerenderten Inhalt zusammen.
3. `lit/post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben). 3. `lit/post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben).
Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus
`lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am `lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am
+32
View File
@@ -0,0 +1,32 @@
# Divi-Einstellungen (Vollform)
Jedes Divi-5-Modul speichert seine Einstellungen unter **Attributpfaden**, z. B.
- `title.innerContent` – Text der Überschrift,
- `title.decoration.font.font` – Schrift der Überschrift (Farbe, Größe, Ebene `headingLevel`),
- `button.innerContent` – Beschriftung und Link eines Buttons (`text`, `linkUrl`, `linkTarget`),
- `module.decoration.background` – Hintergrund, `module.decoration.spacing` – Innen-/Außenabstand,
- `module.meta.adminLabel` – Admin-Label im Divi-Builder.
Der Wert eines Pfads ist eine Zuordnung **Breakpoint → Zustand → Wert**:
```json
{ "desktop": { "value": { "color": "#1a1a1a", "headingLevel": "h2" }, "hover": { "color": "#000000" } },
"phone": { "value": { "size": "28px" } } }
```
Breakpoints: `desktop`, `tablet`, `phone` (je nach Website auch `tabletWide`, `phoneWide`, `widescreen`,
`ultraWide`); Zustände: `value`, `hover`, `sticky`.
## Pfade finden
- `lit-divi-catalog` mit `module` (z. B. `"heading"`) listet alle gültigen Pfade dieses Moduls auf dieser
Website (live aus Divi) und die Kurzform-Felder mit ihrem Pfad. `path_kinds` nennt die Feldart eines Pfads
(`text`, `richtext`, `code`, `url`, `image`, `color`, `enum`, `onoff`, `number`, `list`, `icon`), soweit aus
den Divi-Metadaten ableitbar; zusammengesetzte Pfade (mehrere mögliche Feldarten je nach Unterschlüssel)
gelten als unbekannt, statt eine Feldart zu raten.
- `lit-divi-node-get` zeigt die gesetzten Werte eines vorhandenen Knotens in genau dieser Form (`attrs`).
- Schrift-Gruppen liegen eine Ebene tiefer: `…decoration.font.font`, `…decoration.bodyFont.body.font`,
`…decoration.headingFont.h2.font`, `module.advanced.text.text`.
- Werte wie `$variable({…})$` sind Verweise auf globale Farben, Variablen oder dynamische Inhalte – nicht
durch feste Werte ersetzen (siehe `divi-design-system`).
+20
View File
@@ -0,0 +1,20 @@
# Divi-Designsystem
`lit-divi-design-system` liest das Designsystem der Website live aus Divi:
- **Globale Farben** (`colors`): ID (`gcid-…`), Name, Wert und der Platzhalter für neue Inhalte, z. B.
`{{color:primary}}` für die Primärfarbe oder `{{color:Nachtblau}}` für eine eigene globale Farbe. Die fünf
Theme-Farben heißen `primary`, `secondary`, `heading`, `body` und `link`.
- **Globale Variablen** (`variables`): Zahlen, Texte, Bilder, Links (`gvid-…`), Platzhalter `{{var:<Name>}}`.
- **Theme-Schriften** (`fonts`): Überschrift und Fließtext, Platzhalter `{{font:heading}}` / `{{font:body}}`.
- **Größen** (`sizes`): Schriftgrößen, Inhaltsbreite und Spaltenabstand aus den Theme-Optionen.
- **Presets** (`presets`): Modul-Presets je Modul (`kind: module`, z. B. alle Button-Presets) und
Gruppen-Presets (`kind: group`); `default: true` markiert das Standard-Preset eines Moduls.
Regeln:
1. Vor dem Gestalten einer Seite das Designsystem lesen.
2. Globale Farben, Variablen und Presets vor festen Werten verwenden; feste Farben oder Schriften nur, wenn
der Nutzer es ausdrücklich möchte.
3. Gibt es für einen Zweck noch keine Farbe, Variable oder kein Preset, dem Nutzer vorschlagen, es im
Divi-Builder anzulegen, statt feste Werte zu verteilen.
+37
View File
@@ -0,0 +1,37 @@
# Divi-Seiten: Aufbau lesen
Divi 5 speichert eine Seite als Block-Markup: Abschnitte (`section`) enthalten Zeilen (`row`), Zeilen
Spalten (`column`), Spalten Module (`text`, `heading`, `button`, …). Manche Module haben Kind-Module
(`accordion` → `accordion-item`). Die Divi-Werkzeuge gibt es nur, wenn der Divi-5-Builder aktiv ist.
## Seitenbaum
- `lit-divi-outline` mit `post_id` liefert jeden Knoten mit **Pfad-ID**, Modultyp, Admin-Label, Textauszug,
Layout-Info (z. B. `Spalten 8_24+16_24`) und Preset-Namen – als kompakten Text (`outline`) und als Liste
(`nodes`). Mit `id` nur ein Teilbaum, mit `depth` begrenzte Tiefe.
- Pfad-IDs: `s0` = erster Abschnitt, `s0.r1` = zweite Zeile darin, `s0.r1.c0` = erste Spalte,
`s0.r1.c0.m2` = drittes Modul, `…m2.m0` = erstes Kind-Modul. `g` = globales Modul, `x` = fremder Block
(z. B. aus dem Block-Editor). Gezählt wird je Buchstabe innerhalb des Elternknotens.
- `lit-divi-node-get` mit `post_id` und `id` liefert alle Einstellungen eines Knotens (siehe `divi-attrs`),
die aktuellen Kurzform-Werte (`shortform`) und die IDs der Kinder.
## Stand und Sperre
- `tree_rev` ist der Stand des Seiteninhalts. Pfad-IDs gelten nur für diesen Stand – ändert jemand die Seite
(auch im Divi-Builder), verschieben sich die IDs. Divi-Änderungen verlangen deshalb immer die `tree_rev`
des letzten Lesevorgangs.
- `rev` ist davon getrennt: der Stand des ganzen Eintrags (Titel, Auszug, Status …) für `lit-content-update`.
## Was nicht bearbeitet wird
- Knoten mit „nur lesbar“ (globale Module `g…`, fremde Blöcke `x…`) ändern die Divi-Werkzeuge nicht.
- Divi-4-Inhalt (Shortcodes `[et_pb_…]`) wird nicht gelesen oder geändert: Seite einmal im Divi-Builder
öffnen und speichern, Divi 5 wandelt sie dabei um.
- Ist das Markup beschädigt, lehnen die Werkzeuge ab (`unsupported`); die Seite im Divi-Builder öffnen und
speichern.
## Ergebnis prüfen
`lit-page-render-summary` rendert den Inhaltsbereich wie im Frontend und listet Überschriften, Texte, Links
und Bilder samt Warnungen (Sprünge in der Überschriftenhierarchie, fehlende Alt-Texte, Links ohne Ziel).
Header, Footer und Theme-Builder-Vorlagen sind nicht enthalten.
+4 -1
View File
@@ -33,7 +33,10 @@ Status (`lit/post-unpublish`, `lit/post-schedule`, `lit/post-trash`), Kopien (`l
Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`, Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`,
`lit/media-update`, `lit/media-delete`). `lit/media-update`, `lit/media-delete`).
Weitere Themen: `lit-guide` mit `topic` = `content-types`, `workflows` oder `media`. Die `lit-divi-…`-Werkzeuge gibt es nur, wenn der Divi-5-Builder aktiv ist.
Weitere Themen: `lit-guide` mit `topic` = `content-types`, `workflows`, `media`, `divi-structure`,
`divi-attrs` oder `divi-design-system`.
## Rückmeldungen ## Rückmeldungen
@@ -0,0 +1,64 @@
# Etappe 2a – Manuelle Abnahme (Divi lesen)
Vorbereitet von Task 8 (Hinweise/Guides/Serveranweisungen/Ende-zu-Ende-Test). Automatisiert geprüft ist
bereits: alle Unit-Tests (`composer test`, 692 Tests), Lint (`composer lint`) und alle vier
Integrationsskripte (`tests/Integration/divi-read-smoke.sh`, `tests/Integration/mcp-smoke.sh`,
`tests/Integration/wp-basis-smoke.sh`, `tests/Integration/media-admin-smoke.sh`) – Details im Report von
Task 8 (`.superpowers/sdd/2026-09-26-etappe-2a-divi-lesen/task-8-report.md`).
Die folgenden Punkte lassen sich nicht automatisiert prüfen (echter Divi Visual Builder,
claude.ai-Connector); das führt Jan-Niclas selbst durch.
## 1. Builder-Fixtures exportieren (Roundtrip mit echten Seiten)
Alle bisherigen Divi-Fixtures sind entweder handgeschrieben (`synthetic-*.html`), von Divis eigener
Shortcode→Block-Konvertierung erzeugt (`converted-*.html`) oder reine Modul-Metadaten
(`metadata-sample.json`). Es fehlt noch mindestens eine echte, im Visual Builder gebaute Seite als
Gegenprobe für Parser/Serializer.
- [ ] 2–3 Seiten im Divi Visual Builder bauen. Zusammen sollten sie enthalten:
- ein **globales Modul** (aus der Divi-Bibliothek eingefügt),
- ein **Modul-Preset**,
- eine **globale Farbe** und eine **globale Variable**,
- **Dynamic Content** (z. B. Beitragstitel, ein Meta-Feld),
- einen **Spezialabschnitt** (Specialty Section) und eine **verschachtelte Zeile**.
- Keine personenbezogenen Daten verwenden – die Fixtures werden eingecheckt.
- [ ] Je Seite exportieren und den Roundtrip prüfen:
```bash
cd /home/janniclas/Projekte/WordPress
timeout 300 ddev exec wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-page.php <post_id> <name>
cd wp-content/plugins/WP-LIT-MCP && composer test
```
Erwartet: `Success: builder-<name>.html: <N> bytes, <N> blocks, globalModule: ja/nein, tree_rev <hash>.`,
danach `composer test` weiterhin grün (der Test läuft automatisch über alle `builder-*.html`-Dateien mit).
- [ ] Schlägt der Roundtrip fehl (Parser/Serializer erzeugen nicht mehr das byte-identische Markup): das
ist ein **Fehler im Plugin** (`Parser`/`Serializer`), nie ein Grund, die Fixture anzupassen. Als
Folgeaufgabe melden statt selbst zu reparieren.
- [ ] Fixtures ohne personenbezogene Daten einchecken:
```bash
git add tests/Fixtures/divi/builder-*.html
git -c user.name="Jan-Niclas Loosen" -c user.email="janniclas@loosen-it.de" commit -m "test: Divi builder fixtures"
```
## 2. claude.ai: Divi-Lese-Werkzeuge im Gespräch
Mit dem claude.ai-Connector gegen eine Website mit aktivem Divi-5-Theme (z. B. dieses DDEV-Setup oder
Staging) verbinden und prüfen, dass die Antworten mit dem tatsächlichen Aufbau im Divi-Builder
übereinstimmen:
- [ ] „Zeig mir den Aufbau der Startseite“ → sollte `lit-divi-outline` aufrufen und einen Seitenbaum mit
Pfad-IDs, Modultypen und Textauszügen liefern, der zum Divi-Builder passt.
- [ ] „Welche Farben und Presets hat die Website?“ → sollte `lit-divi-design-system` aufrufen und die
globalen Farben/Presets nennen, die auch im Divi-Builder (Designsystem-Bereich) zu sehen sind.
- [ ] „Was sieht ein Besucher auf der Seite?“ → sollte `lit-page-render-summary` aufrufen und
Überschriften/Texte/Links/Bilder liefern, die dem Frontend entsprechen.
- [ ] Bei einer der zuvor im Visual Builder gebauten Seiten (Schritt 1): stichprobenartig prüfen, dass
globales Modul, Preset, Dynamic Content und Spezialabschnitt im Outline/Node-Get sinnvoll
wiedergegeben werden (globales Modul und Spezialabschnitt ggf. als „nur lesbar“ markiert – das ist
in dieser Etappe so vorgesehen, siehe `divi-structure`-Guide).
## Ergebnis
Kurze Rückmeldung an den Ausführenden (Task 8 / Controller): welche Punkte grün sind, welche
Builder-Fixtures committet wurden, und was ggf. als Folgeaufgabe (Bugfix im Parser/Serializer oder
Doku-Korrektur) aufgenommen werden muss.
+6 -1
View File
@@ -52,6 +52,11 @@ final class InstructionsBuilder
. ' (Felder und Taxonomien: lit/content-types, direkt: lit-content-types).'; . ' (Felder und Taxonomien: lit/content-types, direkt: lit-content-types).';
} }
$divi = str_contains(', ' . $active . ',', ', divi,')
? ['- Divi-Seiten: vor Änderungen lit-divi-outline und lit-divi-design-system lesen; Aufbau: lit-guide '
. 'topic "divi-structure".']
: [];
return implode("\n", array_merge($lines, [ return implode("\n", array_merge($lines, [
'Regeln:', 'Regeln:',
'- Vor dem ersten Arbeitsschritt lit-guide (ohne Topic) aufrufen; Details je Thema über lit-guide ' '- Vor dem ersten Arbeitsschritt lit-guide (ohne Topic) aufrufen; Details je Thema über lit-guide '
@@ -63,6 +68,6 @@ final class InstructionsBuilder
'- Beim Bearbeiten immer den rev-Wert aus dem letzten Lesevorgang mitgeben (Pflicht).', '- Beim Bearbeiten immer den rev-Wert aus dem letzten Lesevorgang mitgeben (Pflicht).',
'- Löschen und Ersetzen erst nach Rückfrage beim Nutzer (confirm: true).', '- Löschen und Ersetzen erst nach Rückfrage beim Nutzer (confirm: true).',
'- Fehler enthalten einen hint – ihm folgen, statt blind zu wiederholen.', '- Fehler enthalten einen hint – ihm folgen, statt blind zu wiederholen.',
])); ], $divi));
} }
} }
@@ -121,7 +121,7 @@ final class ContentUpdateAbility implements Ability
if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) { if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) {
return Errors::unsupported( return Errors::unsupported(
'Divi-Inhalte können nicht als Rohtext überschrieben werden; nichts wurde gespeichert.', 'Divi-Inhalte können nicht als Rohtext überschrieben werden; nichts wurde gespeichert.',
PostPresenter::DIVI_NOTE . ' Titel, Auszug, Felder und Begriffe hier ohne content ändern.' PostPresenter::diviNote() . ' Titel, Auszug, Felder und Begriffe hier ohne content ändern.'
); );
} }
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\WordPress\Content;
use LIT\MCP\Abilities\JsonMap; use LIT\MCP\Abilities\JsonMap;
use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\Rev;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition; use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use WP_Post; use WP_Post;
@@ -13,14 +14,36 @@ use WP_Post;
/** /**
* Output format of an entry for the content tools. Not final so abilities can be * Output format of an entry for the content tools. Not final so abilities can be
* unit-tested with a mock. * unit-tested with a mock.
*
* Layering exception: diviNote()/diviHint() call DiviModules::isAvailable() (Divi
* integration) to decide whether the Divi tools exist – the WordPress integration
* otherwise never depends on Divi. Kept because the alternative (injecting
* availability through every constructor that reaches PostPresenter) would spread the
* same one-line check across content-get, content-update and revisions-list for no
* behavioral gain; $toolsAvailable can still be passed in explicitly for tests.
*/ */
class PostPresenter class PostPresenter
{ {
public const DIVI_NOTE = 'Divi-Werkzeuge folgen in einer späteren Version; bis dahin im Divi-Builder bearbeiten.'; /** How Divi content is read and changed with the Divi tools. */
public const DIVI_NOTE = 'Den Aufbau mit lit/divi-outline (direkt: lit-divi-outline) lesen, einzelne Knoten mit '
. 'lit/divi-node-get (direkt: lit-divi-node-get); ändern vorerst im Divi-Builder.';
public const DIVI_HINT = 'Dieser Inhalt ist mit Divi gebaut; content wird deshalb nicht ausgegeben. ' /** Divi content on a site where the Divi 5 builder is not loaded (no Divi tools registered). */
. self::DIVI_NOTE . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update ' public const DIVI_INACTIVE_NOTE = 'Der Divi-5-Builder ist auf dieser Website nicht aktiv, daher gibt es keine '
. '(direkt: lit-content-update) änderbar.'; . 'Divi-Werkzeuge; Divi-Inhalte nur im Divi-Builder bearbeiten.';
/** $toolsAvailable defaults to whether the Divi integration is available (Divi 5 loaded). */
public static function diviNote(?bool $toolsAvailable = null): string
{
return ($toolsAvailable ?? DiviModules::isAvailable()) ? self::DIVI_NOTE : self::DIVI_INACTIVE_NOTE;
}
public static function diviHint(?bool $toolsAvailable = null): string
{
return 'Dieser Inhalt ist mit Divi gebaut; content wird deshalb nicht ausgegeben. '
. self::diviNote($toolsAvailable) . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update '
. '(direkt: lit-content-update) änderbar.';
}
public function __construct(private ContentSchema $schema) public function __construct(private ContentSchema $schema)
{ {
@@ -63,7 +86,7 @@ class PostPresenter
'fields' => JsonMap::of($this->fields($post)), 'fields' => JsonMap::of($this->fields($post)),
'featured_image' => $this->featuredImage($post), 'featured_image' => $this->featuredImage($post),
'rev' => Rev::ofPost($post), 'rev' => Rev::ofPost($post),
'hints' => $divi ? [self::DIVI_HINT] : [], 'hints' => $divi ? [self::diviHint()] : [],
]); ]);
} }
@@ -19,7 +19,7 @@ use WP_Post;
* the same detection lit/content-get uses) because a Divi/block-built revision's * the same detection lit/content-get uses) because a Divi/block-built revision's
* post_content is shortcode or block-comment markup, not prose – excerpt is then a * post_content is shortcode or block-comment markup, not prose – excerpt is then a
* stripped fragment of that markup rather than a real preview; hints repeats * stripped fragment of that markup rather than a real preview; hints repeats
* PostPresenter::DIVI_HINT once when any item is flagged. * PostPresenter::diviHint() once when any item is flagged.
*/ */
final class RevisionsListAbility implements Ability final class RevisionsListAbility implements Ability
{ {
@@ -124,7 +124,7 @@ final class RevisionsListAbility implements Ability
'post_id' => (int) $post->ID, 'post_id' => (int) $post->ID,
'items' => $items, 'items' => $items,
'next_cursor' => $page['next_cursor'], 'next_cursor' => $page['next_cursor'],
'hints' => in_array(true, array_column($items, 'divi'), true) ? [PostPresenter::DIVI_HINT] : [], 'hints' => in_array(true, array_column($items, 'divi'), true) ? [PostPresenter::diviHint()] : [],
]; ];
} }
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# End-to-end checks of the Etappe-2a Divi read tools through the OAuth-protected endpoint.
# Runs INSIDE the DDEV web container (Divi 5 must be the active theme):
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-read-smoke.sh
# Creates draft pages from tests/Fixtures/divi and deletes them again.
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)
FIXTURES=$PLUGIN/tests/Fixtures/divi
CREATED_POSTS=()
cleanup() {
for id in "${CREATED_POSTS[@]}"; do
wp post delete "$id" --force >/dev/null 2>&1 || true
done
cleanup_app_passwords_for "$ADMIN_LOGIN"
}
trap cleanup EXIT
# Creates a draft page from a fixture file as the admin (no kses re-serialization) and prints its ID.
create_page() { # $1 = fixture file, $2 = title
LIT_FILE="$FIXTURES/$1" LIT_TITLE="$2" 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) file_get_contents(getenv("LIT_FILE")))]);
update_post_meta($id, "_et_pb_use_builder", "on");
update_post_meta($id, "_et_pb_use_divi_5", "on");
echo $id;'
}
content_hash() { # $1 = post ID -> tree_rev of the stored content
wp eval "echo substr(hash('sha256', (string) get_post($1)->post_content), 0, 12);"
}
[ "$(wp eval 'echo get_template();')" = "Divi" ] || fail "Divi must be the active theme"
wp eval-file "$PLUGIN/tests/Integration/divi-shortform-check.php" >/dev/null || fail "short form does not match the Divi catalog"
pass "short form matches the live Divi catalog"
PAGE_ID=$(create_page converted-landing.html "LIT Divi Lesen")
CREATED_POSTS+=("$PAGE_ID")
D4_ID=$(create_page divi4-shortcodes.html "LIT Divi 4")
CREATED_POSTS+=("$D4_ID")
BEFORE=$(content_hash "$PAGE_ID")
connect "$ADMIN_LOGIN"
# 1. Direct read tools with readOnlyHint
LIST=$(rpc tools/list '{}')
for t in lit-divi-outline lit-divi-node-get lit-divi-catalog lit-divi-design-system lit-page-render-summary; do
echo "$LIST" | jq -e --arg t "$t" '.result.tools[] | select(.name == $t) | .annotations.readOnlyHint == true' >/dev/null \
|| fail "tools/list lacks $t with readOnlyHint"
done
pass "tools/list: Divi read tools are direct and read-only"
# 2. Outline with tree_rev = hash of post_content (ruling P16) and path IDs
OUTLINE=$(tool lit-divi-outline "{\"post_id\":$PAGE_ID}")
[ "$(echo "$OUTLINE" | jq -r '.tree_rev')" = "$BEFORE" ] || fail "tree_rev is not the hash of post_content: $OUTLINE"
echo "$OUTLINE" | jq -e '.rev != .tree_rev' >/dev/null || fail "rev and tree_rev must differ: $OUTLINE"
echo "$OUTLINE" | jq -e '.nodes[] | select(.id == "s1.r1.c0.m2.m0" and .type == "accordion-item")' >/dev/null \
|| fail "outline lacks the accordion item: $OUTLINE"
echo "$OUTLINE" | jq -r '.outline' | head -1 | grep -q '^s0 section "Hero"' || fail "outline text: $OUTLINE"
pass "lit-divi-outline: path IDs, tree_rev, rev"
# 3. Node in full form plus short-form values
NODE=$(tool lit-divi-node-get "{\"post_id\":$PAGE_ID,\"id\":\"s0.r0.c0.m0\"}")
echo "$NODE" | jq -e '.type == "divi/heading" and .shortform.text == "Handwerk mit Herz"' >/dev/null || fail "node-get: $NODE"
echo "$NODE" | jq -e '.attrs["title.innerContent"].desktop.value == "Handwerk mit Herz"' >/dev/null || fail "node-get attrs: $NODE"
MISSING=$(tool lit-divi-node-get "{\"post_id\":$PAGE_ID,\"id\":\"s9\"}")
[ "$(error_code "$MISSING")" = "not_found" ] || fail "unknown node: $MISSING"
pass "lit-divi-node-get: full form, short form, unknown node"
# 4. Catalog and design system live from Divi
CATALOG=$(tool lit-divi-catalog '{}')
echo "$CATALOG" | jq -e '(.modules | length) >= 90' >/dev/null || fail "catalog too small: $CATALOG"
BUTTON=$(tool lit-divi-catalog '{"module":"button"}')
echo "$BUTTON" | jq -e '.module.paths | index("button.innerContent") != null' >/dev/null || fail "catalog button: $BUTTON"
DESIGN=$(tool lit-divi-design-system '{}')
echo "$DESIGN" | jq -e '.colors[] | select(.id == "gcid-primary-color" and .placeholder == "{{color:primary}}")' >/dev/null \
|| fail "design system: $DESIGN"
pass "lit-divi-catalog and lit-divi-design-system read live data"
# 5. Rendered summary
SUMMARY=$(tool lit-page-render-summary "{\"post_id\":$PAGE_ID}")
echo "$SUMMARY" | jq -e '.headings[0].text == "Handwerk mit Herz"' >/dev/null || fail "render summary: $SUMMARY"
echo "$SUMMARY" | jq -e '[.texts[] | select(contains("Seit 1987"))] | length == 1' >/dev/null || fail "render texts: $SUMMARY"
pass "lit-page-render-summary renders Divi modules"
# 6. Hints and refusals
GET=$(tool lit-content-get "{\"post_id\":$PAGE_ID}")
echo "$GET" | jq -e '.content == null and (.hints[0] | contains("lit-divi-outline"))' >/dev/null || fail "content-get hint: $GET"
D4=$(tool lit-divi-outline "{\"post_id\":$D4_ID}")
[ "$(error_code "$D4")" = "unsupported" ] || fail "Divi 4 page must be unsupported: $D4"
pass "content-get points to the Divi tools; Divi 4 content is refused"
# 7. Reading changed nothing
[ "$(content_hash "$PAGE_ID")" = "$BEFORE" ] || fail "reading changed post_content"
pass "post_content unchanged after reading"
echo "ALL CHECKS PASSED"
@@ -45,6 +45,22 @@ final class InstructionsBuilderTest extends TestCase
self::assertLessThan(2000, strlen($text), 'Instructions must stay short – they are loaded in every conversation.'); self::assertLessThan(2000, strlen($text), 'Instructions must stay short – they are loaded in every conversation.');
} }
public function testAddsTheDiviRuleOnlyWhenDiviIsActive(): void
{
$divi = Mockery::mock(Integration::class);
$divi->allows('id')->andReturn('divi');
$divi->allows('isAvailable')->andReturn(true);
$divi->allows('abilities')->andReturn([$this->ability('lit/divi-outline', true)]);
$core = $this->coreWith($this->ability('lit/guide', true));
$with = (new InstructionsBuilder(new IntegrationRegistry($core, $divi)))->build();
$without = (new InstructionsBuilder(new IntegrationRegistry($core)))->build();
self::assertStringContainsString('lit-divi-outline und lit-divi-design-system lesen', $with);
self::assertStringNotContainsString('lit-divi-outline und', $without);
self::assertLessThan(2000, strlen($with));
}
public function testListsPostTypesWhenAProviderIsGiven(): void public function testListsPostTypesWhenAProviderIsGiven(): void
{ {
$core = $this->coreWith($this->ability('lit/guide', true)); $core = $this->coreWith($this->ability('lit/guide', true));
@@ -10,6 +10,7 @@ use LIT\MCP\Abilities\Rev;
use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility; use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility;
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Content\PostWriter;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Tests\Unit\TestCase; use LIT\MCP\Tests\Unit\TestCase;
@@ -116,9 +117,7 @@ final class ContentUpdateAbilityTest extends TestCase
->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']); ->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']);
self::assertSame('lit_unsupported', $result->get_error_code()); self::assertSame('lit_unsupported', $result->get_error_code());
self::assertStringNotContainsString('lit-divi', $result->get_error_message()); self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, $result->get_error_data()['hint']);
self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version; bis dahin im Divi-Builder '
. 'bearbeiten.', $result->get_error_data()['hint']);
} }
public function testNothingToChangeIsAValidationError(): void public function testNothingToChangeIsAValidationError(): void
@@ -87,13 +87,20 @@ final class PostPresenterTest extends TestCase
self::assertTrue($full['divi']); self::assertTrue($full['divi']);
self::assertNull($full['content']); self::assertNull($full['content']);
self::assertNull($full['featured_image']); self::assertNull($full['featured_image']);
self::assertStringNotContainsString('lit-divi', $full['hints'][0]); self::assertSame([PostPresenter::diviHint(false)], $full['hints']);
self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version', $full['hints'][0]);
self::assertInstanceOf(stdClass::class, $full['terms']); self::assertInstanceOf(stdClass::class, $full['terms']);
self::assertInstanceOf(stdClass::class, $full['fields']); self::assertInstanceOf(stdClass::class, $full['fields']);
self::assertSame(Rev::ofPost($post), $full['rev']); self::assertSame(Rev::ofPost($post), $full['rev']);
} }
public function testDiviHintNamesTheDiviToolsOnlyWhenDiviIsActive(): void
{
self::assertStringContainsString('lit-divi-outline', PostPresenter::diviHint(true));
self::assertStringContainsString('lit-content-update', PostPresenter::diviHint(true));
self::assertStringNotContainsString('lit-divi', PostPresenter::diviHint(false));
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, PostPresenter::diviHint());
}
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
{ {
$this->stubLinks(); $this->stubLinks();
@@ -66,7 +66,7 @@ final class RevisionsListAbilityTest extends TestCase
self::assertTrue($result['items'][0]['divi']); self::assertTrue($result['items'][0]['divi']);
self::assertFalse($result['items'][1]['divi']); self::assertFalse($result['items'][1]['divi']);
self::assertSame([PostPresenter::DIVI_HINT], $result['hints']); self::assertSame([PostPresenter::diviHint()], $result['hints']);
} }
public function testRevisionsOfATrashedPostCanStillBeListed(): void public function testRevisionsOfATrashedPostCanStillBeListed(): void