Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion includes/Abilities/Image/Import_Base64_Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ protected function import_image( string $data, array $args = array() ) {
);

if ( $args['ai_generated'] ) {
$post_data['meta_input']['ai_generated'] = 1;
$post_data['meta_input']['wpai_generated'] = 1;
}

$attachment_id = media_handle_sideload( $file_array, 0, $args['description'], $post_data );
Expand Down
2 changes: 2 additions & 0 deletions includes/Admin/Upgrades.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use WordPress\AI\Admin\Upgrades\V0_5_0;
use WordPress\AI\Admin\Upgrades\V0_6_0;
use WordPress\AI\Admin\Upgrades\V1_0_0;
use WordPress\AI\Admin\Upgrades\V1_3_0;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
Expand Down Expand Up @@ -52,6 +53,7 @@ final class Upgrades {
V0_5_0::class,
V0_6_0::class,
V1_0_0::class,
V1_3_0::class,
);

/**
Expand Down
84 changes: 84 additions & 0 deletions includes/Admin/Upgrades/V1_3_0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* Upgrade routines for version 1.3.0.
*
* @package WordPress\AI\Admin\Upgrades
* @since x.x.x
*/

declare( strict_types=1 );

namespace WordPress\AI\Admin\Upgrades;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
* Upgrade routine for standardizing meta keys on the `wpai_` prefix.
*
* Renames the legacy `ai_generated` and `ai_generated_summary` post meta keys and
* the `ai_note` comment meta key to their `wpai_`-prefixed equivalents so all
* plugin-owned meta shares a consistent namespace.
*
* @since x.x.x
* @internal
*/
class V1_3_0 extends Abstract_Upgrade {
Comment thread
dkotter marked this conversation as resolved.

/**
* {@inheritDoc}
*
* @since x.x.x
*/
public static string $version = '1.3.0';

/**
* {@inheritDoc}
*
* Migrates post and comment meta keys from the legacy `ai_` prefix to the
* `wpai_` prefix.
*
* @since x.x.x
*/
protected function upgrade(): void {
$this->rename_post_meta_key( 'ai_generated', 'wpai_generated' );
$this->rename_post_meta_key( 'ai_generated_summary', 'wpai_generated_summary' );
$this->rename_comment_meta_key( 'ai_note', 'wpai_note' );
Comment on lines +44 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're using direct DB queries to update meta, may need a cache flush after this runs in order to ensure any cached values are flushed out. Likely not a problem as we're not changing data, just keys but may be better safe than sorry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, Will flush the cache to ensure everything works expected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_cache_flush works and is likely fine since this is a one-time thing but does clear out the entire cache, whereas we only need to worry about the post and comment cache. We could modify things to track which post IDs and comment IDs were updated and then just clear those caches (potentially using clean_post_cache/clean_comment_cache). Any thoughts on that or is that not needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think, clean_post_cache/clean_comment_cache is needed, as wp_cache_flush would wipe every cache on the site. So IMO it's not needed as post/comment and every object cache would be flushed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, wp_cache_flush will clear the cache we need here but also deletes all other caches. So on sites heavily reliant on cached data for performance, running wp_cache_flush can have a negative impact on the site. Likely won't be a problem here but we could be more specific with the cache flush functions to only clear what we need

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, also this is a one off thing, so we can go with wp_cache_flush here.

}

/**
* Renames a post meta key for every row that uses it.
*
* @since x.x.x
*
* @param string $old_key The existing meta key.
* @param string $new_key The meta key to migrate to.
*/
private function rename_post_meta_key( string $old_key, string $new_key ): void {
global $wpdb;

$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
Comment thread
dkotter marked this conversation as resolved.
Outdated
$wpdb->postmeta,
array( 'meta_key' => $new_key ),
array( 'meta_key' => $old_key )
);
}

