import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' // The group vocabulary is split across four files and there is no runtime that // checks they agree. When they disagree the failure is SILENT: a panel in a // group nothing renders is fetched, held, and shown on no tab, with nothing // logged. That has eaten cards twice - tracked services moving out of // 'actions', and the 'metrics' to 'monitors' rename. // // So this reads the sources and asserts the vocabularies match. It is a source // check rather than a behaviour check because RENDERED_GROUPS is a module-local // constant in the .vue file with no export to assert against. function read(relative) { return readFileSync(fileURLToPath(new URL(relative, import.meta.url)), 'utf8') } function renderedGroups() { const source = read('./PluginAssetPanels.vue') const match = source.match(/const RENDERED_GROUPS = \[([^\]]*)\]/) if (!match) throw new Error('RENDERED_GROUPS not found in PluginAssetPanels.vue') return match[1].split(',') .map((entry) => entry.trim().replace(/^'|'$/g, '')) .filter(Boolean) } function pcDetailSource() { return read('../../../plugins/computers/frontend/views/PCDetail.vue') } describe('panel group vocabulary', () => { it('renders a group for every tab PCDetail offers', () => { const groups = renderedGroups() const source = pcDetailSource() const tabKeys = [...source.matchAll(/tabs\.push\(\{\s*key:\s*'([a-z]+)'/g)] .map((match) => match[1]) expect(tabKeys.length).toBeGreaterThan(0) for (const key of tabKeys) { expect(groups).toContain(key) } }) 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. const source = pcDetailSource() for (const group of renderedGroups()) { expect(source).toContain(`activegroup === '${group}'`) } }) it('keeps the service/process split wired end to end', () => { // Pins the specific split rather than only the general rule, so removing // the tab is a deliberate edit to this test and not a silent regression. const source = pcDetailSource() expect(renderedGroups()).toContain('serviceprocess') expect(source).toContain("key: 'serviceprocess'") expect(source).toContain("group=\"serviceprocess\"") }) it('still renders the system monitors group after the rename', () => { // The rename is label-only; the group key must not drift with it. expect(renderedGroups()).toContain('monitors') expect(pcDetailSource()).toContain("label: 'System Monitors'") }) it('agrees with the server vocabulary that resolves groups first', () => { // The fourth file this comment block claims to cover, and the one that was // NOT actually being checked. asset_panels() rewrites any group missing // from ASSET_PANEL_GROUPS to 'details' before the client sees it, so the // frontend can be perfectly consistent with itself and still show nothing: // that is exactly how the Service/Process tab went missing after Phase 3. const source = read('../../../shopdb/core/api/pluginui.py') const match = source.match(/ASSET_PANEL_GROUPS = \(([\s\S]*?)\)/) if (!match) throw new Error('ASSET_PANEL_GROUPS not found in pluginui.py') const serverGroups = match[1].split(',') .map((entry) => entry.trim().replace(/^'|'$/g, '')) .filter(Boolean) for (const group of renderedGroups()) { expect(serverGroups).toContain(group) } }) it('keeps operator reports in a tab of its own', () => { const source = pcDetailSource() expect(renderedGroups()).toContain('reports') expect(source).toContain("key: 'reports'") expect(source).toContain('group="reports"') }) })