Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,23 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) {
);
}

/**
* Clones the wrapped prompt builder alongside this instance.
*
* The wrapped builder mutates its own state, so without this a clone would
* share that state with the original and any change made to one would be
* visible in the other.
*
* @since 7.2.0
*/
public function __clone() {
$this->builder = clone $this->builder;

if ( null !== $this->error ) {
$this->error = clone $this->error;
}
}

/**
* Registers WordPress abilities as function declarations for the AI model.
*
Expand Down
69 changes: 69 additions & 0 deletions tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2613,6 +2613,75 @@ static function ( $prevent, $builder ) use ( &$captured_builder ) {
$this->assertInstanceOf( WP_AI_Client_Prompt_Builder::class, $captured_builder );
}

/**
* Returns the text of every message part held by a prompt builder.
*
* @param WP_AI_Client_Prompt_Builder $builder The prompt builder to read.
* @return string[] The text of each message part, in order.
*/
private function get_prompt_parts( WP_AI_Client_Prompt_Builder $builder ): array {
$wrapped = new ReflectionProperty( WP_AI_Client_Prompt_Builder::class, 'builder' );
self::set_accessible( $wrapped );
$inner = $wrapped->getValue( $builder );

$messages = new ReflectionProperty( $inner, 'messages' );
self::set_accessible( $messages );

$parts = array();
foreach ( $messages->getValue( $inner ) as $message ) {
foreach ( $message->getParts() as $part ) {
$parts[] = (string) $part->getText();
}
}

return $parts;
}

/**
* Tests that a clone does not share the wrapped builder with the original.
*
* @ticket 65782
*/
public function test_clone_does_not_share_the_wrapped_builder() {
$builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), 'Original prompt' );
$clone = clone $builder;

$wrapped = new ReflectionProperty( WP_AI_Client_Prompt_Builder::class, 'builder' );
self::set_accessible( $wrapped );

$this->assertNotSame(
$wrapped->getValue( $builder ),
$wrapped->getValue( $clone ),
'A clone should wrap its own builder instance'
);

$clone->with_text( 'Added to the clone' );

$this->assertSame( array( 'Original prompt' ), $this->get_prompt_parts( $builder ), 'Changing the clone should not change the original' );
}

/**
* Tests that the clone passed to the prevent prompt filter cannot change the prompt.
*
* @ticket 65782
*/
public function test_prevent_prompt_filter_cannot_mutate_the_original_prompt() {
add_filter(
'wp_ai_client_prevent_prompt',
static function ( $prevent, $builder ) {
$builder->with_text( 'Added by the filter' );
return $prevent;
},
10,
2
);

$builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), 'Original prompt' );
$builder->is_supported();

$this->assertSame( array( 'Original prompt' ), $this->get_prompt_parts( $builder ), 'A filter should not be able to change the prompt' );
}

/**
* Tests that once in error state, subsequent fluent calls return the same instance.
*
Expand Down
Loading