/**
* Renames a comment meta key for every row that uses it.
*
* @since x.x.x
*
* @param string $old_key The existing meta key.
* @param string $new_key The meta key to migrate to.
*/
private function rename_comment_meta_key( string $old_key, string $new_key ): void {
global $wpdb;

$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->commentmeta,
array( 'meta_key' => $new_key ),
array( 'meta_key' => $old_key )
);
}
}
6 changes: 3 additions & 3 deletions includes/Experiments/Editorial_Notes/Editorial_Notes.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function register(): void {

register_meta(
'comment',
'ai_note',
'wpai_note',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I hadn't thought about until now, these meta keys we're renaming are set as show_in_rest => true. This means this data is exposed via REST and there could be others that are consuming/using that data. This rename would break their integrations.

I would guess there's a low (potentially zero) amount of users doing this but any thoughts on what is a breaking change here? At the very least, we'll need to call this out as a breaking change in our changelog but wondering if there's anything else we should consider here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would guess there's a low (potentially zero) amount of users doing this but any thoughts on what is a breaking change here?

Yes, there could be breaking change, for eg; Users using/depend on Rest API for data would break, since ai_generated key would no longer be return in response, it would be wpai_generated. So user who access it as meta.ai_generated would get undefined, so it would silently break that part.

Another example could be user external writes request with body params as {"meta": {"ai_generated": ""}} would break, because rest does not knows about this key, so it would silently drops this key. So until noticed it won't be visible.

May be we have 2 pathways here,

  1. Document it as a breaking change in newer version. Simplest but may have backward compatibility issue. But since amount of users would potentially be zero doing this would work.
  2. Keep the meta key and register newer one side by side, drop the other one after few releases and add the deprecation notice or similar using the older keys could be an option.

IMO, we should go with the 1st, just document it as a breaking change in the release doc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think I'm fine with option 1 here. But probably worth us discussing @jeffpaul prior to merging this in

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dkotter @jeffpaul, Any updates on this? Let's get this updated before more users uses the AI plugin, so better to update these inconsistencies.

Thanks,

@dkotter dkotter Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not had a chance to discuss with Jeff but I'm still fine with option 1 here so let's move forward with that

Edit: just saw Jeff left a comment on the Issue. I'm fine with the renaming

@hbhalodia hbhalodia Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @dkotter I think this would be added at the time of release right? So no any changes in PR.

Let me know if there is any other feedback on this?

array(
'type' => 'boolean',
'single' => true,
Expand Down Expand Up @@ -97,7 +97,7 @@ public function register_abilities(): void {
* Overrides the author fields for AI-generated Notes before they are inserted.
*
* Fires via the rest_pre_insert_comment filter. When the REST request includes
* meta.ai_note = true on a Note (comment_type "note") created by a user who can
* meta.wpai_note = true on a Note (comment_type "note") created by a user who can
* edit the target post, replaces the authenticated user's identity with a generic
* "AI" author so Notes are not attributed to a personal account.
*
Expand All @@ -114,7 +114,7 @@ public function maybe_set_ai_author( $prepared_comment, \WP_REST_Request $reques

$meta = $request->get_param( 'meta' );

if ( ! is_array( $meta ) || empty( $meta['ai_note'] ) ) {
if ( ! is_array( $meta ) || empty( $meta['wpai_note'] ) ) {
return $prepared_comment;
}

Expand Down
2 changes: 1 addition & 1 deletion includes/Experiments/Summarization/Summarization.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function register_bulk_action_hooks_for_screen(): void {
public function register_post_meta(): void {
register_meta(
'post',
'ai_generated_summary',
'wpai_generated_summary',
array(
'type' => 'string',
'single' => true,
Expand Down
2 changes: 1 addition & 1 deletion includes/Features/Image_Generation/Image_Generation.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public function inject_generate_image_button(): void {
public function register_post_meta(): void {
register_post_meta(
'attachment',
'ai_generated',
'wpai_generated',
array(
'type' => 'integer',
'single' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ async function createNote(
type: 'note',
status: 'hold',
parent: existingNoteId ?? 0,
meta: { ai_note: true },
meta: { wpai_note: true },
}
) ) as NoteRecord | undefined;

Expand Down
2 changes: 1 addition & 1 deletion src/experiments/summarization/bulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ async function processBulkSummary(): Promise< void > {
method: 'POST',
data: {
content: newContent,
meta: { ai_generated_summary: summary },
meta: { wpai_generated_summary: summary },
},
} );
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function useSummaryGeneration() {
editPost( {
meta: {
...meta,
ai_generated_summary: generatedSummary,
wpai_generated_summary: generatedSummary,
},
} );

Expand Down
2 changes: 1 addition & 1 deletion src/features/image-generation/components/AILabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function AILabel( { label }: AILabelProps ): React.JSX.Element {

return (
<>
{ image && image?.meta?.ai_generated === 1 && (
{ image && image?.meta?.wpai_generated === 1 && (
<div className="ai-label">
<span
className="ai-label__text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public function test_execute_callback_with_custom_metadata() {
$this->assertEquals( 'Custom Test Image Alt Text', get_post_meta( $result['image']['id'], '_wp_attachment_image_alt', true ), 'Attachment alt text should match' );

// Verify the AI-generated flag was saved.
$this->assertSame( '1', get_post_meta( $result['image']['id'], 'ai_generated', true ), 'AI-generated flag should be saved' );
$this->assertSame( '1', get_post_meta( $result['image']['id'], 'wpai_generated', true ), 'AI-generated flag should be saved' );
}

/**
Expand Down
98 changes: 98 additions & 0 deletions tests/Integration/Includes/Admin/Upgrades/V1_3_0Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
/**
* Integration tests for V1_3_0.
*
* @package WordPress\AI\Tests\Integration\Admin\Upgrades
*/

namespace WordPress\AI\Tests\Integration\Admin\Upgrades;

use WP_UnitTestCase;
use WordPress\AI\Admin\Upgrades\V1_3_0;

/**
* V1_3_0 test case.
*
* @covers \WordPress\AI\Admin\Upgrades\V1_3_0
* @since x.x.x
*/
class V1_3_0Test extends WP_UnitTestCase {

/**
* Tests that run() renames the ai_generated attachment meta key.
*
* @since x.x.x
*/
public function test_run_renames_generated_meta(): void {
$attachment_id = self::factory()->post->create( array( 'post_type' => 'attachment' ) );
update_post_meta( $attachment_id, 'ai_generated', 1 );

( new V1_3_0( '1.2.0' ) )->run();

// Direct SQL updates bypass the in-request meta cache.
wp_cache_flush();

$this->assertSame( '1', get_post_meta( $attachment_id, 'wpai_generated', true ), 'ai_generated should migrate to wpai_generated.' );
$this->assertSame( '', get_post_meta( $attachment_id, 'ai_generated', true ), 'Old ai_generated meta should be removed.' );
}

/**
* Tests that run() renames the ai_generated_summary post meta key.
*
* @since x.x.x
*/
public function test_run_renames_summary_meta(): void {
$post_id = self::factory()->post->create();
update_post_meta( $post_id, 'ai_generated_summary', 'A summary.' );

( new V1_3_0( '1.2.0' ) )->run();

wp_cache_flush();

$this->assertSame( 'A summary.', get_post_meta( $post_id, 'wpai_generated_summary', true ), 'ai_generated_summary should migrate to wpai_generated_summary.' );
$this->assertSame( '', get_post_meta( $post_id, 'ai_generated_summary', true ), 'Old ai_generated_summary meta should be removed.' );
}

/**
* Tests that run() renames the ai_note comment meta key.
*
* @since x.x.x
*/
public function test_run_renames_note_comment_meta(): void {
$comment_id = self::factory()->comment->create();
update_comment_meta( $comment_id, 'ai_note', true );

( new V1_3_0( '1.2.0' ) )->run();

wp_cache_flush();

$this->assertSame( '1', get_comment_meta( $comment_id, 'wpai_note', true ), 'ai_note should migrate to wpai_note.' );
$this->assertSame( '', get_comment_meta( $comment_id, 'ai_note', true ), 'Old ai_note comment meta should be removed.' );
}

/**
* Tests that run() returns true on success.
*
* @since x.x.x
*/
public function test_run_returns_success(): void {
$this->assertTrue( ( new V1_3_0( '1.2.0' ) )->run() );
}

/**
* Tests that run() skips migration when the version is already current.
*
* @since x.x.x
*/
public function test_run_skips_when_version_already_current(): void {
$post_id = self::factory()->post->create();
update_post_meta( $post_id, 'ai_generated_summary', 'A summary.' );

( new V1_3_0( '1.3.0' ) )->run();

wp_cache_flush();

$this->assertSame( 'A summary.', get_post_meta( $post_id, 'ai_generated_summary', true ), 'Old meta should be untouched when the upgrade is skipped.' );
$this->assertSame( '', get_post_meta( $post_id, 'wpai_generated_summary', true ), 'New meta should not be written when the upgrade is skipped.' );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ public function test_register_abilities_registers_editorial_notes_ability() {
*/
public function test_ai_note_comment_meta_is_registered() {
$registered = get_registered_meta_keys( 'comment' );
$this->assertArrayHasKey( 'ai_note', $registered, 'ai_note meta should be registered for comments' );
$this->assertTrue( $registered['ai_note']['show_in_rest'], 'ai_note meta should have show_in_rest enabled' );
$this->assertArrayHasKey( 'wpai_note', $registered, 'ai_note meta should be registered for comments' );
$this->assertTrue( $registered['wpai_note']['show_in_rest'], 'ai_note meta should have show_in_rest enabled' );
}

/**
Expand Down Expand Up @@ -188,7 +188,7 @@ public function test_maybe_set_ai_author_overrides_author_when_ai_note_true() {
);

$request = new \WP_REST_Request( 'POST', '/wp/v2/comments' );
$request->set_param( 'meta', array( 'ai_note' => true ) );
$request->set_param( 'meta', array( 'wpai_note' => true ) );

$result = $this->experiment->maybe_set_ai_author( $prepared, $request );

Expand Down Expand Up @@ -220,7 +220,7 @@ public function test_maybe_set_ai_author_returns_error_for_unauthorized_user() {
);

$request = new \WP_REST_Request( 'POST', '/wp/v2/comments' );
$request->set_param( 'meta', array( 'ai_note' => true ) );
$request->set_param( 'meta', array( 'wpai_note' => true ) );

$result = $this->experiment->maybe_set_ai_author( $prepared, $request );

Expand Down Expand Up @@ -250,7 +250,7 @@ public function test_maybe_set_ai_author_passes_through_non_note_comment() {
);

$request = new \WP_REST_Request( 'POST', '/wp/v2/comments' );
$request->set_param( 'meta', array( 'ai_note' => true ) );
$request->set_param( 'meta', array( 'wpai_note' => true ) );

$result = $this->experiment->maybe_set_ai_author( $prepared, $request );

Expand Down Expand Up @@ -292,7 +292,7 @@ public function test_maybe_set_ai_author_passes_through_when_ai_note_false() {
);

$request = new \WP_REST_Request( 'POST', '/wp/v2/comments' );
$request->set_param( 'meta', array( 'ai_note' => false ) );
$request->set_param( 'meta', array( 'wpai_note' => false ) );

$result = $this->experiment->maybe_set_ai_author( $prepared, $request );

Expand All @@ -308,7 +308,7 @@ public function test_maybe_set_ai_author_passes_through_when_ai_note_false() {
public function test_maybe_set_ai_author_returns_wp_error_unchanged() {
$error = new \WP_Error( 'test_error', 'Test error message' );
$request = new \WP_REST_Request( 'POST', '/wp/v2/comments' );
$request->set_param( 'meta', array( 'ai_note' => true ) );
$request->set_param( 'meta', array( 'wpai_note' => true ) );

$result = $this->experiment->maybe_set_ai_author( $error, $request );

Expand Down Expand Up @@ -343,7 +343,7 @@ public function test_subscriber_ai_note_request_is_rejected_and_persists_no_comm
$request = new \WP_REST_Request( 'POST', '/wp/v2/comments' );
$request->set_param( 'post', $post_id );
$request->set_param( 'content', 'This paragraph has accessibility issues and should be rewritten.' );
$request->set_param( 'meta', array( 'ai_note' => true ) );
$request->set_param( 'meta', array( 'wpai_note' => true ) );

$response = rest_get_server()->dispatch( $request );

Expand Down Expand Up @@ -371,7 +371,7 @@ public function test_editor_ai_note_request_creates_ai_attributed_comment() {
$request->set_param( 'post', $post_id );
$request->set_param( 'content', 'AI editorial suggestion.' );
$request->set_param( 'type', 'note' );
$request->set_param( 'meta', array( 'ai_note' => true ) );
$request->set_param( 'meta', array( 'wpai_note' => true ) );

$response = rest_get_server()->dispatch( $request );

Expand Down Expand Up @@ -419,10 +419,10 @@ public function test_ai_note_meta_auth_callback_returns_true_when_user_can_edit_
wp_set_current_user( $user_id );

$registered = get_registered_meta_keys( 'comment' );
$callback = $registered['ai_note']['auth_callback'] ?? null;
$callback = $registered['wpai_note']['auth_callback'] ?? null;

$this->assertIsCallable( $callback, 'auth_callback should be callable' );
$this->assertTrue( $callback( true, 'ai_note', $comment_id ), 'auth_callback should return true when the user can edit the comment post.' );
$this->assertTrue( $callback( true, 'wpai_note', $comment_id ), 'auth_callback should return true when the user can edit the comment post.' );
}

/**
Expand All @@ -440,9 +440,9 @@ public function test_ai_note_meta_auth_callback_returns_false_when_user_cannot_e
wp_set_current_user( $other_author_id );

$registered = get_registered_meta_keys( 'comment' );
$callback = $registered['ai_note']['auth_callback'] ?? null;
$callback = $registered['wpai_note']['auth_callback'] ?? null;

$this->assertIsCallable( $callback, 'auth_callback should be callable' );
$this->assertFalse( $callback( true, 'ai_note', $comment_id ), 'auth_callback should return false when the user cannot edit the comment post.' );
$this->assertFalse( $callback( true, 'wpai_note', $comment_id ), 'auth_callback should return false when the user cannot edit the comment post.' );
}
}
Loading
Loading