Problem/Motivation
A sweep of the remaining ai_agents_views:* tools for the same class of defect already fixed elsewhere in this module - an AI-agent-supplied value that Views itself never validates, reaching Drupal internals and crashing instead of being rejected with a readable tool error - turned up four separate, independently reproduced cases. Each was confirmed both with a dedicated Kernel test (fail before the fix, pass after) and live against the real views_agent AI agent (a real tool call through the actual agent loop, not just a direct plugin invocation).
1. create_view_handler: a made-up table/field pair is accepted and saved
Nothing validated that table_name/field_name correspond to real ViewsData for the view. A nonexistent or mismatched pair (e.g. views_bulk_operations/views_bulk_operations on a view that has no such field) was added as a broken handler; the view then fatals the next time it renders, far away from the tool call that actually caused it.
Fixed in ViewHandlerTrait::handleHandlerDataAndFieldSelection() (shared by CreateViewHandler, and already used for the "list available fields" flow) by rejecting any table/field pair not present in ViewsAgentHelper::getViewHandlerData()'s real available list, the same live ViewsData source already used to advertise valid options.
Fixing this exposed a second, pre-existing bug in ViewsAgentHelper::getViewHandlerData(): it computed the available-fields table list via $view_executable->getBaseTables() right after calling getHandlers() - but getHandlers() internally switches the view's active display to look up handlers, then switches it back to 'default' before returning. getBaseTables() was therefore always reading the default display's relationships, not the target display's, so a field brought in through a relationship added on a non-default display was wrongly rejected as "not a valid handler." Fixed by explicitly calling setDisplay($display_id) before computing base tables.
2. create_view: an entity type with no Views base table crashes instead of being rejected
Only the entity type's existence was checked (entityTypeManager->getDefinition()). A real but non-Views-integrated entity type - any config entity such as user_role, or any content entity type with no SQL base table - resolves to a NULL base table, which reaches ViewExecutable::mergeDefaults() and throws an uncaught InvalidArgumentException: A valid cache entry key is required deep inside Views' own ViewsData::get().
Fixed by adding ViewsAgentHelper::getEntityTypeBaseTable(), which resolves the entity type's base/data table and validates it against Views::viewsData()->fetchBaseTables() - the real list of tables Views actually recognizes as base tables - before creating anything.
3. create_view_display: display_id: 'default' silently wipes the existing default display
'default' is a real, registered display plugin ID, so it passed the existing plugin-ID validation cleanly. But Drupal's own View::addDisplay('default') always maps back to the ID 'default' (see generateDisplayId(): if ($plugin_id == 'default') { return 'default'; }), so calling this tool with display_id: 'default' on an existing view silently overwrote the entire default display's configuration - every field, filter and setting - with a bare, empty default display. This is worse than a crash: it is silent, irreversible data loss with no error at all.
Fixed by explicitly rejecting display_id: 'default' with a clear message: every view already has exactly one default display, so "creating" it again is never a legitimate operation.
4. update_view_plugins: a structurally malformed value crashes on save instead of being rejected
Unlike its sibling update_view_handler_option, this tool had no try/catch around its save path. A value that decodes as valid JSON but has the wrong shape for the target option (e.g. the plain number 42 for the array-shaped pager option) sails through Views' own validation-free overrideOption(), then crashes with an uncaught TypeError inside a legacy config-update hook during $view->save(): ViewsConfigUpdater::processRememberRolesUpdate(): Argument #1 ($handler) must be of type array, int given.
Fixed by adding the same catch (\Throwable $e) guard already established in UpdateViewHandlerOptions, reporting "The option could not be applied: " . $e->getMessage() instead of letting the exception propagate.
Steps to reproduce (confirmed live, on the unfixed code)
Each of the four was reproduced against the unfixed code (fixes temporarily reverted) through the real, running views_agent AI agent - ddev drush agent views_agent, an actual OpenAI-backed tool call through the agent's own loop, not a direct plugin invocation - to confirm these are genuine, live-reachable defects and not just artifacts of the automated test harness.
| # | Tool call (unfixed code) | What the agent/tool reported | What actually happened |
|---|---|---|---|
| 1 | create_viewentity_type: user_role |
The raw internal exception, verbatim, as the agent's entire reply - none of the tool's own error handling ever engaged:
|
Nothing created; the crash itself is the only feedback. |
| 2 | create_view_displaydisplay_id: default(on a view whose default display already had a real field and a custom title set) |
Reported success, no error at all:
|
drush config:get on the saved config immediately afterward showed the field and title were both gone:
|
| 3 | update_view_pluginsdisplay_option: pagerconfig: 42 |
Not a caught tool error - the drush process itself crashed and terminated abnormally, printing a full 40-frame PHP stack trace:
|
The pager option was left in a broken, half-written state; the process crash is the only feedback. |
| 4 | create_view_handlertable_name/field_name: views_bulk_operations(not a real field on the view's base table) |
Silently accepted with no error, saved as a broken handler:
|
Rendering that view afterward (Views::getView(...)->preview('default')) crashed separately, far from the tool call that actually caused it:
|
All four repro views were removed after confirming the crash/corruption, and the fixes were restored before any further work.
Proposed resolution
The four fixes above, each verified independently:
src/Trait/ViewHandlerTrait.php: reject an invalid table/field pair inhandleHandlerDataAndFieldSelection().src/Service/ViewsAgent/ViewsAgentHelper.php: addgetEntityTypeBaseTable(); fix the display-scoping bug ingetViewHandlerData().src/Plugin/AiFunctionCall/CreateView.php: validate the entity type's base table before creating the view.src/Plugin/AiFunctionCall/CreateViewDisplay.php: rejectdisplay_id: 'default'.src/Plugin/AiFunctionCall/UpdateViewPlugins.php: wrap the save path in acatch (\Throwable $e)guard.
Remaining tasks
- File this issue on drupal.org.
- Open an MR with the fixes above.
Test coverage added locally
Each fix has a Kernel test at the plugin level (fail before the fix, pass after) plus a Kernel Integration test that drives the real tool chain end to end and proves the working view (real field, real filtered/paginated query results) survives the rejected attempt untouched:
tests/src/Kernel/Plugin/AiFunctionCall/CreateViewHandlerTest::testInvalidTableFieldIsRejectedandtests/src/Kernel/Integration/CreateViewHandlerTableFieldValidationTest.tests/src/Kernel/Plugin/AiFunctionCall/CreateViewTest::testCreateViewWithNonViewsEntityTypeandtests/src/Kernel/Integration/CreateViewEntityTypeValidationTest.tests/src/Kernel/Plugin/AiFunctionCall/CreateViewDisplayTest::testCreateViewDisplayRejectsDefaultandtests/src/Kernel/Integration/CreateViewDisplayDefaultRejectionTest.tests/src/Kernel/Plugin/AiFunctionCall/UpdateViewPluginsTest::testUpdateViewDisplayOptionMalformedValueIsRejectedandtests/src/Kernel/Integration/UpdateViewPluginsMalformedValueTest.- The display-scoping fix is covered by the existing
ConfigureRelationshipFieldsTest::testFieldsViaRelationships, which regressed when the table/field validation first went in and now passes again.
All four fixes were also verified live against the real views_agent AI agent both ways: reproduced crashing/corrupting on the unfixed code first (see Steps to reproduce above), then confirmed clean - a readable rejection, no crash, no data loss - on the fixed code with the exact same prompts. The relationship-fields case was verified the same way, live.
Full ai_agents_views Kernel suite after all fixes: 87/87 tests passing, 965 assertions, 1 pre-existing unrelated skip, no failures.
API changes
None. All four are bug fixes to existing, already-broken behavior - no new context parameters, no signature changes to any tool, no config schema changes. ViewsAgentHelper::getEntityTypeBaseTable() is a new public method, purely additive.
Issue fork ai_agents_views-3622011
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
jibranCommitted and pushed to 1.0.x