From 5a63c5ace1b25515b8a6818fe5858ad2a4090837 Mon Sep 17 00:00:00 2001 From: Nischay Date: Fri, 8 May 2026 10:55:58 +0530 Subject: [PATCH 01/12] refactor: rename experiments to Editorial Notes and Editorial Updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review Notes → Editorial Notes, Refine Notes → Editorial Updates. Full rename: PHP namespaces, class names, feature slugs, DB option keys, Webpack entries, TS source dirs, REST ability routes, plugin IDs, integration tests, E2E specs, mock fixtures, and docs. V1_0_0 upgrade migration copies existing enabled-state options from old keys to new keys on plugin upgrade. --- CHANGELOG.md | 2 + README.md | 4 +- .../{review-notes.md => editorial-notes.md} | 72 +++++++++---------- .../{refine-notes.md => editorial-updates.md} | 48 ++++++------- .../Editorial_Notes.php} | 8 +-- .../system-instruction.php | 4 +- .../Editorial_Updates.php} | 8 +-- .../system-instruction.php | 4 +- includes/Admin/Upgrades.php | 2 + includes/Admin/Upgrades/V1_0_0.php | 60 ++++++++++++++++ .../Editorial_Notes.php} | 22 +++--- .../Editorial_Updates.php} | 22 +++--- includes/Experiments/Experiments.php | 4 +- .../block-editor-augmentation.d.ts | 0 .../components/EditorialNotesPlugin.tsx} | 22 +++--- .../hooks/useEditorialNotes.ts} | 14 ++-- .../index.tsx | 8 +-- .../components/EditorialUpdatesPlugin.tsx} | 14 ++-- .../hooks/useEditorialUpdates.ts} | 24 +++---- .../index.tsx | 10 +-- src/utils/notes.ts | 2 +- ..._NotesTest.php => Editorial_NotesTest.php} | 40 +++++------ ...otesTest.php => Editorial_UpdatesTest.php} | 42 +++++------ .../Includes/Experiment_LoaderTest.php | 10 +-- .../Editorial_NotesTest.php} | 24 +++---- .../Editorial_UpdatesTest.php} | 24 +++---- .../e2e-request-mocking.php | 12 ++-- ....json => editorial-notes-completions.json} | 0 ...es.json => editorial-notes-responses.json} | 0 ...son => editorial-updates-completions.json} | 2 +- ....json => editorial-updates-responses.json} | 4 +- ...-notes.spec.js => editorial-notes.spec.js} | 32 ++++----- ...otes.spec.js => editorial-updates.spec.js} | 24 +++---- webpack.config.js | 8 +-- 34 files changed, 320 insertions(+), 256 deletions(-) rename docs/experiments/{review-notes.md => editorial-notes.md} (81%) rename docs/experiments/{refine-notes.md => editorial-updates.md} (71%) rename includes/Abilities/{Review_Notes/Review_Notes.php => Editorial_Notes/Editorial_Notes.php} (98%) rename includes/Abilities/{Review_Notes => Editorial_Notes}/system-instruction.php (97%) rename includes/Abilities/{Refine_Notes/Refine_Notes.php => Editorial_Updates/Editorial_Updates.php} (97%) rename includes/Abilities/{Refine_Notes => Editorial_Updates}/system-instruction.php (93%) create mode 100644 includes/Admin/Upgrades/V1_0_0.php rename includes/Experiments/{Review_Notes/Review_Notes.php => Editorial_Notes/Editorial_Notes.php} (85%) rename includes/Experiments/{Refine_Notes/Refine_Notes.php => Editorial_Updates/Editorial_Updates.php} (76%) rename src/experiments/{review-notes => editorial-notes}/block-editor-augmentation.d.ts (100%) rename src/experiments/{review-notes/components/ReviewNotesPlugin.tsx => editorial-notes/components/EditorialNotesPlugin.tsx} (85%) rename src/experiments/{review-notes/hooks/useReviewNotes.ts => editorial-notes/hooks/useEditorialNotes.ts} (97%) rename src/experiments/{review-notes => editorial-notes}/index.tsx (61%) rename src/experiments/{refine-notes/components/RefineNotesPlugin.tsx => editorial-updates/components/EditorialUpdatesPlugin.tsx} (81%) rename src/experiments/{refine-notes/hooks/useRefineNotes.ts => editorial-updates/hooks/useEditorialUpdates.ts} (94%) rename src/experiments/{refine-notes => editorial-updates}/index.tsx (55%) rename tests/Integration/Includes/Abilities/{Review_NotesTest.php => Editorial_NotesTest.php} (95%) rename tests/Integration/Includes/Abilities/{Refine_NotesTest.php => Editorial_UpdatesTest.php} (95%) rename tests/Integration/Includes/Experiments/{Review_Notes/Review_NotesTest.php => Editorial_Notes/Editorial_NotesTest.php} (87%) rename tests/Integration/Includes/Experiments/{Refine_Notes/Refine_NotesTest.php => Editorial_Updates/Editorial_UpdatesTest.php} (74%) rename tests/e2e-request-mocking/responses/OpenAI/{review-notes-completions.json => editorial-notes-completions.json} (100%) rename tests/e2e-request-mocking/responses/OpenAI/{review-notes-responses.json => editorial-notes-responses.json} (100%) rename tests/e2e-request-mocking/responses/OpenAI/{refine-notes-completions.json => editorial-updates-completions.json} (94%) rename tests/e2e-request-mocking/responses/OpenAI/{refine-notes-responses.json => editorial-updates-responses.json} (94%) rename tests/e2e/specs/experiments/{review-notes.spec.js => editorial-notes.spec.js} (85%) rename tests/e2e/specs/experiments/{refine-notes.spec.js => editorial-updates.spec.js} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f737469..5ad2dd045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file, per [the Keep a Changelog standard](http://keepachangelog.com/), and will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - TBD +### Changed +- Rename "Review Notes" experiment to "Editorial Notes" and "Refine from Notes" experiment to "Editorial Updates" ([#506](https://github.com/WordPress/ai/issues/506)). ## [0.9.0] - 2026-05-07 ### Added diff --git a/README.md b/README.md index ee3b839e0..6cf3d794a 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ This [Canonical Plugin](https://make.wordpress.org/core/2022/09/11/canonical-plu * **[Image Generation and Editing](docs/features/image-generation.md)** - Create and edit images from post content in the editor, also via the Media Library. * **[Meta Description Generation](docs/experiments/meta-description.md)** - Generates meta description suggestions and integrates those with various SEO plugins. * **Multi-Provider Support** - Works with popular AI providers like OpenAI, Google, and Anthropic. -* **[Refine Notes](docs/experiments/refine-notes.md)** - Automatically apply editorial notes to content. -* **[Review Notes](docs/experiments/review-notes.md)** - Reviews post content block-by-block and adds Notes with suggestions for Accessibility, Readability, Grammar, and SEO. +* **[Editorial Updates](docs/experiments/editorial-updates.md)** - Automatically apply editorial notes to content. +* **[Editorial Notes](docs/experiments/editorial-notes.md)** - Reviews post content block-by-block and adds Notes with suggestions for Accessibility, Readability, Grammar, and SEO. * **[Title Generation](docs/experiments/title-generation.md)** - Generates title suggestions from content. ## Roadmap diff --git a/docs/experiments/review-notes.md b/docs/experiments/editorial-notes.md similarity index 81% rename from docs/experiments/review-notes.md rename to docs/experiments/editorial-notes.md index 7901a7343..805658aed 100644 --- a/docs/experiments/review-notes.md +++ b/docs/experiments/editorial-notes.md @@ -1,14 +1,14 @@ -# AI Review Notes +# AI Editorial Notes ## Summary -The AI Review Notes experiment adds a block-by-block AI editorial review to the WordPress post editor. Clicking "Generate Review Notes" in the post sidebar triggers the AI to examine each reviewable block and create WordPress Notes directly on the relevant blocks with concise, actionable suggestions across four categories: **Accessibility**, **Readability**, **Grammar**, and **SEO**. +The AI Editorial Notes experiment adds a block-by-block AI editorial review to the WordPress post editor. Clicking "Generate Editorial Notes" in the post sidebar triggers the AI to examine each reviewable block and create WordPress Notes directly on the relevant blocks with concise, actionable suggestions across four categories: **Accessibility**, **Readability**, **Grammar**, and **SEO**. ## Overview ### For End Users -When enabled, a "Generate Review Notes" button appears in the post status info panel (the sidebar area below the post status). Clicking it triggers a review pass: +When enabled, a "Generate Editorial Notes" button appears in the post status info panel (the sidebar area below the post status). Clicking it triggers a review pass: 1. The button label updates to show review progress (`Reviewing blocks… (2 of 8)`) 2. Each content block is sent individually to the AI for analysis @@ -28,30 +28,30 @@ When enabled, a "Generate Review Notes" button appears in the post status info p The experiment consists of: -1. **Experiment Class** (`WordPress\AI\Experiments\Review_Notes\Review_Notes`): Registers the ability, enqueues the block editor asset, and wires server-side hooks for Note author override and block metadata cleanup -2. **Ability Class** (`WordPress\AI\Abilities\Review_Notes\Review_Notes`): Receives a single block's content and returns structured JSON suggestions -3. **React Plugin** (`src/experiments/review-notes/`): Drives the UI and orchestrates block traversal, Note creation, and thread management via WordPress data stores +1. **Experiment Class** (`WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes`): Registers the ability, enqueues the block editor asset, and wires server-side hooks for Note author override and block metadata cleanup +2. **Ability Class** (`WordPress\AI\Abilities\Editorial_Notes\Editorial_Notes`): Receives a single block's content and returns structured JSON suggestions +3. **React Plugin** (`src/experiments/editorial-notes/`): Drives the UI and orchestrates block traversal, Note creation, and thread management via WordPress data stores ## Architecture & Implementation ### Key Hooks & Entry Points -`WordPress\AI\Experiments\Review_Notes\Review_Notes::register()` wires everything once the experiment is enabled: +`WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes::register()` wires everything once the experiment is enabled: -- `wp_abilities_api_init` → registers the `ai/review-notes` ability +- `wp_abilities_api_init` → registers the `ai/editorial-notes` ability - `enqueue_block_editor_assets` → enqueues the React bundle whenever the block editor loads - `rest_pre_insert_comment` (filter) → `maybe_set_ai_author()` — overrides the comment author to "WordPress AI" when `meta.ai_note` is `true`, so AI-generated Notes are not attributed to the authenticated user's account ### Assets & Data Flow 1. **PHP Side:** - - `enqueue_assets()` loads `experiments/review-notes` and localizes `window.aiReviewNotesData`: + - `enqueue_assets()` loads `experiments/editorial-notes` and localizes `window.aiEditorialNotesData`: - `enabled`: Whether the experiment is currently enabled 2. **React Side:** - - `index.tsx` registers the `ai-review-notes` plugin - - `ReviewNotesPlugin.tsx` renders the button inside `PluginPostStatusInfo` - - `useReviewNotes.ts` hook manages all state and orchestration: + - `index.tsx` registers the `ai-editorial-notes` plugin + - `EditorialNotesPlugin.tsx` renders the button inside `PluginPostStatusInfo` + - `useEditorialNotes.ts` hook manages all state and orchestration: - Flattens the block tree to get all descendants - Filters to reviewable block types with sufficient content (≥ 20 chars), capped at 25 blocks - Fetches Notes in two parallel requests: @@ -167,7 +167,7 @@ Notes are `WP_Comment` objects with `comment_type = 'note'` and `status = 'hold' ### Endpoint ```text -POST /wp-json/wp-abilities/v1/abilities/ai/review-notes/run +POST /wp-json/wp-abilities/v1/abilities/ai/editorial-notes/run ``` ### Authentication @@ -179,7 +179,7 @@ See [TESTING_REST_API.md](../TESTING_REST_API.md) for authentication details (ap #### Review a Paragraph Block ```bash -curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/review-notes/run" \ +curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/editorial-notes/run" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ @@ -209,7 +209,7 @@ curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/review-n #### Review an Image Block (Accessibility Only) ```bash -curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/review-notes/run" \ +curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/editorial-notes/run" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ @@ -242,7 +242,7 @@ import apiFetch from '@wordpress/api-fetch'; async function reviewBlock( blockType, blockContent, existingNotes = [] ) { const result = await apiFetch( { - path: '/wp-abilities/v1/abilities/ai/review-notes/run', + path: '/wp-abilities/v1/abilities/ai/editorial-notes/run', method: 'POST', data: { input: { @@ -271,7 +271,7 @@ async function reviewBlock( blockType, blockContent, existingNotes = [] ) { ### Customizing the System Instruction -Edit `includes/Abilities/Review_Notes/system-instruction.php` to adjust: +Edit `includes/Abilities/Editorial_Notes/system-instruction.php` to adjust: - Which review types apply to which block types - How strictly prior suggestions are de-duplicated @@ -290,7 +290,7 @@ add_filter( 'wpai_preferred_text_models', function( $models ) { ### Disabling the Experiment Programmatically ```php -add_filter( 'wpai_feature_review-notes_enabled', '__return_false' ); +add_filter( 'wpai_feature_editorial-notes_enabled', '__return_false' ); ``` ### Adding Custom Review Types @@ -299,7 +299,7 @@ The `review_types` input field accepts any string values. Pass additional type n ```javascript // In your custom JS -await runAbility( 'ai/review-notes', { +await runAbility( 'ai/editorial-notes', { block_type: 'core/paragraph', block_content: '...', review_types: [ 'accessibility', 'readability', 'grammar', 'seo', 'tone' ], @@ -316,20 +316,20 @@ Then add guidance for the `tone` type to `system-instruction.php`. 1. **Enable the experiment:** - Go to `Settings → AI` - Enable the global toggle - - Enable **AI Review Notes** + - Enable **AI Editorial Notes** - Ensure valid AI credentials are configured 2. **Run a review:** - Create or open a post with a mix of block types (headings, paragraphs, an image without alt text, a list) - Open the post sidebar (click the **Settings** button in the toolbar) - - Click **Generate Review Notes** in the post info panel + - Click **Generate Editorial Notes** in the post info panel - Watch the progress counter advance (`Reviewing blocks… 2 of 8`) - After completion, open the **Notes** panel (via the block toolbar or the comments icon) - Verify Notes appear on relevant blocks, formatted as `[REVIEW_TYPE] Suggestion text.` - Verify Notes show "WordPress AI" as the author rather than your account name 3. **Re-run accumulation:** - - Click **Generate Review Notes** a second time + - Click **Generate Editorial Notes** a second time - Verify existing Note threads gain replies rather than new top-level Notes - Verify prior suggestions are not repeated @@ -357,8 +357,8 @@ npm run test:php ``` Test files: -- `tests/Integration/Includes/Abilities/Review_NotesTest.php` — Ability class tests -- `tests/Integration/Includes/Experiments/Review_Notes/Review_NotesTest.php` — Experiment class tests +- `tests/Integration/Includes/Abilities/Editorial_NotesTest.php` — Ability class tests +- `tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php` — Experiment class tests Covers: - Input/output schema structure @@ -377,10 +377,10 @@ Covers: ```bash npm run wp-env:test start # Start the test environment -npm run test:e2e -- --grep "AI Review Notes" +npm run test:e2e -- --grep "AI Editorial Notes" ``` -Test file: `tests/e2e/review-notes.spec.ts` +Test file: `tests/e2e/editorial-notes.spec.ts` Covers: - Button visibility in editor sidebar @@ -423,13 +423,13 @@ Covers: ## Related Files -- **Experiment:** `includes/Experiments/Review_Notes/Review_Notes.php` -- **Ability:** `includes/Abilities/Review_Notes/Review_Notes.php` -- **System Instruction:** `includes/Abilities/Review_Notes/system-instruction.php` -- **React Entry:** `src/experiments/review-notes/index.tsx` -- **React Plugin Component:** `src/experiments/review-notes/components/ReviewNotesPlugin.tsx` -- **React Hook:** `src/experiments/review-notes/hooks/useReviewNotes.ts` -- **PHPUnit Tests (Ability):** `tests/Integration/Includes/Abilities/Review_NotesTest.php` -- **PHPUnit Tests (Experiment):** `tests/Integration/Includes/Experiments/Review_Notes/Review_NotesTest.php` -- **E2E Tests:** `tests/e2e/review-notes.spec.ts` -- **Mock Fixtures:** `tests/e2e-request-mocking/responses/OpenAI/review-notes-suggestions.json` +- **Experiment:** `includes/Experiments/Editorial_Notes/Editorial_Notes.php` +- **Ability:** `includes/Abilities/Editorial_Notes/Editorial_Notes.php` +- **System Instruction:** `includes/Abilities/Editorial_Notes/system-instruction.php` +- **React Entry:** `src/experiments/editorial-notes/index.tsx` +- **React Plugin Component:** `src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx` +- **React Hook:** `src/experiments/editorial-notes/hooks/useEditorialNotes.ts` +- **PHPUnit Tests (Ability):** `tests/Integration/Includes/Abilities/Editorial_NotesTest.php` +- **PHPUnit Tests (Experiment):** `tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php` +- **E2E Tests:** `tests/e2e/editorial-notes.spec.ts` +- **Mock Fixtures:** `tests/e2e-request-mocking/responses/OpenAI/editorial-notes-suggestions.json` diff --git a/docs/experiments/refine-notes.md b/docs/experiments/editorial-updates.md similarity index 71% rename from docs/experiments/refine-notes.md rename to docs/experiments/editorial-updates.md index f225bf5f6..c129cc165 100644 --- a/docs/experiments/refine-notes.md +++ b/docs/experiments/editorial-updates.md @@ -1,14 +1,14 @@ -# Refine from Notes +# Editorial Updates ## Summary -The Refine from Notes experiment enables users to automatically apply pending editorial feedback/notes to their WordPress post content using AI. Clicking "Refine from Notes" in the post sidebar triggers the AI to contextually examine all blocks with pending Notes and modify the text according to the provided suggestions. +The Editorial Updates experiment enables users to automatically apply pending editorial feedback/notes to their WordPress post content using AI. Clicking "Editorial Updates" in the post sidebar triggers the AI to contextually examine all blocks with pending Notes and modify the text according to the provided suggestions. ## Overview ### For End Users -When enabled, a "Refine from Notes" button appears in the post status info panel (the sidebar area below the post status) assuming there is at least one Note pending on any block in the post. Clicking it triggers the refinement process: +When enabled, a "Editorial Updates" button appears in the post status info panel (the sidebar area below the post status) assuming there is at least one Note pending on any block in the post. Clicking it triggers the refinement process: 1. The button label updates to show progression across blocks (`Refining block (2 of 4)…`) 2. Each block that has a pending Note attached is sent to the AI alongside the Note's content. @@ -26,31 +26,31 @@ When enabled, a "Refine from Notes" button appears in the post status info panel The experiment consists of: -1. **Experiment Class** (`WordPress\AI\Experiments\Refine_Notes\Refine_Notes`): Registers the ability and enqueues the block editor asset. -2. **Ability Class** (`WordPress\AI\Abilities\Refine_Notes\Refine_Notes`): Receives a single block's content, surrounding context, and associated notes, parsing the resulting AI output back into plain string replacements. -3. **React Plugin** (`src/experiments/refine-notes/`): Drives the sidebar UI, discovers threaded Notes via WordPress data stores, processes block attributes iteratively, and manages Editor saving workflows. +1. **Experiment Class** (`WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates`): Registers the ability and enqueues the block editor asset. +2. **Ability Class** (`WordPress\AI\Abilities\Editorial_Updates\Editorial_Updates`): Receives a single block's content, surrounding context, and associated notes, parsing the resulting AI output back into plain string replacements. +3. **React Plugin** (`src/experiments/editorial-updates/`): Drives the sidebar UI, discovers threaded Notes via WordPress data stores, processes block attributes iteratively, and manages Editor saving workflows. ## Architecture & Implementation ### Key Hooks & Entry Points -`WordPress\AI\Experiments\Refine_Notes\Refine_Notes::register()` wires everything once the experiment is enabled: +`WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates::register()` wires everything once the experiment is enabled: -- `wp_abilities_api_init` → registers the `ai/refine-notes` ability +- `wp_abilities_api_init` → registers the `ai/editorial-updates` ability - `enqueue_block_editor_assets` → enqueues the React bundle whenever the block editor loads ### Assets & Data Flow 1. **PHP Side:** - - `enqueue_assets()` loads `experiments/refine-notes` and localizes `window.RefineNotesData`: + - `enqueue_assets()` loads `experiments/editorial-updates` and localizes `window.aiEditorialUpdatesData`: - `enabled`: Whether the experiment is currently enabled 2. **React Side:** - - `index.tsx` registers the `ai-refine-notes` plugin. - - `RefineNotesPlugin.tsx` conditionally renders the button inside `PluginPostStatusInfo`. - - `useRefineNotes.ts` hook manages all state and orchestration: + - `index.tsx` registers the `ai-editorial-updates` plugin. + - `EditorialUpdatesPlugin.tsx` conditionally renders the button inside `PluginPostStatusInfo`. + - `useEditorialUpdates.ts` hook manages all state and orchestration: - Flattens the active block tree. - Fetches active pending Notes via `GET /wp/v2/comments?type=note&status=hold&post=&per_page=100`. - Maps notes and child threaded-replies directly to their parent `blockClientId`. @@ -119,7 +119,7 @@ In both cases, users without the required capability receive an `insufficient_ca ### Endpoint ```text -POST /wp-json/wp-abilities/v1/abilities/ai/refine-notes/run +POST /wp-json/wp-abilities/v1/abilities/ai/editorial-updates/run ``` ### Authentication @@ -131,7 +131,7 @@ See [TESTING_REST_API.md](../TESTING_REST_API.md) for authentication details (ap #### Refine a Paragraph Block ```bash -curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/refine-notes/run" \ +curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/editorial-updates/run" \ -u "username:application-password" \ -H "Content-Type: application/json" \ -d '{ @@ -161,13 +161,13 @@ curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/refine-n ## Related Files -- **Experiment:** `includes/Experiments/Refine_Notes/Refine_Notes.php` -- **Ability:** `includes/Abilities/Refine_Notes/Refine_Notes.php` -- **System Instruction:** `includes/Abilities/Refine_Notes/system-instruction.php` -- **React Entry:** `src/experiments/refine-notes/index.tsx` -- **React Plugin Component:** `src/experiments/refine-notes/components/RefineNotesPlugin.tsx` -- **React Hook:** `src/experiments/refine-notes/hooks/useRefineNotes.ts` -- **PHPUnit Tests (Ability):** `tests/Integration/Includes/Abilities/Refine_NotesTest.php` -- **PHPUnit Tests (Experiment):** `tests/Integration/Includes/Experiments/Refine_Notes/Refine_NotesTest.php` -- **E2E Tests:** `tests/e2e/specs/experiments/refine-notes.spec.js` -- **Mock Fixtures:** `tests/e2e-request-mocking/responses/OpenAI/refine-notes-completions.json` and `tests/e2e-request-mocking/responses/OpenAI/refine-notes-responses.json` +- **Experiment:** `includes/Experiments/Editorial_Updates/Editorial_Updates.php` +- **Ability:** `includes/Abilities/Editorial_Updates/Editorial_Updates.php` +- **System Instruction:** `includes/Abilities/Editorial_Updates/system-instruction.php` +- **React Entry:** `src/experiments/editorial-updates/index.tsx` +- **React Plugin Component:** `src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx` +- **React Hook:** `src/experiments/editorial-updates/hooks/useEditorialUpdates.ts` +- **PHPUnit Tests (Ability):** `tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php` +- **PHPUnit Tests (Experiment):** `tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php` +- **E2E Tests:** `tests/e2e/specs/experiments/editorial-updates.spec.js` +- **Mock Fixtures:** `tests/e2e-request-mocking/responses/OpenAI/editorial-updates-completions.json` and `tests/e2e-request-mocking/responses/OpenAI/editorial-updates-responses.json` diff --git a/includes/Abilities/Review_Notes/Review_Notes.php b/includes/Abilities/Editorial_Notes/Editorial_Notes.php similarity index 98% rename from includes/Abilities/Review_Notes/Review_Notes.php rename to includes/Abilities/Editorial_Notes/Editorial_Notes.php index 0a88f2a0a..da4fd91c4 100644 --- a/includes/Abilities/Review_Notes/Review_Notes.php +++ b/includes/Abilities/Editorial_Notes/Editorial_Notes.php @@ -1,13 +1,13 @@ migrate_option( 'wpai_feature_review-notes_enabled', 'wpai_feature_editorial-notes_enabled' ); + $this->migrate_option( 'wpai_feature_refine-notes_enabled', 'wpai_feature_editorial-updates_enabled' ); + } + + /** + * Migrates an individual option from the old name. + * + * Will skip migration if the new option already has a value. + * + * @param string $old_option The old option name. + * @param string $new_option The new option name. + */ + private function migrate_option( string $old_option, string $new_option ): void { + $old_value = get_option( $old_option, '' ); + if ( '' === $old_value || '' !== get_option( $new_option, '' ) ) { + return; + } + + update_option( $new_option, $old_value ); + delete_option( $old_option ); + } +} diff --git a/includes/Experiments/Review_Notes/Review_Notes.php b/includes/Experiments/Editorial_Notes/Editorial_Notes.php similarity index 85% rename from includes/Experiments/Review_Notes/Review_Notes.php rename to includes/Experiments/Editorial_Notes/Editorial_Notes.php index daa2b9033..757ee02be 100644 --- a/includes/Experiments/Review_Notes/Review_Notes.php +++ b/includes/Experiments/Editorial_Notes/Editorial_Notes.php @@ -1,15 +1,15 @@ __( 'Review Notes', 'ai' ), + 'label' => __( 'Editorial Notes', 'ai' ), 'description' => __( 'Reviews post content block-by-block and adds Notes with suggestions for Accessibility, Readability, Grammar, and SEO. Requires an AI connector that includes support for text generation models.', 'ai' ), 'category' => Experiment_Category::EDITOR, ); @@ -80,7 +80,7 @@ public function register_abilities(): void { array( 'label' => $this->get_label(), 'description' => $this->get_description(), - 'ability_class' => Review_Notes_Ability::class, + 'ability_class' => Editorial_Notes_Ability::class, ), ); } @@ -123,10 +123,10 @@ public function maybe_set_ai_author( $prepared_comment, \WP_REST_Request $reques * @since 0.4.0 */ public function enqueue_assets(): void { - Asset_Loader::enqueue_script( 'review_notes', 'experiments/review-notes' ); + Asset_Loader::enqueue_script( 'editorial_notes', 'experiments/editorial-notes' ); Asset_Loader::localize_script( - 'review_notes', - 'ReviewNotesData', + 'editorial_notes', + 'EditorialNotesData', array( 'enabled' => $this->is_enabled(), ) diff --git a/includes/Experiments/Refine_Notes/Refine_Notes.php b/includes/Experiments/Editorial_Updates/Editorial_Updates.php similarity index 76% rename from includes/Experiments/Refine_Notes/Refine_Notes.php rename to includes/Experiments/Editorial_Updates/Editorial_Updates.php index f6d404364..f304edd1a 100644 --- a/includes/Experiments/Refine_Notes/Refine_Notes.php +++ b/includes/Experiments/Editorial_Updates/Editorial_Updates.php @@ -1,15 +1,15 @@ __( 'Refine from Notes', 'ai' ), + 'label' => __( 'Editorial Updates', 'ai' ), 'description' => __( 'Analyze feedback that has been left via Notes and apply edits where needed.', 'ai' ), 'category' => Experiment_Category::EDITOR, ); @@ -70,7 +70,7 @@ public function register_abilities(): void { array( 'label' => $this->get_label(), 'description' => $this->get_description(), - 'ability_class' => Refine_Notes_Ability::class, + 'ability_class' => Editorial_Updates_Ability::class, ), ); } @@ -81,7 +81,7 @@ public function register_abilities(): void { * @since 0.8.0 */ public function enqueue_assets(): void { - Asset_Loader::enqueue_script( 'refine_notes', 'experiments/refine-notes' ); + Asset_Loader::enqueue_script( 'editorial_updates', 'experiments/editorial-updates' ); $post_type = get_post_type(); $post_type_object = $post_type ? get_post_type_object( $post_type ) : null; @@ -90,8 +90,8 @@ public function enqueue_assets(): void { : null; Asset_Loader::localize_script( - 'refine_notes', - 'RefineNotesData', + 'editorial_updates', + 'EditorialUpdatesData', array( 'enabled' => $this->is_enabled(), 'rest_base' => $rest_base, diff --git a/includes/Experiments/Experiments.php b/includes/Experiments/Experiments.php index 8aeaecb53..7cd0ede74 100644 --- a/includes/Experiments/Experiments.php +++ b/includes/Experiments/Experiments.php @@ -33,8 +33,8 @@ final class Experiments { \WordPress\AI\Experiments\Excerpt_Generation\Excerpt_Generation::class, \WordPress\AI\Experiments\Alt_Text_Generation\Alt_Text_Generation::class, \WordPress\AI\Experiments\Meta_Description\Meta_Description::class, - \WordPress\AI\Experiments\Review_Notes\Review_Notes::class, - \WordPress\AI\Experiments\Refine_Notes\Refine_Notes::class, + \WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes::class, + \WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates::class, \WordPress\AI\Experiments\Summarization\Summarization::class, \WordPress\AI\Experiments\Title_Generation\Title_Generation::class, \WordPress\AI\Experiments\Comment_Moderation\Comment_Moderation::class, diff --git a/src/experiments/review-notes/block-editor-augmentation.d.ts b/src/experiments/editorial-notes/block-editor-augmentation.d.ts similarity index 100% rename from src/experiments/review-notes/block-editor-augmentation.d.ts rename to src/experiments/editorial-notes/block-editor-augmentation.d.ts diff --git a/src/experiments/review-notes/components/ReviewNotesPlugin.tsx b/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx similarity index 85% rename from src/experiments/review-notes/components/ReviewNotesPlugin.tsx rename to src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx index 2d45dc335..c1bf38314 100644 --- a/src/experiments/review-notes/components/ReviewNotesPlugin.tsx +++ b/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx @@ -1,5 +1,5 @@ /** - * AI Review Notes plugin component. + * AI Editorial Notes plugin component. */ /** @@ -24,24 +24,24 @@ import { commentContent } from '@wordpress/icons'; * Internal dependencies */ import { REVIEWABLE_BLOCK_TYPES } from '../../../utils/notes'; -import { useReviewBlock, useReviewNotes } from '../hooks/useReviewNotes'; +import { useEditorialBlock, useEditorialNotes } from '../hooks/useEditorialNotes'; /** - * ReviewNotesPlugin component. + * EditorialNotesPlugin component. * - * Renders a "Generate Review Notes" button in the post status info panel, - * and a "Generate Review Note" item in the block settings menu for + * Renders a "Generate Editorial Notes" button in the post status info panel, + * and a "Generate Editorial Note" item in the block settings menu for * reviewable blocks. */ -export default function ReviewNotesPlugin() { +export default function EditorialNotesPlugin() { const { isReviewing, progress, total, lastRunCount, runReview } = - useReviewNotes(); - const { isReviewing: isReviewingBlock, reviewBlock } = useReviewBlock(); + useEditorialNotes(); + const { isReviewing: isReviewingBlock, reviewBlock } = useEditorialBlock(); const { openGeneralSidebar } = useDispatch( editPostStore ); const openNotesPanel = () => openGeneralSidebar?.( 'edit-post/collab-sidebar' ); - if ( ! ( window as any ).aiReviewNotesData?.enabled ) { + if ( ! ( window as any ).aiEditorialNotesData?.enabled ) { return null; } @@ -53,7 +53,7 @@ export default function ReviewNotesPlugin() { ); const buttonLabel = isReviewing ? reviewingLabel - : __( 'Generate Review Notes', 'ai' ); + : __( 'Generate Editorial Notes', 'ai' ); const buttonDescription = __( 'This will review the content of this post block-by-block, and create Notes attached to each block with suggestions.', 'ai' @@ -145,7 +145,7 @@ export default function ReviewNotesPlugin() { > { isReviewingBlock ? __( 'Reviewing…', 'ai' ) - : __( 'Generate Review Note', 'ai' ) } + : __( 'Generate Editorial Note', 'ai' ) } ); } } diff --git a/src/experiments/review-notes/hooks/useReviewNotes.ts b/src/experiments/editorial-notes/hooks/useEditorialNotes.ts similarity index 97% rename from src/experiments/review-notes/hooks/useReviewNotes.ts rename to src/experiments/editorial-notes/hooks/useEditorialNotes.ts index 0c0028570..542bf0f4f 100644 --- a/src/experiments/review-notes/hooks/useReviewNotes.ts +++ b/src/experiments/editorial-notes/hooks/useEditorialNotes.ts @@ -1,5 +1,5 @@ /** - * Custom hook for AI Review Notes functionality. + * Custom hook for AI Editorial Notes functionality. */ /** @@ -137,7 +137,7 @@ async function reviewSingleBlock( ); // Call the review Ability. - const result = await runAbility< ReviewResult >( 'ai/review-notes', { + const result = await runAbility< ReviewResult >( 'ai/editorial-notes', { block_type: block.name, block_content: blockText, context, @@ -154,11 +154,11 @@ async function reviewSingleBlock( } /** - * Hook for AI Review Notes functionality. + * Hook for AI Editorial Notes functionality. * * @return Object with review state and the runReview handler. */ -export function useReviewNotes(): { +export function useEditorialNotes(): { isReviewing: boolean; progress: number; total: number; @@ -177,7 +177,7 @@ export function useReviewNotes(): { setLastRunCount( null ); ( dispatch( noticesStore ) as any ).removeNotice( - 'ai_review_notes_error' + 'ai_editorial_notes_error' ); try { @@ -266,7 +266,7 @@ export function useReviewNotes(): { ( dispatch( noticesStore ) as any ).createErrorNotice( error?.message ?? String( error ), { - id: 'ai_review_notes_error', + id: 'ai_editorial_notes_error', isDismissible: true, } ); @@ -283,7 +283,7 @@ export function useReviewNotes(): { * * @return Object with reviewing state and the reviewBlock handler. */ -export function useReviewBlock(): { +export function useEditorialBlock(): { isReviewing: boolean; reviewBlock: ( clientId: string ) => Promise< void >; } { diff --git a/src/experiments/review-notes/index.tsx b/src/experiments/editorial-notes/index.tsx similarity index 61% rename from src/experiments/review-notes/index.tsx rename to src/experiments/editorial-notes/index.tsx index f8e2d9d7f..838dbb799 100644 --- a/src/experiments/review-notes/index.tsx +++ b/src/experiments/editorial-notes/index.tsx @@ -1,5 +1,5 @@ /** - * AI Review Notes plugin registration. + * AI Editorial Notes plugin registration. */ /** @@ -11,12 +11,12 @@ import { registerPlugin } from '@wordpress/plugins'; /** * Internal dependencies */ -import ReviewNotesPlugin from './components/ReviewNotesPlugin'; +import EditorialNotesPlugin from './components/EditorialNotesPlugin'; -registerPlugin( 'ai-review-notes', { +registerPlugin( 'ai-editorial-notes', { render: () => ( - + ), } ); diff --git a/src/experiments/refine-notes/components/RefineNotesPlugin.tsx b/src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx similarity index 81% rename from src/experiments/refine-notes/components/RefineNotesPlugin.tsx rename to src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx index 40eeea613..093b89da7 100644 --- a/src/experiments/refine-notes/components/RefineNotesPlugin.tsx +++ b/src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx @@ -13,17 +13,17 @@ import { commentContent } from '@wordpress/icons'; /** * Internal dependencies */ -import { useRefineNotes } from '../hooks/useRefineNotes'; +import { useEditorialUpdates } from '../hooks/useEditorialUpdates'; /** - * RefineNotesPlugin component. + * EditorialUpdatesPlugin component. * - * Renders a "Refine from Notes" button in the post status info panel + * Renders a "Editorial Updates" button in the post status info panel * when unresolved Notes exist. */ -export default function RefineNotesPlugin() { +export default function EditorialUpdatesPlugin() { const { isRefining, progress, total, hasPendingNotes, runRefinement } = - useRefineNotes(); + useEditorialUpdates(); if ( ! hasPendingNotes && ! isRefining ) { return null; @@ -36,7 +36,7 @@ export default function RefineNotesPlugin() { progress, total ) - : __( 'Refine from Notes', 'ai' ); + : __( 'Editorial Updates', 'ai' ); const buttonDescription = __( 'Automatically updates blocks using unresolved feedback Notes.', @@ -49,7 +49,7 @@ export default function RefineNotesPlugin() { direction="column" align="stretch" justify="flex-start" - className="editor-post-refine-notes" + className="editor-post-editorial-updates" gap={ 2 } > diff --git a/src/experiments/refine-notes/hooks/useRefineNotes.ts b/src/experiments/editorial-updates/hooks/useEditorialUpdates.ts similarity index 94% rename from src/experiments/refine-notes/hooks/useRefineNotes.ts rename to src/experiments/editorial-updates/hooks/useEditorialUpdates.ts index 490ae911c..30e881d5e 100644 --- a/src/experiments/refine-notes/hooks/useRefineNotes.ts +++ b/src/experiments/editorial-updates/hooks/useEditorialUpdates.ts @@ -1,5 +1,5 @@ /** - * Custom hook for Refine from Notes functionality. + * Custom hook for Editorial Updates functionality. */ /** @@ -73,7 +73,7 @@ async function resolveNote( noteId: number ): Promise< void > { * @property {boolean} hasPendingNotes Whether there are pending notes to process. * @property {Function} runRefinement Function to trigger the refinement process. */ -export function useRefineNotes(): { +export function useEditorialUpdates(): { isRefining: boolean; progress: number; total: number; @@ -90,8 +90,8 @@ export function useRefineNotes(): { ); // Reactively derived from the coreStore so the button appears/disappears - // automatically whenever Review Notes creates notes (via saveEntityRecord + - // invalidateResolutionForStoreSelector) or Refine Notes resolves them. + // automatically whenever Editorial Notes creates notes (via saveEntityRecord + + // invalidateResolutionForStoreSelector) or Editorial Updates resolves them. const hasPendingNotes = useSelect( ( sel ) => { if ( ! postId ) { @@ -120,7 +120,7 @@ export function useRefineNotes(): { setTotal( 0 ); ( dispatch( noticesStore ) as any ).removeNotice( - 'wpai_refine_notes_error' + 'wpai_editorial_updates_error' ); try { @@ -186,7 +186,7 @@ export function useRefineNotes(): { let firstErrorMessage: string | null = null; const notesToResolve: number[] = []; - // Process in batches of 4 (similar to Review Notes) + // Process in batches of 4 (similar to Editorial Notes) const BATCH_SIZE = 4; for ( let batchStart = 0; @@ -239,7 +239,7 @@ export function useRefineNotes(): { // Execute refinement try { const refinedContent = await runAbility< string >( - 'ai/refine-notes', + 'ai/editorial-updates', { block_type: block.name, block_content: blockText, @@ -312,7 +312,7 @@ export function useRefineNotes(): { firstErrorMessage ?? __( 'Refinement failed for all blocks.', 'ai' ), { - id: 'wpai_refine_notes_error', + id: 'wpai_editorial_updates_error', isDismissible: true, } ); @@ -324,8 +324,8 @@ export function useRefineNotes(): { // created. This keeps the editor state clean — no "unsaved // changes" prompt when navigating to the revisions link. await ( dispatch( editorStore ) as any ).savePost(); - const { aiRefineNotesData } = window as any; - const restBase = aiRefineNotesData?.rest_base as + const { aiEditorialUpdatesData } = window as any; + const restBase = aiEditorialUpdatesData?.rest_base as | string | undefined; @@ -344,7 +344,7 @@ export function useRefineNotes(): { ).getCurrentPostLastRevisionId() as number | null; } - const adminUrl = aiRefineNotesData?.admin_url as + const adminUrl = aiEditorialUpdatesData?.admin_url as | string | undefined; @@ -388,7 +388,7 @@ export function useRefineNotes(): { ( dispatch( noticesStore ) as any ).createErrorNotice( error?.message ?? String( error ), { - id: 'wpai_refine_notes_error', + id: 'wpai_editorial_updates_error', isDismissible: true, } ); diff --git a/src/experiments/refine-notes/index.tsx b/src/experiments/editorial-updates/index.tsx similarity index 55% rename from src/experiments/refine-notes/index.tsx rename to src/experiments/editorial-updates/index.tsx index d97c22fa9..8b174d707 100644 --- a/src/experiments/refine-notes/index.tsx +++ b/src/experiments/editorial-updates/index.tsx @@ -1,5 +1,5 @@ /** - * Refine Notes Experiment. + * Editorial Updates Experiment. */ /** @@ -11,13 +11,13 @@ import { registerPlugin } from '@wordpress/plugins'; /** * Internal dependencies */ -import RefineNotesPlugin from './components/RefineNotesPlugin'; +import EditorialUpdatesPlugin from './components/EditorialUpdatesPlugin'; -if ( ( window as any ).aiRefineNotesData?.enabled ) { - registerPlugin( 'ai-refine-notes', { +if ( ( window as any ).aiEditorialUpdatesData?.enabled ) { + registerPlugin( 'ai-editorial-updates', { render: () => ( - + ), } ); diff --git a/src/utils/notes.ts b/src/utils/notes.ts index ba4faa68c..db251db91 100644 --- a/src/utils/notes.ts +++ b/src/utils/notes.ts @@ -1,5 +1,5 @@ /** - * Shared utilities for Notes-related experiments (Review Notes, Refine Notes). + * Shared utilities for Notes-related experiments (Editorial Notes, Editorial Updates). */ /** diff --git a/tests/Integration/Includes/Abilities/Review_NotesTest.php b/tests/Integration/Includes/Abilities/Editorial_NotesTest.php similarity index 95% rename from tests/Integration/Includes/Abilities/Review_NotesTest.php rename to tests/Integration/Includes/Abilities/Editorial_NotesTest.php index f15963860..369b55489 100644 --- a/tests/Integration/Includes/Abilities/Review_NotesTest.php +++ b/tests/Integration/Includes/Abilities/Editorial_NotesTest.php @@ -1,6 +1,6 @@ 'Review Notes', + 'label' => 'Editorial Notes', 'description' => 'Reviews post content block-by-block and adds Notes with suggestions.', ); } @@ -46,20 +46,20 @@ public function register(): void { } /** - * Review_Notes_Ability test case. + * Editorial_Notes Ability test case. * * @since 0.4.0 */ -class Review_NotesTest extends WP_UnitTestCase { +class Editorial_NotesTest extends WP_UnitTestCase { use Guidelines_CPT_Helpers; /** - * Review_Notes_Ability instance. + * Editorial_Notes Ability instance. * * @since 0.4.0 * - * @var \WordPress\AI\Abilities\Review_Notes\Review_Notes + * @var \WordPress\AI\Abilities\Editorial_Notes\Editorial_Notes */ private $ability; @@ -68,7 +68,7 @@ class Review_NotesTest extends WP_UnitTestCase { * * @since 0.4.0 * - * @var \WordPress\AI\Tests\Integration\Includes\Abilities\Test_Review_Notes_Experiment + * @var \WordPress\AI\Tests\Integration\Includes\Abilities\Test_Editorial_Notes_Experiment */ private $experiment; @@ -80,9 +80,9 @@ class Review_NotesTest extends WP_UnitTestCase { public function setUp(): void { parent::setUp(); - $this->experiment = new Test_Review_Notes_Experiment(); - $this->ability = new Review_Notes( - 'ai/review-notes', + $this->experiment = new Test_Editorial_Notes_Experiment(); + $this->ability = new Editorial_Notes( + 'ai/editorial-notes', array( 'label' => $this->experiment->get_label(), 'description' => $this->experiment->get_description(), @@ -211,12 +211,12 @@ public function test_execute_callback_with_valid_input() { */ public function test_execute_callback_returns_empty_suggestions_when_no_issues() { // Create a partial mock that overrides generate_review to return empty. - $mock = $this->getMockBuilder( Review_Notes::class ) + $mock = $this->getMockBuilder( Editorial_Notes::class ) ->setConstructorArgs( array( - 'ai/review-notes', + 'ai/editorial-notes', array( - 'label' => 'AI Review Notes', + 'label' => 'AI Editorial Notes', 'description' => 'Test', ), ) @@ -258,12 +258,12 @@ public function test_execute_callback_returns_suggestions_array() { ), ); - $mock = $this->getMockBuilder( Review_Notes::class ) + $mock = $this->getMockBuilder( Editorial_Notes::class ) ->setConstructorArgs( array( - 'ai/review-notes', + 'ai/editorial-notes', array( - 'label' => 'AI Review Notes', + 'label' => 'AI Editorial Notes', 'description' => 'Test', ), ) diff --git a/tests/Integration/Includes/Abilities/Refine_NotesTest.php b/tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php similarity index 95% rename from tests/Integration/Includes/Abilities/Refine_NotesTest.php rename to tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php index 6553cf1d4..13c59da5d 100644 --- a/tests/Integration/Includes/Abilities/Refine_NotesTest.php +++ b/tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php @@ -1,6 +1,6 @@ 'Refine from Notes', + 'label' => 'Editorial Updates', 'description' => 'Refines block content based on editorial notes.', ); } @@ -50,21 +50,21 @@ public function register(): void { } /** - * Refine_Notes Ability test case. + * Editorial_Updates Ability test case. * * @since 0.8.0 * * @group abilities - * @group refine-notes + * @group editorial-updates */ -class Refine_NotesTest extends WP_UnitTestCase { +class Editorial_UpdatesTest extends WP_UnitTestCase { /** - * Refine_Notes Ability instance. + * Editorial_Updates Ability instance. * * @since 0.8.0 * - * @var Refine_Notes + * @var Editorial_Updates */ private $ability; @@ -73,7 +73,7 @@ class Refine_NotesTest extends WP_UnitTestCase { * * @since 0.8.0 * - * @var Test_Refine_Notes_Experiment + * @var Test_Editorial_Updates_Experiment */ private $experiment; @@ -85,9 +85,9 @@ class Refine_NotesTest extends WP_UnitTestCase { public function setUp(): void { parent::setUp(); - $this->experiment = new Test_Refine_Notes_Experiment(); - $this->ability = new Refine_Notes( - 'ai/refine-notes', + $this->experiment = new Test_Editorial_Updates_Experiment(); + $this->ability = new Editorial_Updates( + 'ai/editorial-updates', array( 'label' => $this->experiment->get_label(), 'description' => $this->experiment->get_description(), @@ -437,12 +437,12 @@ public function test_execute_callback_filters_non_string_notes() { * @since 0.8.0 */ public function test_execute_callback_returns_refined_text() { - $mock = $this->getMockBuilder( Refine_Notes::class ) + $mock = $this->getMockBuilder( Editorial_Updates::class ) ->setConstructorArgs( array( - 'ai/refine-notes', + 'ai/editorial-updates', array( - 'label' => 'Refine from Notes', + 'label' => 'Editorial Updates', 'description' => 'Test', ), ) @@ -473,12 +473,12 @@ public function test_execute_callback_returns_refined_text() { * @since 0.8.0 */ public function test_execute_callback_propagates_wp_error_from_generate_refinement() { - $mock = $this->getMockBuilder( Refine_Notes::class ) + $mock = $this->getMockBuilder( Editorial_Updates::class ) ->setConstructorArgs( array( - 'ai/refine-notes', + 'ai/editorial-updates', array( - 'label' => 'Refine from Notes', + 'label' => 'Editorial Updates', 'description' => 'Test', ), ) diff --git a/tests/Integration/Includes/Experiment_LoaderTest.php b/tests/Integration/Includes/Experiment_LoaderTest.php index c90b64dbb..508c005da 100644 --- a/tests/Integration/Includes/Experiment_LoaderTest.php +++ b/tests/Integration/Includes/Experiment_LoaderTest.php @@ -155,8 +155,8 @@ public function test_register_features() { 'Image generation experiment should be registered' ); $this->assertTrue( - $this->registry->has_feature( 'review-notes' ), - 'Review Notes experiment should be registered' + $this->registry->has_feature( 'editorial-notes' ), + 'Editorial Notes experiment should be registered' ); $this->assertTrue( $this->registry->has_feature( 'summarization' ), @@ -187,9 +187,9 @@ public function test_register_features() { $this->assertEquals( 'image-generation', $image_feature->get_id() ); $this->assertEquals( Experiment_Category::OTHER, $image_feature->get_category() ); - $review_notes_experiment = $this->registry->get_feature( 'review-notes' ); - $this->assertNotNull( $review_notes_experiment, 'Review Notes experiment should exist' ); - $this->assertEquals( 'review-notes', $review_notes_experiment->get_id() ); + $editorial_notes_experiment = $this->registry->get_feature( 'editorial-notes' ); + $this->assertNotNull( $editorial_notes_experiment, 'Editorial Notes experiment should exist' ); + $this->assertEquals( 'editorial-notes', $editorial_notes_experiment->get_id() ); $summarization_experiment = $this->registry->get_feature( 'summarization' ); $this->assertNotNull( $summarization_experiment, 'Summarization experiment should exist' ); diff --git a/tests/Integration/Includes/Experiments/Review_Notes/Review_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php similarity index 87% rename from tests/Integration/Includes/Experiments/Review_Notes/Review_NotesTest.php rename to tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php index 1369f235a..534a48d9e 100644 --- a/tests/Integration/Includes/Experiments/Review_Notes/Review_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php @@ -1,30 +1,30 @@ init(); - $experiment = $registry->get_feature( 'review-notes' ); - $this->assertInstanceOf( Review_Notes::class, $experiment, 'Review_Notes experiment should be registered in the registry.' ); + $experiment = $registry->get_feature( 'editorial-notes' ); + $this->assertInstanceOf( Editorial_Notes::class, $experiment, 'Editorial_Notes experiment should be registered in the registry.' ); $this->experiment = $experiment; } @@ -64,7 +64,7 @@ public function setUp(): void { public function tearDown(): void { wp_set_current_user( 0 ); delete_option( 'wpai_features_enabled' ); - delete_option( 'wpai_feature_review-notes_enabled' ); + delete_option( 'wpai_feature_editorial-notes_enabled' ); delete_option( 'wp_ai_client_provider_credentials' ); remove_filter( 'wpai_pre_has_valid_credentials_check', '__return_true' ); parent::tearDown(); @@ -80,8 +80,8 @@ public function tearDown(): void { * @since 0.4.0 */ public function test_experiment_registration() { - $this->assertEquals( 'review-notes', $this->experiment->get_id() ); - $this->assertEquals( 'Review Notes', $this->experiment->get_label() ); + $this->assertEquals( 'editorial-notes', $this->experiment->get_id() ); + $this->assertEquals( 'Editorial Notes', $this->experiment->get_label() ); $this->assertTrue( $this->experiment->is_enabled() ); } diff --git a/tests/Integration/Includes/Experiments/Refine_Notes/Refine_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php similarity index 74% rename from tests/Integration/Includes/Experiments/Refine_Notes/Refine_NotesTest.php rename to tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php index d25e70993..14c4e694b 100644 --- a/tests/Integration/Includes/Experiments/Refine_Notes/Refine_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php @@ -1,31 +1,31 @@ init(); @@ -51,8 +51,8 @@ public function setUp(): void { $loader = new Loader( $registry ); $loader->init(); - $experiment = $registry->get_feature( 'refine-notes' ); - $this->assertInstanceOf( Refine_Notes::class, $experiment, 'Refine_Notes experiment should be registered in the registry.' ); + $experiment = $registry->get_feature( 'editorial-updates' ); + $this->assertInstanceOf( Editorial_Updates::class, $experiment, 'Editorial_Updates experiment should be registered in the registry.' ); $this->experiment = $experiment; } @@ -65,7 +65,7 @@ public function setUp(): void { public function tearDown(): void { wp_set_current_user( 0 ); delete_option( 'wpai_features_enabled' ); - delete_option( 'wpai_feature_refine-notes_enabled' ); + delete_option( 'wpai_feature_editorial-updates_enabled' ); remove_filter( 'wpai_pre_has_valid_credentials_check', '__return_true' ); remove_all_filters( 'wpai_default_feature_classes' ); parent::tearDown(); @@ -81,8 +81,8 @@ public function tearDown(): void { * @since 0.8.0 */ public function test_experiment_registration() { - $this->assertEquals( 'refine-notes', $this->experiment->get_id() ); - $this->assertEquals( 'Refine from Notes', $this->experiment->get_label() ); + $this->assertEquals( 'editorial-updates', $this->experiment->get_id() ); + $this->assertEquals( 'Editorial Updates', $this->experiment->get_label() ); $this->assertTrue( $this->experiment->is_enabled() ); } diff --git a/tests/e2e-request-mocking/e2e-request-mocking.php b/tests/e2e-request-mocking/e2e-request-mocking.php index 14e073fbc..c0dc9aa1e 100644 --- a/tests/e2e-request-mocking/e2e-request-mocking.php +++ b/tests/e2e-request-mocking/e2e-request-mocking.php @@ -66,11 +66,11 @@ function ai_e2e_test_request_mocking( $preempt, $parsed_args, $url ) { if ( str_contains( $url, 'https://api.openai.com/v1/responses' ) ) { $body = $parsed_args['body'] ?? ''; - // Route review-notes and refine-notes requests to their own fixture. + // Route editorial-notes and editorial-updates requests to their own fixture. if ( is_string( $body ) && str_contains( $body, 'Category guidance by block type' ) ) { - $response = file_get_contents( __DIR__ . '/responses/OpenAI/review-notes-responses.json' ); + $response = file_get_contents( __DIR__ . '/responses/OpenAI/editorial-notes-responses.json' ); } elseif ( is_string( $body ) && str_contains( $body, 'You are an editorial assistant for WordPress. Your task is to update a single block' ) ) { - $response = file_get_contents( __DIR__ . '/responses/OpenAI/refine-notes-responses.json' ); + $response = file_get_contents( __DIR__ . '/responses/OpenAI/editorial-updates-responses.json' ); } elseif ( is_string( $body ) && str_contains( $body, 'content taxonomy assistant' ) ) { // Route content-classification requests to their own fixture. $response = file_get_contents( __DIR__ . '/responses/OpenAI/content-classification-responses.json' ); @@ -85,11 +85,11 @@ function ai_e2e_test_request_mocking( $preempt, $parsed_args, $url ) { if ( str_contains( $url, 'https://api.openai.com/v1/chat/completions' ) ) { $body = $parsed_args['body'] ?? ''; - // Route review-notes and refine-notes requests to their own fixture. + // Route editorial-notes and editorial-updates requests to their own fixture. if ( is_string( $body ) && str_contains( $body, 'Category guidance by block type' ) ) { - $response = file_get_contents( __DIR__ . '/responses/OpenAI/review-notes-completions.json' ); + $response = file_get_contents( __DIR__ . '/responses/OpenAI/editorial-notes-completions.json' ); } elseif ( is_string( $body ) && str_contains( $body, 'You are an editorial assistant for WordPress. Your task is to update a single block' ) ) { - $response = file_get_contents( __DIR__ . '/responses/OpenAI/refine-notes-completions.json' ); + $response = file_get_contents( __DIR__ . '/responses/OpenAI/editorial-updates-completions.json' ); } elseif ( is_string( $body ) && str_contains( $body, 'content taxonomy assistant' ) ) { // Route content-classification requests to their own fixture. $response = file_get_contents( __DIR__ . '/responses/OpenAI/content-classification-completions.json' ); diff --git a/tests/e2e-request-mocking/responses/OpenAI/review-notes-completions.json b/tests/e2e-request-mocking/responses/OpenAI/editorial-notes-completions.json similarity index 100% rename from tests/e2e-request-mocking/responses/OpenAI/review-notes-completions.json rename to tests/e2e-request-mocking/responses/OpenAI/editorial-notes-completions.json diff --git a/tests/e2e-request-mocking/responses/OpenAI/review-notes-responses.json b/tests/e2e-request-mocking/responses/OpenAI/editorial-notes-responses.json similarity index 100% rename from tests/e2e-request-mocking/responses/OpenAI/review-notes-responses.json rename to tests/e2e-request-mocking/responses/OpenAI/editorial-notes-responses.json diff --git a/tests/e2e-request-mocking/responses/OpenAI/refine-notes-completions.json b/tests/e2e-request-mocking/responses/OpenAI/editorial-updates-completions.json similarity index 94% rename from tests/e2e-request-mocking/responses/OpenAI/refine-notes-completions.json rename to tests/e2e-request-mocking/responses/OpenAI/editorial-updates-completions.json index f57d34927..cb4a52fbf 100644 --- a/tests/e2e-request-mocking/responses/OpenAI/refine-notes-completions.json +++ b/tests/e2e-request-mocking/responses/OpenAI/editorial-updates-completions.json @@ -1,5 +1,5 @@ { - "id": "chatcmpl-RefineNotesTest", + "id": "chatcmpl-EditorialUpdatesTest", "object": "chat.completion", "created": 1772133006, "model": "gpt-4o-mini-2024-07-18", diff --git a/tests/e2e-request-mocking/responses/OpenAI/refine-notes-responses.json b/tests/e2e-request-mocking/responses/OpenAI/editorial-updates-responses.json similarity index 94% rename from tests/e2e-request-mocking/responses/OpenAI/refine-notes-responses.json rename to tests/e2e-request-mocking/responses/OpenAI/editorial-updates-responses.json index 8e329f22f..3db4ac58d 100644 --- a/tests/e2e-request-mocking/responses/OpenAI/refine-notes-responses.json +++ b/tests/e2e-request-mocking/responses/OpenAI/editorial-updates-responses.json @@ -1,5 +1,5 @@ { - "id": "resp_MockRefineNotes", + "id": "resp_MockEditorialUpdates", "object": "response", "created_at": 1772806547, "status": "completed", @@ -17,7 +17,7 @@ "model": "gpt-4o-mini-2024-07-18", "output": [ { - "id": "msg_MockRefineNotesMsg", + "id": "msg_MockEditorialUpdatesMsg", "type": "message", "status": "completed", "content": [ diff --git a/tests/e2e/specs/experiments/review-notes.spec.js b/tests/e2e/specs/experiments/editorial-notes.spec.js similarity index 85% rename from tests/e2e/specs/experiments/review-notes.spec.js rename to tests/e2e/specs/experiments/editorial-notes.spec.js index eee97b906..01e867a62 100644 --- a/tests/e2e/specs/experiments/review-notes.spec.js +++ b/tests/e2e/specs/experiments/editorial-notes.spec.js @@ -13,30 +13,30 @@ import { enableExperiment, } from '../../utils/helpers'; -const EXPERIMENT_LABEL = 'Review Notes'; +const EXPERIMENT_LABEL = 'Editorial Notes'; -test.describe( 'AI Review Notes Experiment', () => { +test.describe( 'AI Editorial Notes Experiment', () => { test.beforeEach( async ( { admin, page } ) => { // Globally turn on Experiments. await enableExperiments( admin, page ); - // Enable the Review Notes Experiment. + // Enable the Editorial Notes Experiment. await enableExperiment( admin, page, EXPERIMENT_LABEL ); } ); - test( 'Shows the "Generate Review Notes" button in the post editor sidebar', async ( { + test( 'Shows the "Generate Editorial Notes" button in the post editor sidebar', async ( { admin, editor, page, } ) => { - await admin.createNewPost( { title: 'Review Notes Test' } ); + await admin.createNewPost( { title: 'Editorial Notes Test' } ); // Ensure the sidebar is visible. await editor.openDocumentSettingsSidebar(); // The button should be visible in the post status info panel. await expect( - page.getByRole( 'button', { name: 'Generate Review Notes' } ) + page.getByRole( 'button', { name: 'Generate Editorial Notes' } ) ).toBeVisible(); } ); @@ -45,7 +45,7 @@ test.describe( 'AI Review Notes Experiment', () => { editor, page, } ) => { - await admin.createNewPost( { title: 'Review Notes Test' } ); + await admin.createNewPost( { title: 'Editorial Notes Test' } ); // Add reviewable blocks. await editor.insertBlock( { @@ -62,7 +62,7 @@ test.describe( 'AI Review Notes Experiment', () => { // The button should be visible in the block toolbar. await expect( page.locator( 'button', { - hasText: 'Generate Review Note', + hasText: 'Generate Editorial Note', } ) ).toBeVisible(); } ); @@ -93,7 +93,7 @@ test.describe( 'AI Review Notes Experiment', () => { // Run review. await page - .getByRole( 'button', { name: 'Generate Review Notes' } ) + .getByRole( 'button', { name: 'Generate Editorial Notes' } ) .click(); // Wait for completion and check for suggestion count feedback. @@ -123,7 +123,7 @@ test.describe( 'AI Review Notes Experiment', () => { } ); // Run review on the single block. - await editor.clickBlockOptionsMenuItem( 'Generate Review Note' ); + await editor.clickBlockOptionsMenuItem( 'Generate Editorial Note' ); // Wait for completion and check for suggestion count feedback. await expect( @@ -145,7 +145,7 @@ test.describe( 'AI Review Notes Experiment', () => { await editor.openDocumentSettingsSidebar(); const reviewButton = page.getByRole( 'button', { - name: 'Generate Review Notes', + name: 'Generate Editorial Notes', } ); await expect( reviewButton ).toBeVisible(); @@ -175,7 +175,7 @@ test.describe( 'AI Review Notes Experiment', () => { await editor.openDocumentSettingsSidebar(); await expect( - page.getByRole( 'button', { name: 'Generate Review Notes' } ) + page.getByRole( 'button', { name: 'Generate Editorial Notes' } ) ).toHaveCount( 0 ); // Add reviewable blocks. @@ -193,7 +193,7 @@ test.describe( 'AI Review Notes Experiment', () => { // The button should not be visible in the block toolbar. await expect( page.locator( 'button', { - hasText: 'Generate Review Note', + hasText: 'Generate Editorial Note', } ) ).not.toBeVisible(); } ); @@ -203,7 +203,7 @@ test.describe( 'AI Review Notes Experiment', () => { editor, page, } ) => { - // Disable the Review Notes Experiment. + // Disable the Editorial Notes Experiment. await disableExperiment( admin, page, EXPERIMENT_LABEL ); // Create a new post and verify button is absent. @@ -213,7 +213,7 @@ test.describe( 'AI Review Notes Experiment', () => { await editor.openDocumentSettingsSidebar(); await expect( - page.getByRole( 'button', { name: 'Generate Review Notes' } ) + page.getByRole( 'button', { name: 'Generate Editorial Notes' } ) ).toHaveCount( 0 ); // Add reviewable blocks. @@ -231,7 +231,7 @@ test.describe( 'AI Review Notes Experiment', () => { // The button should be visible in the block toolbar. await expect( page.locator( 'button', { - hasText: 'Generate Review Note', + hasText: 'Generate Editorial Note', } ) ).not.toBeVisible(); } ); diff --git a/tests/e2e/specs/experiments/refine-notes.spec.js b/tests/e2e/specs/experiments/editorial-updates.spec.js similarity index 95% rename from tests/e2e/specs/experiments/refine-notes.spec.js rename to tests/e2e/specs/experiments/editorial-updates.spec.js index e1e6c004b..ba9cc5e84 100644 --- a/tests/e2e/specs/experiments/refine-notes.spec.js +++ b/tests/e2e/specs/experiments/editorial-updates.spec.js @@ -13,9 +13,9 @@ import { enableExperiment, } from '../../utils/helpers'; -const EXPERIMENT_LABEL = 'Refine from Notes'; +const EXPERIMENT_LABEL = 'Editorial Updates'; -test.describe( 'Refine from Notes Experiment', () => { +test.describe( 'Editorial Updates Experiment', () => { test.beforeEach( async ( { admin, page } ) => { // Globally turn on Experiments. await enableExperiments( admin, page ); @@ -36,7 +36,7 @@ test.describe( 'Refine from Notes Experiment', () => { // The button should NOT be visible initially since there are no notes. await expect( - page.getByRole( 'button', { name: 'Refine from Notes' } ) + page.getByRole( 'button', { name: 'Editorial Updates' } ) ).toBeHidden(); } ); @@ -170,7 +170,7 @@ test.describe( 'Refine from Notes Experiment', () => { // The button should be visible now. const refineButton = page.getByRole( 'button', { - name: 'Refine from Notes', + name: 'Editorial Updates', } ); await expect( refineButton ).toBeVisible( { timeout: 10000 } ); @@ -183,7 +183,7 @@ test.describe( 'Refine from Notes Experiment', () => { const url = decodeURIComponent( req.url() ); return ( url.includes( 'wp-abilities' ) && - url.includes( 'refine-notes' ) + url.includes( 'editorial-updates' ) ); }, { timeout: 15000 } @@ -237,7 +237,7 @@ test.describe( 'Refine from Notes Experiment', () => { await editor.openDocumentSettingsSidebar(); await expect( - page.getByRole( 'button', { name: 'Refine from Notes' } ) + page.getByRole( 'button', { name: 'Editorial Updates' } ) ).toHaveCount( 0 ); } ); @@ -256,7 +256,7 @@ test.describe( 'Refine from Notes Experiment', () => { await editor.openDocumentSettingsSidebar(); await expect( - page.getByRole( 'button', { name: 'Refine from Notes' } ) + page.getByRole( 'button', { name: 'Editorial Updates' } ) ).toHaveCount( 0 ); } ); @@ -358,7 +358,7 @@ test.describe( 'Refine from Notes Experiment', () => { await page.route( ( url ) => url.href.includes( 'wp-abilities' ) && - url.href.includes( 'refine-notes' ), + url.href.includes( 'editorial-updates' ), async ( route ) => { await route.fulfill( { status: 500, @@ -390,7 +390,7 @@ test.describe( 'Refine from Notes Experiment', () => { }, noteId ); const refineButton = page.getByRole( 'button', { - name: 'Refine from Notes', + name: 'Editorial Updates', } ); await expect( refineButton ).toBeVisible( { timeout: 10000 } ); @@ -416,7 +416,7 @@ test.describe( 'Refine from Notes Experiment', () => { const decoded = decodeURIComponent( url ); if ( decoded.includes( 'wp-abilities' ) && - decoded.includes( 'refine-notes' ) + decoded.includes( 'editorial-updates' ) ) { return Promise.resolve( new Response( @@ -439,7 +439,7 @@ test.describe( 'Refine from Notes Experiment', () => { await refineButton.click(); // Button should return to idle state after the error. - await expect( refineButton ).toHaveText( 'Refine from Notes', { + await expect( refineButton ).toHaveText( 'Editorial Updates', { timeout: 15000, } ); @@ -450,7 +450,7 @@ test.describe( 'Refine from Notes Experiment', () => { const notices = window.wp.data .select( 'core/notices' ) .getNotices(); - return notices.find( ( n ) => n.id === 'wpai_refine_notes_error' ); + return notices.find( ( n ) => n.id === 'wpai_editorial_updates_error' ); } ); expect( errorNotice ).toBeDefined(); expect( errorNotice.status ).toBe( 'error' ); diff --git a/webpack.config.js b/webpack.config.js index 153d26838..c3926bb8b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -63,14 +63,14 @@ module.exports = { 'src/experiments/meta-description', 'index.tsx' ), - 'experiments/review-notes': path.resolve( + 'experiments/editorial-notes': path.resolve( process.cwd(), - 'src/experiments/review-notes', + 'src/experiments/editorial-notes', 'index.tsx' ), - 'experiments/refine-notes': path.resolve( + 'experiments/editorial-updates': path.resolve( process.cwd(), - 'src/experiments/refine-notes', + 'src/experiments/editorial-updates', 'index.tsx' ), 'experiments/summarization': path.resolve( From e4ff18193b362970cf90a64a60da4b4edead2d4e Mon Sep 17 00:00:00 2001 From: Nischay Date: Fri, 8 May 2026 23:03:02 +0530 Subject: [PATCH 02/12] style: fix line-length formatting in plugin and e2e spec --- .../editorial-notes/components/EditorialNotesPlugin.tsx | 5 ++++- tests/e2e/specs/experiments/editorial-updates.spec.js | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx b/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx index c1bf38314..eadf94a96 100644 --- a/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx +++ b/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx @@ -24,7 +24,10 @@ import { commentContent } from '@wordpress/icons'; * Internal dependencies */ import { REVIEWABLE_BLOCK_TYPES } from '../../../utils/notes'; -import { useEditorialBlock, useEditorialNotes } from '../hooks/useEditorialNotes'; +import { + useEditorialBlock, + useEditorialNotes, +} from '../hooks/useEditorialNotes'; /** * EditorialNotesPlugin component. diff --git a/tests/e2e/specs/experiments/editorial-updates.spec.js b/tests/e2e/specs/experiments/editorial-updates.spec.js index ba9cc5e84..1b3ec20fd 100644 --- a/tests/e2e/specs/experiments/editorial-updates.spec.js +++ b/tests/e2e/specs/experiments/editorial-updates.spec.js @@ -450,7 +450,9 @@ test.describe( 'Editorial Updates Experiment', () => { const notices = window.wp.data .select( 'core/notices' ) .getNotices(); - return notices.find( ( n ) => n.id === 'wpai_editorial_updates_error' ); + return notices.find( + ( n ) => n.id === 'wpai_editorial_updates_error' + ); } ); expect( errorNotice ).toBeDefined(); expect( errorNotice.status ).toBe( 'error' ); From aa96086343ab0766d9a324b5863acbb1d9539c07 Mon Sep 17 00:00:00 2001 From: Nischay Date: Fri, 8 May 2026 23:58:32 +0530 Subject: [PATCH 03/12] style: improve formatting in suggestions_schema method --- .../Abilities/Editorial_Notes/Editorial_Notes.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/includes/Abilities/Editorial_Notes/Editorial_Notes.php b/includes/Abilities/Editorial_Notes/Editorial_Notes.php index da4fd91c4..338864f5c 100644 --- a/includes/Abilities/Editorial_Notes/Editorial_Notes.php +++ b/includes/Abilities/Editorial_Notes/Editorial_Notes.php @@ -259,24 +259,22 @@ protected function meta(): array { */ protected function suggestions_schema(): array { return array( - 'type' => 'object', - 'properties' => array( + 'type' => 'object', + 'properties' => array( 'suggestions' => array( 'type' => 'array', 'items' => array( - 'type' => 'object', - 'properties' => array( + 'type' => 'object', + 'properties' => array( 'review_type' => array( 'type' => 'string' ), 'text' => array( 'type' => 'string' ), 'priority' => array( 'type' => 'integer' ), ), - 'required' => array( 'review_type', 'text', 'priority' ), - 'additionalProperties' => false, + 'required' => array( 'review_type', 'text', 'priority' ), ), ), ), - 'required' => array( 'suggestions' ), - 'additionalProperties' => false, + 'required' => array( 'suggestions' ), ); } From 49ccc36c6c611cfface90a104468fcdd04ff61c0 Mon Sep 17 00:00:00 2001 From: Nischay Date: Sat, 9 May 2026 20:56:35 +0530 Subject: [PATCH 04/12] test: add V1_0_0 upgrade tests and cover register_abilities in experiment tests --- .../Editorial_Notes/Editorial_Notes.php | 4 +- .../Editorial_Updates/Editorial_Updates.php | 4 +- .../Includes/Admin/Upgrades/V1_0_0Test.php | 211 ++++++++++++++++++ .../Editorial_Notes/Editorial_NotesTest.php | 12 + .../Editorial_UpdatesTest.php | 12 + 5 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 tests/Integration/Includes/Admin/Upgrades/V1_0_0Test.php diff --git a/includes/Abilities/Editorial_Notes/Editorial_Notes.php b/includes/Abilities/Editorial_Notes/Editorial_Notes.php index 338864f5c..dc8184834 100644 --- a/includes/Abilities/Editorial_Notes/Editorial_Notes.php +++ b/includes/Abilities/Editorial_Notes/Editorial_Notes.php @@ -11,7 +11,7 @@ use WP_Error; use WordPress\AI\Abstracts\Abstract_Ability; -use WordPress\AI\Experiments\Review_Notes\Review_Notes as Review_Notes_Experiment; +use WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes as Editorial_Notes_Experiment; use function WordPress\AI\normalize_content; @@ -372,7 +372,7 @@ private function get_prompt_builder( string $prompt, string $block_type ) { ->using_system_instruction( $this->get_system_instruction( null, array( 'block_name' => $block_type ) ) ) ->as_json_response( $this->suggestions_schema() ); - $prompt_builder = $this->set_provider_model_preference( $prompt_builder, Review_Notes_Experiment::class ); + $prompt_builder = $this->set_provider_model_preference( $prompt_builder, Editorial_Notes_Experiment::class ); return $this->ensure_text_generation_supported( $prompt_builder, diff --git a/includes/Abilities/Editorial_Updates/Editorial_Updates.php b/includes/Abilities/Editorial_Updates/Editorial_Updates.php index 0538d1067..0328cbada 100644 --- a/includes/Abilities/Editorial_Updates/Editorial_Updates.php +++ b/includes/Abilities/Editorial_Updates/Editorial_Updates.php @@ -11,7 +11,7 @@ use WP_Error; use WordPress\AI\Abstracts\Abstract_Ability; -use WordPress\AI\Experiments\Refine_Notes\Refine_Notes as Refine_Notes_Experiment; +use WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates as Editorial_Updates_Experiment; use function WordPress\AI\normalize_content; @@ -247,7 +247,7 @@ private function get_prompt_builder( string $prompt ) { $prompt_builder = wp_ai_client_prompt( $prompt ) ->using_system_instruction( $this->get_system_instruction() ); - $prompt_builder = $this->set_provider_model_preference( $prompt_builder, Refine_Notes_Experiment::class ); + $prompt_builder = $this->set_provider_model_preference( $prompt_builder, Editorial_Updates_Experiment::class ); return $this->ensure_text_generation_supported( $prompt_builder, diff --git a/tests/Integration/Includes/Admin/Upgrades/V1_0_0Test.php b/tests/Integration/Includes/Admin/Upgrades/V1_0_0Test.php new file mode 100644 index 000000000..7bd65c0c7 --- /dev/null +++ b/tests/Integration/Includes/Admin/Upgrades/V1_0_0Test.php @@ -0,0 +1,211 @@ +run(); + + $this->assertEquals( '1', get_option( 'wpai_feature_editorial-notes_enabled' ) ); + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_review-notes_enabled' ), + 'Old review-notes option should be deleted after migration' + ); + } + + /** + * Tests that run() migrates refine-notes option to editorial-updates. + * + * @since x.x.x + */ + public function test_run_migrates_refine_notes_to_editorial_updates() { + update_option( 'wpai_feature_refine-notes_enabled', '1' ); + + ( new V1_0_0( '' ) )->run(); + + $this->assertEquals( '1', get_option( 'wpai_feature_editorial-updates_enabled' ) ); + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_refine-notes_enabled' ), + 'Old refine-notes option should be deleted after migration' + ); + } + + /** + * Tests that run() migrates both options in a single pass. + * + * @since x.x.x + */ + public function test_run_migrates_both_options() { + update_option( 'wpai_feature_review-notes_enabled', '1' ); + update_option( 'wpai_feature_refine-notes_enabled', '1' ); + + ( new V1_0_0( '' ) )->run(); + + $this->assertEquals( '1', get_option( 'wpai_feature_editorial-notes_enabled' ) ); + $this->assertEquals( '1', get_option( 'wpai_feature_editorial-updates_enabled' ) ); + $this->assertNull( $this->get_option_from_db( 'wpai_feature_review-notes_enabled' ) ); + $this->assertNull( $this->get_option_from_db( 'wpai_feature_refine-notes_enabled' ) ); + } + + /** + * Tests that run() returns true on success. + * + * @since x.x.x + */ + public function test_run_returns_success_after_migration() { + $result = ( new V1_0_0( '' ) )->run(); + + $this->assertTrue( $result ); + } + + /** + * Tests that run() skips when version is already at target. + * + * @since x.x.x + */ + public function test_run_skips_when_version_already_current() { + update_option( 'wpai_feature_review-notes_enabled', '1' ); + update_option( 'wpai_feature_refine-notes_enabled', '1' ); + + ( new V1_0_0( '1.0.0' ) )->run(); + + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_editorial-notes_enabled' ), + 'Should not migrate when version is already current' + ); + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_editorial-updates_enabled' ), + 'Should not migrate when version is already current' + ); + } + + /** + * Tests that run() does nothing on fresh install with no old options. + * + * @since x.x.x + */ + public function test_run_does_nothing_on_fresh_install() { + ( new V1_0_0( '' ) )->run(); + + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_editorial-notes_enabled' ), + 'editorial-notes option should not be written on fresh install' + ); + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_editorial-updates_enabled' ), + 'editorial-updates option should not be written on fresh install' + ); + } + + /** + * Tests that run() skips migration when new option already has a value. + * + * @since x.x.x + */ + public function test_run_skips_when_new_option_already_set() { + update_option( 'wpai_feature_review-notes_enabled', '1' ); + update_option( 'wpai_feature_editorial-notes_enabled', 'already-set' ); + + ( new V1_0_0( '' ) )->run(); + + $this->assertEquals( + 'already-set', + get_option( 'wpai_feature_editorial-notes_enabled' ), + 'New option should not be overwritten' + ); + $this->assertEquals( + '1', + get_option( 'wpai_feature_review-notes_enabled' ), + 'Old option should remain when migration is skipped' + ); + } + + /** + * Tests that run() skips migration when old option is an empty string. + * + * @since x.x.x + */ + public function test_run_skips_empty_old_values() { + update_option( 'wpai_feature_review-notes_enabled', '' ); + + ( new V1_0_0( '' ) )->run(); + + $this->assertNull( + $this->get_option_from_db( 'wpai_feature_editorial-notes_enabled' ), + 'New option should not be set when old option is empty string' + ); + } + + /** + * Returns the raw option value directly from the database, bypassing all filters. + * + * Returns null if the option row does not exist. + * + * @since x.x.x + * + * @param string $option_name The option name to look up. + * @return string|null The raw value, or null if the row is absent. + */ + private function get_option_from_db( string $option_name ): ?string { + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", + $option_name + ) + ); + } +} diff --git a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php index 534a48d9e..6b6827a5c 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php @@ -101,6 +101,18 @@ public function test_hooks_are_registered() { ); } + /** + * Tests that register_abilities() registers the ai/editorial-notes ability. + * + * @since x.x.x + */ + public function test_register_abilities_registers_editorial_notes_ability() { + do_action( 'wp_abilities_api_init' ); + + $ability = wp_get_ability( 'ai/editorial-notes' ); + $this->assertNotNull( $ability, 'ai/editorial-notes ability should be registered' ); + } + /** * Tests that the ai_note comment meta is registered with show_in_rest. * diff --git a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php index 14c4e694b..025971b1b 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php @@ -106,4 +106,16 @@ public function test_hooks_are_registered() { 'enqueue_block_editor_assets action should be registered' ); } + + /** + * Tests that register_abilities() registers the ai/editorial-updates ability. + * + * @since x.x.x + */ + public function test_register_abilities_registers_editorial_updates_ability() { + do_action( 'wp_abilities_api_init' ); + + $ability = wp_get_ability( 'ai/editorial-updates' ); + $this->assertNotNull( $ability, 'ai/editorial-updates ability should be registered' ); + } } From d1fff9deccf0594f925683ef7dc1bfb1add9621f Mon Sep 17 00:00:00 2001 From: Nischay Date: Sat, 9 May 2026 21:26:16 +0530 Subject: [PATCH 05/12] test(experiments): add register_abilities ability-registration coverage --- .../Experiments/Editorial_Notes/Editorial_NotesTest.php | 2 ++ .../Experiments/Editorial_Updates/Editorial_UpdatesTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php index 6b6827a5c..a977fe685 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php @@ -107,6 +107,8 @@ public function test_hooks_are_registered() { * @since x.x.x */ public function test_register_abilities_registers_editorial_notes_ability() { + $this->setExpectedIncorrectUsage( 'WP_Abilities_Registry::register' ); + do_action( 'wp_abilities_api_init' ); $ability = wp_get_ability( 'ai/editorial-notes' ); diff --git a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php index 025971b1b..d652844d3 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php @@ -113,6 +113,8 @@ public function test_hooks_are_registered() { * @since x.x.x */ public function test_register_abilities_registers_editorial_updates_ability() { + $this->setExpectedIncorrectUsage( 'WP_Abilities_Registry::register' ); + do_action( 'wp_abilities_api_init' ); $ability = wp_get_ability( 'ai/editorial-updates' ); From 808573366b6c920a0468c245310e2c3d9126349e Mon Sep 17 00:00:00 2001 From: Nischay Date: Thu, 14 May 2026 12:08:19 +0530 Subject: [PATCH 06/12] fix(editorial-notes): address review feedback and improve coverage --- CHANGELOG.md | 4 --- .../Editorial_Notes/Editorial_Notes.php | 2 +- .../Editorial_Updates/Editorial_Updates.php | 2 +- .../components/EditorialNotesPlugin.tsx | 2 +- .../hooks/useEditorialNotes.ts | 4 +-- .../components/EditorialUpdatesPlugin.tsx | 4 +-- .../Abilities/Editorial_NotesTest.php | 23 ++++++++++++ .../Abilities/Editorial_UpdatesTest.php | 23 ++++++++++++ .../Editorial_Notes/Editorial_NotesTest.php | 35 +++++++++++++++++++ .../Editorial_UpdatesTest.php | 15 ++++++++ .../experiments/editorial-updates.spec.js | 12 +++---- 11 files changed, 109 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad2dd045..4b21a20c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,6 @@ All notable changes to this project will be documented in this file, per [the Keep a Changelog standard](http://keepachangelog.com/), and will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - TBD -### Changed -- Rename "Review Notes" experiment to "Editorial Notes" and "Refine from Notes" experiment to "Editorial Updates" ([#506](https://github.com/WordPress/ai/issues/506)). - ## [0.9.0] - 2026-05-07 ### Added - New Experiment: Comment Moderation to automatically moderate comments based on toxicity detection and sentiment analysis ([#155](https://github.com/WordPress/ai/pull/155), [#516](https://github.com/WordPress/ai/pull/516)). diff --git a/includes/Experiments/Editorial_Notes/Editorial_Notes.php b/includes/Experiments/Editorial_Notes/Editorial_Notes.php index 757ee02be..789cc9015 100644 --- a/includes/Experiments/Editorial_Notes/Editorial_Notes.php +++ b/includes/Experiments/Editorial_Notes/Editorial_Notes.php @@ -42,7 +42,7 @@ public static function get_id(): string { protected function load_metadata(): array { return array( 'label' => __( 'Editorial Notes', 'ai' ), - 'description' => __( 'Reviews post content block-by-block and adds Notes with suggestions for Accessibility, Readability, Grammar, and SEO. Requires an AI connector that includes support for text generation models.', 'ai' ), + 'description' => __( 'Adds editorial suggestions to posts block-by-block, covering Accessibility, Readability, Grammar, and SEO. Requires an AI connector that includes support for text generation models.', 'ai' ), 'category' => Experiment_Category::EDITOR, ); } diff --git a/includes/Experiments/Editorial_Updates/Editorial_Updates.php b/includes/Experiments/Editorial_Updates/Editorial_Updates.php index f304edd1a..41e72757a 100644 --- a/includes/Experiments/Editorial_Updates/Editorial_Updates.php +++ b/includes/Experiments/Editorial_Updates/Editorial_Updates.php @@ -44,7 +44,7 @@ public static function get_id(): string { protected function load_metadata(): array { return array( 'label' => __( 'Editorial Updates', 'ai' ), - 'description' => __( 'Analyze feedback that has been left via Notes and apply edits where needed.', 'ai' ), + 'description' => __( 'Applies pending editorial Notes to your content automatically. Requires an AI connector that includes support for text generation models.', 'ai' ), 'category' => Experiment_Category::EDITOR, ); } diff --git a/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx b/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx index eadf94a96..e419cdaa2 100644 --- a/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx +++ b/src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx @@ -58,7 +58,7 @@ export default function EditorialNotesPlugin() { ? reviewingLabel : __( 'Generate Editorial Notes', 'ai' ); const buttonDescription = __( - 'This will review the content of this post block-by-block, and create Notes attached to each block with suggestions.', + 'This analyzes the content of this post block-by-block and adds editorial Notes with suggestions on each block.', 'ai' ); diff --git a/src/experiments/editorial-notes/hooks/useEditorialNotes.ts b/src/experiments/editorial-notes/hooks/useEditorialNotes.ts index 542bf0f4f..0e587d89e 100644 --- a/src/experiments/editorial-notes/hooks/useEditorialNotes.ts +++ b/src/experiments/editorial-notes/hooks/useEditorialNotes.ts @@ -293,7 +293,7 @@ export function useEditorialBlock(): { setIsReviewing( true ); ( dispatch( noticesStore ) as any ).removeNotice( - 'ai_review_block_error' + 'ai_editorial_block_error' ); try { @@ -366,7 +366,7 @@ export function useEditorialBlock(): { ( dispatch( noticesStore ) as any ).createErrorNotice( error?.message ?? String( error ), { - id: 'ai_review_block_error', + id: 'ai_editorial_block_error', isDismissible: true, } ); diff --git a/src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx b/src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx index 093b89da7..9e378f216 100644 --- a/src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx +++ b/src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx @@ -36,10 +36,10 @@ export default function EditorialUpdatesPlugin() { progress, total ) - : __( 'Editorial Updates', 'ai' ); + : __( 'Apply Editorial Updates', 'ai' ); const buttonDescription = __( - 'Automatically updates blocks using unresolved feedback Notes.', + 'Automatically applies pending editorial Notes to update your content.', 'ai' ); diff --git a/tests/Integration/Includes/Abilities/Editorial_NotesTest.php b/tests/Integration/Includes/Abilities/Editorial_NotesTest.php index 369b55489..f45c203b3 100644 --- a/tests/Integration/Includes/Abilities/Editorial_NotesTest.php +++ b/tests/Integration/Includes/Abilities/Editorial_NotesTest.php @@ -643,4 +643,27 @@ public function test_get_system_instruction_includes_guidelines(): void { $this->assertStringContainsString( 'Keep sentences under 25 words.', $instruction ); } + // ------------------------------------------------------------------------- + // get_prompt_builder() + // ------------------------------------------------------------------------- + + /** + * Tests that get_prompt_builder() returns a WP_Error when no text generation model is available. + * + * @since x.x.x + */ + public function test_get_prompt_builder_returns_error_without_valid_model() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'get_prompt_builder' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->ability, 'Test prompt', 'core/paragraph' ); + + if ( is_wp_error( $result ) ) { + $this->assertEquals( 'unsupported_model', $result->get_error_code(), 'Error code should be unsupported_model when no provider is configured' ); + } else { + $this->assertIsObject( $result, 'Should return a prompt builder object when a model is available' ); + } + } + } diff --git a/tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php b/tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php index 13c59da5d..a871bc515 100644 --- a/tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php +++ b/tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php @@ -658,4 +658,27 @@ public function test_create_prompt_includes_multiple_notes() { $this->assertStringContainsString( 'First note', $prompt, 'Prompt should contain first note' ); $this->assertStringContainsString( 'Second note', $prompt, 'Prompt should contain second note' ); } + + // ------------------------------------------------------------------------- + // get_prompt_builder() + // ------------------------------------------------------------------------- + + /** + * Tests that get_prompt_builder() returns a WP_Error when no text generation model is available. + * + * @since x.x.x + */ + public function test_get_prompt_builder_returns_error_without_valid_model() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'get_prompt_builder' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->ability, 'Test prompt' ); + + if ( is_wp_error( $result ) ) { + $this->assertEquals( 'unsupported_model', $result->get_error_code(), 'Error code should be unsupported_model when no provider is configured' ); + } else { + $this->assertIsObject( $result, 'Should return a prompt builder object when a model is available' ); + } + } } diff --git a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php index a977fe685..649bc9d64 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php @@ -211,4 +211,39 @@ public function test_maybe_set_ai_author_returns_wp_error_unchanged() { $this->assertInstanceOf( \WP_Error::class, $result ); $this->assertEquals( 'test_error', $result->get_error_code() ); } + + // ------------------------------------------------------------------------- + // enqueue_assets() + // ------------------------------------------------------------------------- + + /** + * Tests that enqueue_assets() runs without error and attempts to enqueue the script. + * + * @since x.x.x + */ + public function test_enqueue_assets_runs_without_error() { + $this->experiment->enqueue_assets(); + + $this->assertTrue( true, 'enqueue_assets() should run without throwing an exception' ); + } + + // ------------------------------------------------------------------------- + // register_meta() auth_callback + // ------------------------------------------------------------------------- + + /** + * Tests that the ai_note meta auth_callback returns true for a user with edit_posts. + * + * @since x.x.x + */ + public function test_ai_note_meta_auth_callback_returns_true_for_editor() { + $user_id = self::factory()->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( $user_id ); + + $registered = get_registered_meta_keys( 'comment' ); + $callback = $registered['ai_note']['auth_callback'] ?? null; + + $this->assertIsCallable( $callback, 'auth_callback should be callable' ); + $this->assertTrue( $callback(), 'auth_callback should return true for a user with edit_posts' ); + } } diff --git a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php index d652844d3..c4d4103b4 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php @@ -120,4 +120,19 @@ public function test_register_abilities_registers_editorial_updates_ability() { $ability = wp_get_ability( 'ai/editorial-updates' ); $this->assertNotNull( $ability, 'ai/editorial-updates ability should be registered' ); } + + // ------------------------------------------------------------------------- + // enqueue_assets() + // ------------------------------------------------------------------------- + + /** + * Tests that enqueue_assets() runs without error and attempts to enqueue the script. + * + * @since x.x.x + */ + public function test_enqueue_assets_runs_without_error() { + $this->experiment->enqueue_assets(); + + $this->assertTrue( true, 'enqueue_assets() should run without throwing an exception' ); + } } diff --git a/tests/e2e/specs/experiments/editorial-updates.spec.js b/tests/e2e/specs/experiments/editorial-updates.spec.js index 1b3ec20fd..adcbc72e7 100644 --- a/tests/e2e/specs/experiments/editorial-updates.spec.js +++ b/tests/e2e/specs/experiments/editorial-updates.spec.js @@ -36,7 +36,7 @@ test.describe( 'Editorial Updates Experiment', () => { // The button should NOT be visible initially since there are no notes. await expect( - page.getByRole( 'button', { name: 'Editorial Updates' } ) + page.getByRole( 'button', { name: 'Apply Editorial Updates' } ) ).toBeHidden(); } ); @@ -170,7 +170,7 @@ test.describe( 'Editorial Updates Experiment', () => { // The button should be visible now. const refineButton = page.getByRole( 'button', { - name: 'Editorial Updates', + name: 'Apply Editorial Updates', } ); await expect( refineButton ).toBeVisible( { timeout: 10000 } ); @@ -237,7 +237,7 @@ test.describe( 'Editorial Updates Experiment', () => { await editor.openDocumentSettingsSidebar(); await expect( - page.getByRole( 'button', { name: 'Editorial Updates' } ) + page.getByRole( 'button', { name: 'Apply Editorial Updates' } ) ).toHaveCount( 0 ); } ); @@ -256,7 +256,7 @@ test.describe( 'Editorial Updates Experiment', () => { await editor.openDocumentSettingsSidebar(); await expect( - page.getByRole( 'button', { name: 'Editorial Updates' } ) + page.getByRole( 'button', { name: 'Apply Editorial Updates' } ) ).toHaveCount( 0 ); } ); @@ -390,7 +390,7 @@ test.describe( 'Editorial Updates Experiment', () => { }, noteId ); const refineButton = page.getByRole( 'button', { - name: 'Editorial Updates', + name: 'Apply Editorial Updates', } ); await expect( refineButton ).toBeVisible( { timeout: 10000 } ); @@ -439,7 +439,7 @@ test.describe( 'Editorial Updates Experiment', () => { await refineButton.click(); // Button should return to idle state after the error. - await expect( refineButton ).toHaveText( 'Editorial Updates', { + await expect( refineButton ).toHaveText( 'Apply Editorial Updates', { timeout: 15000, } ); From b4a4377228957d52e86c365c34bf254a07d7be8b Mon Sep 17 00:00:00 2001 From: Nischay Date: Thu, 14 May 2026 12:17:19 +0530 Subject: [PATCH 07/12] add provider status check and refactor notice handling --- .../hooks/useEditorialNotes.ts | 12 ++- .../hooks/useEditorialUpdates.ts | 14 ++-- src/utils/provider-status.ts | 74 +++++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/utils/provider-status.ts diff --git a/src/experiments/editorial-notes/hooks/useEditorialNotes.ts b/src/experiments/editorial-notes/hooks/useEditorialNotes.ts index 0e587d89e..966dca0ab 100644 --- a/src/experiments/editorial-notes/hooks/useEditorialNotes.ts +++ b/src/experiments/editorial-notes/hooks/useEditorialNotes.ts @@ -28,11 +28,13 @@ import { fetchAllNotesByStatus, buildContextWindow, } from '../../../utils/notes'; +import { ensureProvider } from '../../../utils/provider-status'; import { runAbility } from '../../../utils/run-ability'; const BLOCK_PLACEHOLDER = '[[BLOCK_GOES_HERE]]'; const BATCH_SIZE = 4; +const NOTICE_ID = 'ai_editorial_notes_error'; const NOTES_SIDEBAR_ID = 'edit-post/collab-sidebar'; interface BlockAttributes { @@ -171,14 +173,16 @@ export function useEditorialNotes(): { const [ lastRunCount, setLastRunCount ] = useState< number | null >( null ); const runReview = async () => { + if ( ! ensureProvider( NOTICE_ID ) ) { + return; + } + setIsReviewing( true ); setProgress( 0 ); setTotal( 0 ); setLastRunCount( null ); - ( dispatch( noticesStore ) as any ).removeNotice( - 'ai_editorial_notes_error' - ); + ( dispatch( noticesStore ) as any ).removeNotice( NOTICE_ID ); try { const postId = ( @@ -266,7 +270,7 @@ export function useEditorialNotes(): { ( dispatch( noticesStore ) as any ).createErrorNotice( error?.message ?? String( error ), { - id: 'ai_editorial_notes_error', + id: NOTICE_ID, isDismissible: true, } ); diff --git a/src/experiments/editorial-updates/hooks/useEditorialUpdates.ts b/src/experiments/editorial-updates/hooks/useEditorialUpdates.ts index 30e881d5e..82ebcc926 100644 --- a/src/experiments/editorial-updates/hooks/useEditorialUpdates.ts +++ b/src/experiments/editorial-updates/hooks/useEditorialUpdates.ts @@ -27,9 +27,11 @@ import { fetchAllNotesByStatus, buildContextWindow, } from '../../../utils/notes'; +import { ensureProvider } from '../../../utils/provider-status'; import { runAbility } from '../../../utils/run-ability'; const BLOCK_PLACEHOLDER = '[[BLOCK_GOES_HERE]]'; +const NOTICE_ID = 'wpai_editorial_updates_error'; interface BlockAttributes { content?: string; @@ -115,13 +117,15 @@ export function useEditorialUpdates(): { ); const runRefinement = async () => { + if ( ! ensureProvider( NOTICE_ID ) ) { + return; + } + setIsRefining( true ); setProgress( 0 ); setTotal( 0 ); - ( dispatch( noticesStore ) as any ).removeNotice( - 'wpai_editorial_updates_error' - ); + ( dispatch( noticesStore ) as any ).removeNotice( NOTICE_ID ); try { const content = ( @@ -312,7 +316,7 @@ export function useEditorialUpdates(): { firstErrorMessage ?? __( 'Refinement failed for all blocks.', 'ai' ), { - id: 'wpai_editorial_updates_error', + id: NOTICE_ID, isDismissible: true, } ); @@ -388,7 +392,7 @@ export function useEditorialUpdates(): { ( dispatch( noticesStore ) as any ).createErrorNotice( error?.message ?? String( error ), { - id: 'wpai_editorial_updates_error', + id: NOTICE_ID, isDismissible: true, } ); diff --git a/src/utils/provider-status.ts b/src/utils/provider-status.ts new file mode 100644 index 000000000..b00cb8130 --- /dev/null +++ b/src/utils/provider-status.ts @@ -0,0 +1,74 @@ +/** + * Provider status utilities. + */ + +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; + +interface ProviderStatus { + hasProvider: boolean; + connectorsUrl: string; +} + +declare global { + interface Window { + aiProviderData?: ProviderStatus; + } +} + +function getProviderData(): ProviderStatus { + return ( + window.aiProviderData ?? { + hasProvider: false, + connectorsUrl: '', + } + ); +} + +/** + * Checks whether an AI provider is available and creates a notice to guide them if not. + * + * @param noticeId Unique ID for the notice to prevent duplicates. + * @return True if a provider is available, false otherwise. + */ +export function ensureProvider( noticeId: string ): boolean { + const providerStatus = getProviderData(); + + if ( providerStatus.hasProvider ) { + return true; + } + + const { connectorsUrl } = providerStatus; + + ( dispatch( noticesStore ) as any ).createErrorNotice( + __( + 'This feature requires an AI Connector to function properly.', + 'ai' + ), + { + id: noticeId, + isDismissible: true, + actions: connectorsUrl + ? [ + { + label: __( 'Manage Connectors', 'ai' ), + url: connectorsUrl, + }, + ] + : [], + } + ); + + return false; +} + +/** + * Returns whether an AI provider is currently configured. + */ +export function isProviderAvailable(): boolean { + return getProviderData().hasProvider; +} From 4153e0ef83803cd93487b9031394a41cf4335a63 Mon Sep 17 00:00:00 2001 From: Nischay Date: Thu, 14 May 2026 20:27:34 +0530 Subject: [PATCH 08/12] test: add setExpectedIncorrectUsage to enqueue_assets tests --- .../Experiments/Editorial_Notes/Editorial_NotesTest.php | 2 ++ .../Experiments/Editorial_Updates/Editorial_UpdatesTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php index 649bc9d64..6a04b5fba 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php @@ -222,6 +222,8 @@ public function test_maybe_set_ai_author_returns_wp_error_unchanged() { * @since x.x.x */ public function test_enqueue_assets_runs_without_error() { + $this->setExpectedIncorrectUsage( 'WordPress\AI\Asset_Loader' ); + $this->experiment->enqueue_assets(); $this->assertTrue( true, 'enqueue_assets() should run without throwing an exception' ); diff --git a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php index c4d4103b4..08dcf6aaf 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php @@ -131,6 +131,8 @@ public function test_register_abilities_registers_editorial_updates_ability() { * @since x.x.x */ public function test_enqueue_assets_runs_without_error() { + $this->setExpectedIncorrectUsage( 'WordPress\AI\Asset_Loader' ); + $this->experiment->enqueue_assets(); $this->assertTrue( true, 'enqueue_assets() should run without throwing an exception' ); From 02097a136ef5c574c6205330c39fd1ba56837cb7 Mon Sep 17 00:00:00 2001 From: Nischay Date: Thu, 14 May 2026 22:07:13 +0530 Subject: [PATCH 09/12] test: remove setExpectedIncorrectUsage from enqueue_assets tests --- .../Editorial_Notes/Editorial_NotesTest.php | 2 -- .../Editorial_Updates/Editorial_UpdatesTest.php | 2 -- .../Example_Experiment/Example_ExperimentTest.php | 5 +++++ .../specs/experiments/no-provider-degradation.spec.js | 10 +++++----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php index 6a04b5fba..649bc9d64 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php @@ -222,8 +222,6 @@ public function test_maybe_set_ai_author_returns_wp_error_unchanged() { * @since x.x.x */ public function test_enqueue_assets_runs_without_error() { - $this->setExpectedIncorrectUsage( 'WordPress\AI\Asset_Loader' ); - $this->experiment->enqueue_assets(); $this->assertTrue( true, 'enqueue_assets() should run without throwing an exception' ); diff --git a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php index 08dcf6aaf..c4d4103b4 100644 --- a/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php +++ b/tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php @@ -131,8 +131,6 @@ public function test_register_abilities_registers_editorial_updates_ability() { * @since x.x.x */ public function test_enqueue_assets_runs_without_error() { - $this->setExpectedIncorrectUsage( 'WordPress\AI\Asset_Loader' ); - $this->experiment->enqueue_assets(); $this->assertTrue( true, 'enqueue_assets() should run without throwing an exception' ); diff --git a/tests/Integration/Includes/Experiments/Example_Experiment/Example_ExperimentTest.php b/tests/Integration/Includes/Experiments/Example_Experiment/Example_ExperimentTest.php index dedad2a7a..fa2f4e791 100644 --- a/tests/Integration/Includes/Experiments/Example_Experiment/Example_ExperimentTest.php +++ b/tests/Integration/Includes/Experiments/Example_Experiment/Example_ExperimentTest.php @@ -91,6 +91,11 @@ public function test_experiment_registration() { public function test_add_footer_content_for_logged_in_users() { $this->logInAsAdmin(); + // WP 6.9.1 added a check that fires _doing_it_wrong() when a script has + // unregistered deps. wp-edit-post depends on postbox, which is not registered + // in the test environment, so register a stub to silence the notice. + wp_register_script( 'postbox', '' ); + $this->setExpectedDeprecated( 'the_block_template_skip_link' ); ob_start(); diff --git a/tests/e2e/specs/experiments/no-provider-degradation.spec.js b/tests/e2e/specs/experiments/no-provider-degradation.spec.js index c512f8e16..0b1510129 100644 --- a/tests/e2e/specs/experiments/no-provider-degradation.spec.js +++ b/tests/e2e/specs/experiments/no-provider-degradation.spec.js @@ -175,15 +175,15 @@ test.describe( 'Graceful degradation when no AI provider is configured', () => { await expectProviderNotice( page ); } ); - test( 'Review Notes shows notice when clicking Generate Review Notes without a provider', async ( { + test( 'Editorial Notes shows notice when clicking Generate Editorial Notes without a provider', async ( { admin, editor, page, } ) => { - await enableExperiment( admin, page, 'Review Notes' ); + await enableExperiment( admin, page, 'Editorial Notes' ); await admin.createNewPost( { - title: 'Test Review Notes No Provider', + title: 'Test Editorial Notes No Provider', } ); await editor.insertBlock( { @@ -195,7 +195,7 @@ test.describe( 'Graceful degradation when no AI provider is configured', () => { } ); // After inserting a block, the sidebar switches to Block tab. - // Switch back to Post tab where the Generate Review Notes button lives. + // Switch back to Post tab where the Generate Editorial Notes button lives. await editor.openDocumentSettingsSidebar(); const postTab = page.getByRole( 'tab', { name: 'Post' } ); if ( ( await postTab.count() ) > 0 ) { @@ -203,7 +203,7 @@ test.describe( 'Graceful degradation when no AI provider is configured', () => { } const reviewButton = page.getByRole( 'button', { - name: 'Generate Review Notes', + name: 'Generate Editorial Notes', } ); await expect( reviewButton ).toBeVisible( { timeout: 5000 } ); await reviewButton.click(); From d0944b7db9b3977e29ddd87d64c12676f87d1763 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Fri, 15 May 2026 09:59:45 -0600 Subject: [PATCH 10/12] Bring back the additionalProperties field as this is required for OpenAI --- .../Editorial_Notes/Editorial_Notes.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/includes/Abilities/Editorial_Notes/Editorial_Notes.php b/includes/Abilities/Editorial_Notes/Editorial_Notes.php index dc8184834..8b66ff7fe 100644 --- a/includes/Abilities/Editorial_Notes/Editorial_Notes.php +++ b/includes/Abilities/Editorial_Notes/Editorial_Notes.php @@ -1,6 +1,6 @@ 'object', - 'properties' => array( + 'type' => 'object', + 'properties' => array( 'suggestions' => array( 'type' => 'array', 'items' => array( - 'type' => 'object', - 'properties' => array( + 'type' => 'object', + 'properties' => array( 'review_type' => array( 'type' => 'string' ), 'text' => array( 'type' => 'string' ), 'priority' => array( 'type' => 'integer' ), ), - 'required' => array( 'review_type', 'text', 'priority' ), + 'required' => array( 'review_type', 'text', 'priority' ), + 'additionalProperties' => false, ), ), ), - 'required' => array( 'suggestions' ), + 'required' => array( 'suggestions' ), + 'additionalProperties' => false, ); } From 6644bf647f3a365705fdc9f6bf085050576b082d Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Fri, 15 May 2026 10:03:56 -0600 Subject: [PATCH 11/12] Simplify docs --- docs/experiments/editorial-notes.md | 139 -------------------------- docs/experiments/editorial-updates.md | 51 ---------- 2 files changed, 190 deletions(-) diff --git a/docs/experiments/editorial-notes.md b/docs/experiments/editorial-notes.md index 805658aed..33d5aaf40 100644 --- a/docs/experiments/editorial-notes.md +++ b/docs/experiments/editorial-notes.md @@ -26,48 +26,8 @@ When enabled, a "Generate Editorial Notes" button appears in the post status inf ### For Developers -The experiment consists of: - -1. **Experiment Class** (`WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes`): Registers the ability, enqueues the block editor asset, and wires server-side hooks for Note author override and block metadata cleanup -2. **Ability Class** (`WordPress\AI\Abilities\Editorial_Notes\Editorial_Notes`): Receives a single block's content and returns structured JSON suggestions -3. **React Plugin** (`src/experiments/editorial-notes/`): Drives the UI and orchestrates block traversal, Note creation, and thread management via WordPress data stores - ## Architecture & Implementation -### Key Hooks & Entry Points - -`WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes::register()` wires everything once the experiment is enabled: - -- `wp_abilities_api_init` → registers the `ai/editorial-notes` ability -- `enqueue_block_editor_assets` → enqueues the React bundle whenever the block editor loads -- `rest_pre_insert_comment` (filter) → `maybe_set_ai_author()` — overrides the comment author to "WordPress AI" when `meta.ai_note` is `true`, so AI-generated Notes are not attributed to the authenticated user's account - -### Assets & Data Flow - -1. **PHP Side:** - - `enqueue_assets()` loads `experiments/editorial-notes` and localizes `window.aiEditorialNotesData`: - - `enabled`: Whether the experiment is currently enabled - -2. **React Side:** - - `index.tsx` registers the `ai-editorial-notes` plugin - - `EditorialNotesPlugin.tsx` renders the button inside `PluginPostStatusInfo` - - `useEditorialNotes.ts` hook manages all state and orchestration: - - Flattens the block tree to get all descendants - - Filters to reviewable block types with sufficient content (≥ 20 chars), capped at 25 blocks - - Fetches Notes in two parallel requests: - - `GET /wp/v2/comments?type=note&status=hold&post=&per_page=100` — pending Notes used as context to avoid repeating suggestions - - `GET /wp/v2/comments?type=note&status=approve&post=&per_page=100` — resolved Note IDs; blocks with a resolved Note are skipped entirely - - Processes blocks in parallel batches of 4, calling the ability for each - - Creates new Note threads via `POST /wp/v2/comments` (with `meta: { ai_note: true }` to trigger the AI author override) and updates block `metadata.noteId` - - Subsequent runs append replies to existing Note threads - -3. **Ability Execution:** - - Receives one block's content at a time (block type, plain text, post context, prior Notes, review types) - - Builds a structured prompt and sends it to the AI with the system instruction and a JSON schema for structured output - - Parses the JSON response, sanitizes each suggestion, and returns `{ suggestions: [...] }` - - Returns `{ suggestions: [] }` when the AI finds no issues - - Deduplicates against `existing_notes`: if a Note already contains a `[TYPE]` marker for a given review type, that type is skipped in the current run - ### Block Types Reviewed ```typescript @@ -267,48 +227,6 @@ async function reviewBlock( blockType, blockContent, existingNotes = [] ) { | `post_not_found` | The post ID passed does not exist | | `insufficient_capabilities` | User lacks `edit_posts` (or `edit_post` for the specific post) | -## Extending the Experiment - -### Customizing the System Instruction - -Edit `includes/Abilities/Editorial_Notes/system-instruction.php` to adjust: - -- Which review types apply to which block types -- How strictly prior suggestions are de-duplicated - -### Filtering Preferred Models - -```php -add_filter( 'wpai_preferred_text_models', function( $models ) { - return array( - array( 'openai', 'gpt-4o' ), - array( 'openai', 'gpt-4o-mini' ), - ); -} ); -``` - -### Disabling the Experiment Programmatically - -```php -add_filter( 'wpai_feature_editorial-notes_enabled', '__return_false' ); -``` - -### Adding Custom Review Types - -The `review_types` input field accepts any string values. Pass additional type names from the JS side and update the system instruction to provide guidance for those types: - -```javascript -// In your custom JS -await runAbility( 'ai/editorial-notes', { - block_type: 'core/paragraph', - block_content: '...', - review_types: [ 'accessibility', 'readability', 'grammar', 'seo', 'tone' ], - existing_notes: [], -} ); -``` - -Then add guidance for the `tone` type to `system-instruction.php`. - ## Testing ### Manual Testing Steps @@ -348,53 +266,10 @@ Then add guidance for the `tone` type to `system-instruction.php`. - All blocks already have Notes → second run skips repeats - Disable experiment → button disappears from sidebar -### Automated Testing - -**PHPUnit integration tests:** - -```bash -npm run test:php -``` - -Test files: -- `tests/Integration/Includes/Abilities/Editorial_NotesTest.php` — Ability class tests -- `tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php` — Experiment class tests - -Covers: -- Input/output schema structure -- `suggestions_schema()` OpenAI wrapper structure (name, strict, schema keys; inner type must be object) -- Empty content validation -- Mock-based suggestion return and structure -- Content sanitization -- Permission callbacks: no post ID path (editor, subscriber, logged-out), and post-specific path (valid post, missing post, insufficient edit_post, non-REST post type) -- `execute_callback` with missing post ID → WP_Error -- `get_existing_review_types_from_notes()`: type extraction, case normalisation, multiple types per Note, Notes without brackets -- Experiment hook registration (rest_pre_insert_comment) -- `ai_note` comment meta registered with `show_in_rest` -- `maybe_set_ai_author()`: overrides author when `ai_note` is true, passes through otherwise, handles WP_Error - -**Playwright E2E tests:** - -```bash -npm run wp-env:test start # Start the test environment -npm run test:e2e -- --grep "AI Editorial Notes" -``` - -Test file: `tests/e2e/editorial-notes.spec.ts` - -Covers: -- Button visibility in editor sidebar -- Button busy/disabled state during review -- Suggestion count feedback after completion -- Empty result handling -- Button hidden when experiment is disabled -- No-op when post has no reviewable blocks - ## Notes & Considerations ### Requirements -- WordPress 6.9+ (Notes feature required for block-level comment association) - Valid AI credentials configured in `Settings → Connectors` - User must have `edit_posts` capability (or `edit_post` for the specific post when a post ID is provided) - The block editor must be active (classic editor is not supported) @@ -404,7 +279,6 @@ Covers: - Each block generates one API call; blocks are processed in parallel batches of 4 - The review is capped at 25 blocks per run to control cost - Blocks with fewer than 20 characters of text are skipped -- AI temperature is set to 0.7 ### Note Storage @@ -420,16 +294,3 @@ Covers: - Block metadata (`noteId`) is only persisted after the post is saved - The 25-block cap means very long posts will have only the first 25 reviewable blocks analyzed per run - Resolved blocks (approved Notes) are skipped in full; they will not receive new suggestions until the Note is un-resolved or deleted - -## Related Files - -- **Experiment:** `includes/Experiments/Editorial_Notes/Editorial_Notes.php` -- **Ability:** `includes/Abilities/Editorial_Notes/Editorial_Notes.php` -- **System Instruction:** `includes/Abilities/Editorial_Notes/system-instruction.php` -- **React Entry:** `src/experiments/editorial-notes/index.tsx` -- **React Plugin Component:** `src/experiments/editorial-notes/components/EditorialNotesPlugin.tsx` -- **React Hook:** `src/experiments/editorial-notes/hooks/useEditorialNotes.ts` -- **PHPUnit Tests (Ability):** `tests/Integration/Includes/Abilities/Editorial_NotesTest.php` -- **PHPUnit Tests (Experiment):** `tests/Integration/Includes/Experiments/Editorial_Notes/Editorial_NotesTest.php` -- **E2E Tests:** `tests/e2e/editorial-notes.spec.ts` -- **Mock Fixtures:** `tests/e2e-request-mocking/responses/OpenAI/editorial-notes-suggestions.json` diff --git a/docs/experiments/editorial-updates.md b/docs/experiments/editorial-updates.md index c129cc165..5cdb17ecf 100644 --- a/docs/experiments/editorial-updates.md +++ b/docs/experiments/editorial-updates.md @@ -24,46 +24,8 @@ When enabled, a "Editorial Updates" button appears in the post status info panel ### For Developers -The experiment consists of: - -1. **Experiment Class** (`WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates`): Registers the ability and enqueues the block editor asset. -2. **Ability Class** (`WordPress\AI\Abilities\Editorial_Updates\Editorial_Updates`): Receives a single block's content, surrounding context, and associated notes, parsing the resulting AI output back into plain string replacements. -3. **React Plugin** (`src/experiments/editorial-updates/`): Drives the sidebar UI, discovers threaded Notes via WordPress data stores, processes block attributes iteratively, and manages Editor saving workflows. - ## Architecture & Implementation -### Key Hooks & Entry Points - -`WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates::register()` wires everything once the experiment is enabled: - -- `wp_abilities_api_init` → registers the `ai/editorial-updates` ability -- `enqueue_block_editor_assets` → enqueues the React bundle whenever the block editor loads - -### Assets & Data Flow - -1. **PHP Side:** - - - `enqueue_assets()` loads `experiments/editorial-updates` and localizes `window.aiEditorialUpdatesData`: - - `enabled`: Whether the experiment is currently enabled - -2. **React Side:** - - - `index.tsx` registers the `ai-editorial-updates` plugin. - - `EditorialUpdatesPlugin.tsx` conditionally renders the button inside `PluginPostStatusInfo`. - - `useEditorialUpdates.ts` hook manages all state and orchestration: - - Flattens the active block tree. - - Fetches active pending Notes via `GET /wp/v2/comments?type=note&status=hold&post=&per_page=100`. - - Maps notes and child threaded-replies directly to their parent `blockClientId`. - - Skips any blocks that do not have active pending notes attached. - - Processes qualifying blocks in parallel batches of 4. - - Dispatches an `updateBlockAttributes` directly to the `core/block-editor` store with the returned refactored content. - - Triggers `wp.data.dispatch( 'core/editor' ).savePost()` to persist changes and create a revision. - -3. **Ability Execution:** - - Receives target block type, current content, note texts, and optionally surrounding text context. - - Builds a standard prompt matching against the system instruction. - - Extracts plain string response from the AI and returns the direct replacement to the block content. - ### Block Types Supported Can safely run against any block. Output targets formatting of standard block markup (e.g. inner wrappers). @@ -158,16 +120,3 @@ curl -X POST "https://yoursite.com/wp-json/wp-abilities/v1/abilities/ai/editoria | `notes_required` | No valid notes were supplied in the array | | `post_not_found` | The post ID passed does not exist | | `insufficient_capabilities` | User lacks `edit_posts` (or `edit_post` for the specific post) | - -## Related Files - -- **Experiment:** `includes/Experiments/Editorial_Updates/Editorial_Updates.php` -- **Ability:** `includes/Abilities/Editorial_Updates/Editorial_Updates.php` -- **System Instruction:** `includes/Abilities/Editorial_Updates/system-instruction.php` -- **React Entry:** `src/experiments/editorial-updates/index.tsx` -- **React Plugin Component:** `src/experiments/editorial-updates/components/EditorialUpdatesPlugin.tsx` -- **React Hook:** `src/experiments/editorial-updates/hooks/useEditorialUpdates.ts` -- **PHPUnit Tests (Ability):** `tests/Integration/Includes/Abilities/Editorial_UpdatesTest.php` -- **PHPUnit Tests (Experiment):** `tests/Integration/Includes/Experiments/Editorial_Updates/Editorial_UpdatesTest.php` -- **E2E Tests:** `tests/e2e/specs/experiments/editorial-updates.spec.js` -- **Mock Fixtures:** `tests/e2e-request-mocking/responses/OpenAI/editorial-updates-completions.json` and `tests/e2e-request-mocking/responses/OpenAI/editorial-updates-responses.json` From d18593e44eebbb64682418cc6b34353d07fcd16b Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Fri, 15 May 2026 10:10:43 -0600 Subject: [PATCH 12/12] Update text in a few other places --- CHANGELOG.md | 2 ++ includes/Abilities/Editorial_Notes/Editorial_Notes.php | 6 +++--- includes/Experiments/Editorial_Notes/Editorial_Notes.php | 6 +++--- .../Experiments/Editorial_Updates/Editorial_Updates.php | 2 +- readme.txt | 4 ++-- .../editorial-notes/components/EditorialNotesPlugin.tsx | 2 +- src/experiments/editorial-notes/hooks/useEditorialNotes.ts | 6 +++--- src/experiments/editorial-notes/index.tsx | 2 +- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b21a20c3..09f737469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file, per [the Keep a Changelog standard](http://keepachangelog.com/), and will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] - TBD + ## [0.9.0] - 2026-05-07 ### Added - New Experiment: Comment Moderation to automatically moderate comments based on toxicity detection and sentiment analysis ([#155](https://github.com/WordPress/ai/pull/155), [#516](https://github.com/WordPress/ai/pull/516)). diff --git a/includes/Abilities/Editorial_Notes/Editorial_Notes.php b/includes/Abilities/Editorial_Notes/Editorial_Notes.php index 8b66ff7fe..695317a42 100644 --- a/includes/Abilities/Editorial_Notes/Editorial_Notes.php +++ b/includes/Abilities/Editorial_Notes/Editorial_Notes.php @@ -361,11 +361,11 @@ protected function generate_review( } /** - * Gets a prompt builder for generating review notes. + * Gets a prompt builder for generating editorial notes. * * @since 0.7.0 * - * @param string $prompt The prompt to generate review notes from. + * @param string $prompt The prompt to generate editorial notes from. * @param string $block_type The block type identifier. * @return \WP_AI_Client_Prompt_Builder|\WP_Error The prompt builder, or a WP_Error on failure. */ @@ -378,7 +378,7 @@ private function get_prompt_builder( string $prompt, string $block_type ) { return $this->ensure_text_generation_supported( $prompt_builder, - esc_html__( 'Review notes generation failed. Please ensure you have a connected provider that supports text generation.', 'ai' ) + esc_html__( 'Editorial notes generation failed. Please ensure you have a connected provider that supports text generation.', 'ai' ) ); } diff --git a/includes/Experiments/Editorial_Notes/Editorial_Notes.php b/includes/Experiments/Editorial_Notes/Editorial_Notes.php index 789cc9015..fc3afa15a 100644 --- a/includes/Experiments/Editorial_Notes/Editorial_Notes.php +++ b/includes/Experiments/Editorial_Notes/Editorial_Notes.php @@ -1,6 +1,6 @@