AI: Clone the wrapped builder when cloning WP_AI_Client_Prompt_Builder - #12801
AI: Clone the wrapped builder when cloning WP_AI_Client_Prompt_Builder#12801jigneshbhavani wants to merge 4 commits into
Conversation
WP_AI_Client_Prompt_Builder had no __clone(), so cloning it copied the reference to the wrapped PromptBuilder rather than the builder itself. The wrapped builder mutates its own state, so the clone handed to the wp_ai_client_prevent_prompt filter shared its messages and configuration with the original, and a filter could change the prompt that was then sent to the provider despite the clone being documented as read only. Add __clone() so the wrapped builder, and any stored error, are copied too.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
…lder tests. The two tests added for #65782 called ReflectionProperty::setAccessible() directly. PHP 8.5 deprecates that method because it has had no effect since PHP 8.1, so both tests errored on every PHP 8.5 job. This file already has a set_accessible() helper that only calls setAccessible() below PHP 8.1. Route the three new call sites through it, as the rest of the file does.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds deep-clone behavior to WP_AI_Client_Prompt_Builder so cloned instances don’t share mutable internal state, and introduces PHPUnit tests to prevent regressions around cloning and filter immutability.
Changes:
- Implement
__clone()to clone the wrapped builder (and error object when present). - Add tests verifying clones don’t share the wrapped builder and that prevent-prompt filters can’t mutate the original prompt.
- Add a small test helper to extract prompt text via reflection for assertions.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php | Adds clone-related regression tests and a helper to read prompt text from internal messages. |
| src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php | Implements deep cloning of wrapped builder (and $error) to prevent shared state between clones. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…hem. Keeps the parts separate so a failure shows which part was added rather than one run together string. See #65782.
gziolo
left a comment
There was a problem hiding this comment.
Thanks, this looks good. I applied the patch locally: the full ai-client group passes (259 tests), both new tests fail with only the src/ change reverted, and phpcs and phpstan are clean.
One thing to update before commit: the new method says @since 7.1.0, but trunk moved to 7.2-alpha after you opened this. It should be @since 7.2.0.
…_clone()`. Trunk moved to 7.2-alpha after the patch was written. See #65782.
WP_AI_Client_Prompt_Builder::__call()hands a clone of itself to thewp_ai_client_prevent_promptfilter, and the hook doc calls it read only:The class has no
__clone(), so that is a shallow copy and the clone still points at the same wrappedPromptBuilder. The wrapped builder mutates itself rather than returning new instances:So whatever a filter does to the clone lands on the original, and the prompt sent to the provider is not the one the caller built.
This runs on every call that reaches the filter, so support checks and generating methods both.
Fix
Add
__clone()so the wrapped builder is cloned too. The bundled client already implementsPromptBuilder::__clone()to deep clone messages, model config and request options. It was written for this, it just never ran because only the wrapper was being cloned.A stored
WP_Erroris copied as well. Nothing today clones the wrapper while it holds an error, since the filter is only reached when the error is still null, but__clone()is a magic method any caller can trigger and leaving half the state shared would be the same bug in a different place.Testing
Two tests, both failing on trunk:
Both pass with the patch. The existing
test_prevent_prompt_filter_receives_cloned_builder_instance()only asserted the outer objects differ, which is why this went unnoticed, and it still passes. The fullai-clientgroup is green, 259 tests.phpcspasses on both changed files.Trac ticket: https://core.trac.wordpress.org/ticket/65782
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Reading through the AI client for places where the code disagrees with its own documented behaviour, which is how this turned up, and drafting this description. I confirmed the mutation by hand, checked that the bundled client already has a deep
__clone(), wrote and ran the tests, checked they fail without the patch, ranphpcs, and I take responsibility for the change.This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.