Problem/Motivation

The ai_agents_views:update_view_plugins tool had been silently ineffective on any non-default display for its entire lifetime - the single largest-blast-radius defect found while testing the shipped Views Agent against a live LLM. A change that looked completely correct in a config dump could have zero real effect at runtime, on options as consequential as access.

Confirmed with a functional access check: an admin view built entirely through the agent, with its page_1</Fix update_view_plugins silently ineffective on non-default displayscode> display's <code>access option explicitly set to a permission-gated perm plugin, still let a real user account without that permission load the display - the access restriction the config claimed to have simply never took effect.

Steps to reproduce

  1. Create a View with a non-default display (e.g. a page display) that is still inheriting most of its options from the default display - true of any freshly created page/block/feed display until something explicitly overrides them.
  2. Call ai_agents_views:update_view_plugins to set that display's access option to something restrictive, e.g. {"type":"perm","options":{"perm":"access content overview"}}.
  3. Load the View and inspect $view->getDisplay('page_1')['display_options']['access'] - it shows the new, correct value.
  4. Now check whether access is actually enforced: $view->getExecutable()->access('page_1', $account) for a real account without the granted permission.

Expected: the account without the permission is denied access.

Actual (before this fix): the account is granted access - the display still functionally uses whatever access plugin the default display has (frequently none), regardless of what was just saved on page_1.

Root cause

execute() wrote the new value directly into the display's raw config array:

$display = &$view->getDisplay($display_id);
// Update the view display option with the provided config.
$display['display_options'][$display_option] = $config;

Views' own inheritance mechanism ("defaultable sections") lets most display options on any non-default display be either explicitly set on that display, or silently inherited from the default display - controlled by a separate defaults flag stored alongside the option itself. A freshly created page/block/feed display starts out inheriting nearly everything from default, until something explicitly overrides a given option and clears that flag.

The raw write above only ever touched the option's own value - it never cleared the corresponding defaults flag. So the new value sat in the saved config, technically present and correct-looking, while Views' own DisplayPluginBase::getOption()/getPlugin() - which check the defaults flag first - kept resolving to whatever the default display already had, at every real request. This affected every option that participates in this inheritance mechanism: access, cache, style, row, and more.

Proposed resolution

Switch to Views' own DisplayPluginBase::overrideOption(), which correctly clears the inheritance flag before writing the new value:

// Use overrideOption(), not a raw config write: a display can still be
// inheriting this option from the default display, and a raw write
// would save the new value without making it take effect at runtime.
$view_executable = $view->getExecutable();
$view_executable->initDisplay();
$view_executable->displayHandlers->get($display_id)->overrideOption($display_option, $config);
$this->addViewTag($view, 'ai_agents_views_updated');
$view->save();

This is a no-op for options that were never defaultable to begin with (e.g. path, use_admin_theme), so it takes effect correctly either way.

Remaining tasks

  • File this issue on drupal.org.
  • Open an MR with the fix above.

Note: Views groups several options together as a single defaultable section (for example style/row, or use_more/use_more_always/use_more_text) - overrideOption() correctly un-defaults the whole group, not just the option requested, which is expected Views behavior but produces its own follow-on side effect. Filed and fixed separately, since it's a distinct, narrower concern with its own regression test.

Test coverage added locally

New Integration test CreateAdminAuditViewTest::testAgentTools() builds an admin content-listing view through the real tool chain (bulk-operations field, operations field, a permission-gated access option on a page display) and proves, with a real editor account and a real visitor account, that access genuinely gates the display - not just that the saved config looks right. It also confirms the bulk-operations/operations fields resolve to real, working handlers when the view is actually rendered.

The pre-existing CreateAdminViewTest needed one assertion loosened: row's exact array previously asserted just {"type":"fields"}; with this fix, overriding the linked style option also completes row's schema-declared options default (an empty array) through the same override mechanism - a harmless completion of the same value, not a different one, so only the type key is asserted now.

Full suite with this fix applied (isolated on its own branch): 2/2 kernel tests passing (70 assertions) across both Integration test files, no failures; the pre-existing UpdateViewPluginsTest suite (14 tests, 116 assertions) also confirmed unaffected.

API changes

None. This is a bug fix to existing, already-broken behavior - no new context parameters, no signature changes, no config schema changes.

Command icon 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

jibran created an issue. See original summary.

jibran’s picture

Issue summary: View changes
Status: Active » Needs review

  • jibran committed dd05c1d0 on 1.0.x
    fix: #3621953 Fix update_view_plugins silently ineffective on non-...
jibran’s picture

Issue summary: View changes
Status: Needs review » Fixed

Committed and pushed to 1.0.x

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.