From 7905917cd45aa8d64c0d059868e48607c3bd973a Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Sun, 6 Sep 2026 05:38:44 -0400 Subject: [PATCH] Green Up: scope panel-group check to panel-backed tabs Corporate Tools is the first PCDetail tab that mounts no PluginAssetPanels: it is permission-gated and renders a fixed action list, so no panel can land in it. The vocabulary check asserted every tabs.push key appears in RENDERED_GROUPS, which cd70556 broke by adding that tab. Adding 'corptools' to the vocabulary would have made it a legal panel group with no panel host - fetched, held, rendered nowhere - the silent-drop failure this file exists to catch, and it would also have failed the server-vocabulary check against ASSET_PANEL_GROUPS. So the invariant is narrowed to panel-backed tabs instead, with a mirror check that any tab mounting a PluginAssetPanels must have its group rendered. --- frontend/src/components/panelGroups.spec.js | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/panelGroups.spec.js b/frontend/src/components/panelGroups.spec.js index 9942d3f7..3a4e439c 100644 --- a/frontend/src/components/panelGroups.spec.js +++ b/frontend/src/components/panelGroups.spec.js @@ -29,12 +29,25 @@ function pcDetailSource() { return read('../../../plugins/computers/frontend/views/PCDetail.vue') } +// The tabs that actually HOST panels - those where PCDetail mounts a +// PluginAssetPanels for the group. A tab is only bound by the vocabulary if a +// panel can land in it. Corporate Tools is the first tab that hosts none: it is +// permission-gated and renders a fixed action list, so RENDERED_GROUPS has +// nothing to say about it and requiring it there would make 'corptools' a legal +// panel group with no panel host - fetched, held, rendered nowhere, which is +// the very failure this file exists to catch. +function panelHostedGroups(source) { + return [...source.matchAll(/group="([a-z]+)"/g)].map((match) => match[1]) +} + describe('panel group vocabulary', () => { - it('renders a group for every tab PCDetail offers', () => { + it('renders a group for every panel-backed tab PCDetail offers', () => { const groups = renderedGroups() const source = pcDetailSource() + const hosted = panelHostedGroups(source) const tabKeys = [...source.matchAll(/tabs\.push\(\{\s*key:\s*'([a-z]+)'/g)] .map((match) => match[1]) + .filter((key) => hosted.includes(key)) expect(tabKeys.length).toBeGreaterThan(0) for (const key of tabKeys) { @@ -42,6 +55,19 @@ describe('panel group vocabulary', () => { } }) + it('never hosts panels in a group this build does not render', () => { + // The mirror of that exemption, so opting a tab out of panels cannot become + // a loophole: the moment a tab mounts a PluginAssetPanels, its group MUST + // be rendered, or the panels are fetched and dropped with nothing logged. + const groups = renderedGroups() + const hosted = panelHostedGroups(pcDetailSource()) + + expect(hosted.length).toBeGreaterThan(0) + for (const group of hosted) { + expect(groups).toContain(group) + } + }) + it('gives every rendered group somewhere to actually show', () => { // A group in RENDERED_GROUPS with no v-show block is the mirror failure: // the panel is kept rather than dropped, but still appears nowhere.