diff --git a/docs/guides/content-types.md b/docs/guides/content-types.md index cd93d80..213800c 100644 --- a/docs/guides/content-types.md +++ b/docs/guides/content-types.md @@ -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 Entwürfe/private Inhalte ohne die passende Berechtigung erscheinen nicht). - `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 - Divi-Inhalte im Divi-Builder bearbeitet (Titel, Auszug, Felder und Begriffe gehen weiter über + Bei Divi-Seiten fehlt `content`; den Aufbau liest `lit-divi-outline` (siehe `divi-structure`), geändert + wird er vorerst im Divi-Builder (Titel, Auszug, Felder und Begriffe gehen weiter über `lit-content-update`). ## Anlegen und Ändern @@ -59,7 +59,8 @@ Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities, ## Sicher überarbeiten 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). 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 diff --git a/docs/guides/divi-attrs.md b/docs/guides/divi-attrs.md new file mode 100644 index 0000000..e4e5a8f --- /dev/null +++ b/docs/guides/divi-attrs.md @@ -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`). diff --git a/docs/guides/divi-design-system.md b/docs/guides/divi-design-system.md new file mode 100644 index 0000000..ab092c9 --- /dev/null +++ b/docs/guides/divi-design-system.md @@ -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:}}`. +- **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. diff --git a/docs/guides/divi-structure.md b/docs/guides/divi-structure.md new file mode 100644 index 0000000..51e74f0 --- /dev/null +++ b/docs/guides/divi-structure.md @@ -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. diff --git a/docs/guides/overview.md b/docs/guides/overview.md index b7dfe5d..f453861 100644 --- a/docs/guides/overview.md +++ b/docs/guides/overview.md @@ -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`, `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 diff --git a/docs/superpowers/notes/2026-09-26-etappe-2a-abnahme.md b/docs/superpowers/notes/2026-09-26-etappe-2a-abnahme.md new file mode 100644 index 0000000..6049b32 --- /dev/null +++ b/docs/superpowers/notes/2026-09-26-etappe-2a-abnahme.md @@ -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 + cd wp-content/plugins/WP-LIT-MCP && composer test + ``` + Erwartet: `Success: builder-.html: bytes, blocks, globalModule: ja/nein, tree_rev .`, + 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. diff --git a/src/Guides/InstructionsBuilder.php b/src/Guides/InstructionsBuilder.php index 81f830f..073fa14 100644 --- a/src/Guides/InstructionsBuilder.php +++ b/src/Guides/InstructionsBuilder.php @@ -52,6 +52,11 @@ final class InstructionsBuilder . ' (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, [ 'Regeln:', '- 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).', '- Löschen und Ersetzen erst nach Rückfrage beim Nutzer (confirm: true).', '- Fehler enthalten einen hint – ihm folgen, statt blind zu wiederholen.', - ])); + ], $divi)); } } diff --git a/src/Integrations/WordPress/Content/ContentUpdateAbility.php b/src/Integrations/WordPress/Content/ContentUpdateAbility.php index 687401e..2c6dc7f 100644 --- a/src/Integrations/WordPress/Content/ContentUpdateAbility.php +++ b/src/Integrations/WordPress/Content/ContentUpdateAbility.php @@ -121,7 +121,7 @@ final class ContentUpdateAbility implements Ability if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) { return Errors::unsupported( '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.' ); } diff --git a/src/Integrations/WordPress/Content/PostPresenter.php b/src/Integrations/WordPress/Content/PostPresenter.php index ea8f3f1..f26c812 100644 --- a/src/Integrations/WordPress/Content/PostPresenter.php +++ b/src/Integrations/WordPress/Content/PostPresenter.php @@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\WordPress\Content; use LIT\MCP\Abilities\JsonMap; 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\FieldDefinition; 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 * 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 { - 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. ' - . self::DIVI_NOTE . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update ' - . '(direkt: lit-content-update) änderbar.'; + /** Divi content on a site where the Divi 5 builder is not loaded (no Divi tools registered). */ + public const DIVI_INACTIVE_NOTE = 'Der Divi-5-Builder ist auf dieser Website nicht aktiv, daher gibt es keine ' + . '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) { @@ -63,7 +86,7 @@ class PostPresenter 'fields' => JsonMap::of($this->fields($post)), 'featured_image' => $this->featuredImage($post), 'rev' => Rev::ofPost($post), - 'hints' => $divi ? [self::DIVI_HINT] : [], + 'hints' => $divi ? [self::diviHint()] : [], ]); } diff --git a/src/Integrations/WordPress/Revisions/RevisionsListAbility.php b/src/Integrations/WordPress/Revisions/RevisionsListAbility.php index cf374ec..cab1380 100644 --- a/src/Integrations/WordPress/Revisions/RevisionsListAbility.php +++ b/src/Integrations/WordPress/Revisions/RevisionsListAbility.php @@ -19,7 +19,7 @@ use WP_Post; * 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 * 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 { @@ -124,7 +124,7 @@ final class RevisionsListAbility implements Ability 'post_id' => (int) $post->ID, 'items' => $items, '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()] : [], ]; } diff --git a/tests/Integration/divi-read-smoke.sh b/tests/Integration/divi-read-smoke.sh new file mode 100755 index 0000000..14d0864 --- /dev/null +++ b/tests/Integration/divi-read-smoke.sh @@ -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" diff --git a/tests/Unit/Guides/InstructionsBuilderTest.php b/tests/Unit/Guides/InstructionsBuilderTest.php index cd63be2..1b6c9ed 100644 --- a/tests/Unit/Guides/InstructionsBuilderTest.php +++ b/tests/Unit/Guides/InstructionsBuilderTest.php @@ -45,6 +45,22 @@ final class InstructionsBuilderTest extends TestCase 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 { $core = $this->coreWith($this->ability('lit/guide', true)); diff --git a/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php b/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php index 9db7dd5..dfd1033 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php @@ -10,6 +10,7 @@ use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; 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\Schema\ContentSchema; 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' => '

Neu

']); self::assertSame('lit_unsupported', $result->get_error_code()); - self::assertStringNotContainsString('lit-divi', $result->get_error_message()); - self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version; bis dahin im Divi-Builder ' - . 'bearbeiten.', $result->get_error_data()['hint']); + self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, $result->get_error_data()['hint']); } public function testNothingToChangeIsAValidationError(): void diff --git a/tests/Unit/Integrations/WordPress/Content/PostPresenterTest.php b/tests/Unit/Integrations/WordPress/Content/PostPresenterTest.php index cd1b3d9..3daefcb 100644 --- a/tests/Unit/Integrations/WordPress/Content/PostPresenterTest.php +++ b/tests/Unit/Integrations/WordPress/Content/PostPresenterTest.php @@ -87,13 +87,20 @@ final class PostPresenterTest extends TestCase self::assertTrue($full['divi']); self::assertNull($full['content']); self::assertNull($full['featured_image']); - self::assertStringNotContainsString('lit-divi', $full['hints'][0]); - self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version', $full['hints'][0]); + self::assertSame([PostPresenter::diviHint(false)], $full['hints']); self::assertInstanceOf(stdClass::class, $full['terms']); self::assertInstanceOf(stdClass::class, $full['fields']); 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 { $this->stubLinks(); diff --git a/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php b/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php index 89636e3..7bdafdc 100644 --- a/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php @@ -66,7 +66,7 @@ final class RevisionsListAbilityTest extends TestCase self::assertTrue($result['items'][0]['divi']); self::assertFalse($result['items'][1]['divi']); - self::assertSame([PostPresenter::DIVI_HINT], $result['hints']); + self::assertSame([PostPresenter::diviHint()], $result['hints']); } public function testRevisionsOfATrashedPostCanStillBeListed(): void