Files
sencho/frontend/src/components/settings/__tests__/registry.test.ts
T
Anso e9c262ae6a feat: split Host Alerts into Host Alerts, Container Alerts, and Stacks guardrails (#1461)
* feat: split Host Alerts into Host Alerts, Container Alerts, and Stacks guardrails

Move global_crash from Host Alerts to new Monitoring > Container Alerts section.
Move health gate and env deploy guardrails from Host Alerts to
Infrastructure > Stacks > Deploy Guardrails subsection.

Host Alerts now contains only host threshold settings (CPU, RAM, disk,
alert suppression, and the master host_alerts_enabled toggle).
Stacks gains a Deploy Guardrails subsection (node-scoped, admin-gated)
alongside the existing Workflow controls (browser-local).

Dashboard Crash detection row now routes to Container Alerts.

* docs: update crash detection toggle description to match new Container Alerts section
2026-06-25 21:04:55 -04:00

141 lines
6.5 KiB
TypeScript

/**
* Structural invariants for the settings registry after the hub reorganization.
*
* Guards the ten-group taxonomy and the System Limits split: every item lands in
* a real group, the three successor sections (Host Alerts, Docker & Storage,
* Fleet Mesh) exist with the right group/scope/gate, Developer is split from
* Data Retention, and the renamed labels are applied.
*/
import { describe, it, expect } from 'vitest';
import { SETTINGS_GROUPS, SETTINGS_ITEMS, scopeLabel } from '../registry';
import type { SettingsItemMeta } from '../registry';
describe('settings registry', () => {
it('points every item at a defined group', () => {
const groupIds = new Set(SETTINGS_GROUPS.map(g => g.id));
for (const item of SETTINGS_ITEMS) {
expect(groupIds.has(item.group), `item ${item.id} -> group ${item.group}`).toBe(true);
}
});
it('gives every group at least one item', () => {
for (const group of SETTINGS_GROUPS) {
const count = SETTINGS_ITEMS.filter(i => i.group === group.id).length;
expect(count, `group ${group.id}`).toBeGreaterThan(0);
}
});
it('keeps item ids unique', () => {
const ids = SETTINGS_ITEMS.map(i => i.id);
expect(new Set(ids).size).toBe(ids.length);
});
it('replaces System Limits with three focused sections', () => {
expect(SETTINGS_ITEMS.some(i => (i.id as string) === 'system')).toBe(false);
const hostAlerts = SETTINGS_ITEMS.find(i => i.id === 'host-alerts');
const dockerStorage = SETTINGS_ITEMS.find(i => i.id === 'docker-storage');
const fleetMesh = SETTINGS_ITEMS.find(i => i.id === 'fleet-mesh');
expect(hostAlerts?.group).toBe('monitoring');
expect(dockerStorage?.group).toBe('monitoring');
expect(fleetMesh?.group).toBe('infrastructure');
// All three edit the active instance's own settings through the proxy.
expect(hostAlerts?.scope).toBe('node');
expect(dockerStorage?.scope).toBe('node');
expect(fleetMesh?.scope).toBe('node');
const containerAlerts = SETTINGS_ITEMS.find(i => i.id === 'container-alerts');
expect(containerAlerts?.group).toBe('monitoring');
expect(containerAlerts?.scope).toBe('node');
expect(containerAlerts?.tier).toBeNull();
for (const term of ['crash', 'oom', 'healthcheck', 'container']) {
expect(containerAlerts?.keywords, `keyword ${term}`).toContain(term);
}
});
it('gates the Fleet Mesh section to admins so the sidebar entry and panel both hide', () => {
const fleetMesh = SETTINGS_ITEMS.find(i => i.id === 'fleet-mesh');
expect(fleetMesh?.adminOnly).toBe(true);
});
it('splits Developer into Developer Diagnostics and Data Retention under Operations', () => {
const developer = SETTINGS_ITEMS.find(i => i.id === 'developer');
const dataRetention = SETTINGS_ITEMS.find(i => i.id === 'data-retention');
expect(developer?.label).toBe('Developer Diagnostics');
expect(developer?.group).toBe('operations');
expect(dataRetention?.group).toBe('operations');
});
it('registers the Image update checks section under Automation, viewer-visible and node-scoped', () => {
const item = SETTINGS_ITEMS.find(i => i.id === 'image-updates');
expect(item?.group).toBe('automation');
expect(item?.scope).toBe('node');
expect(item?.tier).toBeNull();
// No adminOnly: viewers see the section read-only (the control disables
// itself); the backend PUT is the authoritative admin guard.
expect(item?.adminOnly).toBeUndefined();
});
it('applies the renamed section labels', () => {
const byId = new Map(SETTINGS_ITEMS.map(i => [i.id, i]));
expect(byId.get('notifications')?.label).toBe('Channels');
expect(byId.get('notification-routing')?.label).toBe('Notification Routing');
});
it('no longer registers the standalone Vulnerability Scanning section (moved to the Security page)', () => {
expect(SETTINGS_ITEMS.some(i => (i.id as string) === 'security')).toBe(false);
});
it('opens Registries to Community while keeping it admin-only', () => {
const registries = SETTINGS_ITEMS.find(i => i.id === 'registries');
expect(registries?.tier).toBeNull();
expect(registries?.adminOnly).toBe(true);
});
it('registers the Stacks section under Infrastructure with searchable workflow keywords', () => {
const stacks = SETTINGS_ITEMS.find(i => i.id === 'stacks');
expect(stacks?.group).toBe('infrastructure');
expect(stacks?.tier).toBeNull();
for (const term of ['stack', 'deploy', 'guardrail', 'health gate', 'observation', 'env', 'required variable', 'progress', 'diff', 'save']) {
expect(stacks?.keywords, `keyword ${term}`).toContain(term);
}
});
it('scopes the browser-local sections to the browser', () => {
// Appearance is the only remaining browser-local section. Stacks moved to
// node scope when Deploy Guardrails (backend settings) were added to it
// alongside the existing browser-local Workflow controls.
expect(SETTINGS_ITEMS.find(i => i.id === 'appearance')?.scope).toBe('browser');
expect(SETTINGS_ITEMS.find(i => i.id === 'stacks')?.scope).toBe('node');
});
it('exposes the Calm/Signature appearance keywords for search', () => {
const appearance = SETTINGS_ITEMS.find(i => i.id === 'appearance');
for (const term of ['calm', 'signature', 'readability', 'heading', 'chart', 'motion', 'effects']) {
expect(appearance?.keywords, `keyword ${term}`).toContain(term);
}
});
});
describe('scopeLabel', () => {
const item = (over: Partial<SettingsItemMeta>): SettingsItemMeta => ({
id: 'stacks', group: 'infrastructure', label: 'X', description: '',
keywords: [], tier: null, scope: 'global', ...over,
});
it('reads browser for browser-scoped sections regardless of their group', () => {
expect(scopeLabel(item({ scope: 'browser', group: 'personal' }))).toBe('browser');
expect(scopeLabel(item({ scope: 'browser', group: 'infrastructure' }))).toBe('browser');
});
it('reads operator for the signed-in Account (personal group)', () => {
expect(scopeLabel(item({ scope: 'global', group: 'personal' }))).toBe('operator');
});
it('reads global for other non-node, non-browser groups', () => {
expect(scopeLabel(item({ scope: 'global', group: 'access' }))).toBe('global');
});
});