Tracked Services and Tracked Processes were sharing a tab with system health cards, so what is running on the box and how the box itself is doing read as one list. They now sit in a Service/Process Monitors tab and the old tab is System Monitors. The group vocabulary is duplicated across the renderer, the tab map and the panel declarations with nothing checking they agree, and a group named in one but not the others is dropped in silence - that has eaten cards twice. Tests now pin the vocabulary on both sides. Boot type and NIC link state were collected in Phase 2 but shown nowhere. Boot type only renders when fast startup applies, because uptime measured from a resume understates how long the kernel has been up and that is the case worth naming. Link state leaves a disconnected NIC unknown rather than half duplex: live hardware returns empty speed and duplex when unplugged, and half duplex is the cable fault signature this column exists to catch.
70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
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'")
|
|
})
|
|
})
|