Green Up: scope panel-group check to panel-backed tabs
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled

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.
This commit is contained in:
benjamin.baker
2026-09-06 05:38:44 -04:00
parent cd705563d0
commit 7905917cd4

View File

@@ -29,12 +29,25 @@ function pcDetailSource() {
return read('../../../plugins/computers/frontend/views/PCDetail.vue') 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', () => { 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 groups = renderedGroups()
const source = pcDetailSource() const source = pcDetailSource()
const hosted = panelHostedGroups(source)
const tabKeys = [...source.matchAll(/tabs\.push\(\{\s*key:\s*'([a-z]+)'/g)] const tabKeys = [...source.matchAll(/tabs\.push\(\{\s*key:\s*'([a-z]+)'/g)]
.map((match) => match[1]) .map((match) => match[1])
.filter((key) => hosted.includes(key))
expect(tabKeys.length).toBeGreaterThan(0) expect(tabKeys.length).toBeGreaterThan(0)
for (const key of tabKeys) { 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', () => { it('gives every rendered group somewhere to actually show', () => {
// A group in RENDERED_GROUPS with no v-show block is the mirror failure: // 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. // the panel is kept rather than dropped, but still appears nowhere.