160 lines
6.2 KiB
TypeScript
160 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
groupMaintenanceScanErrors,
|
|
groupMaintenanceVersions
|
|
} from '../../src/lib/ui/maintenance-presentation.js';
|
|
import type { MaintenanceJob, PlutoniumVersion } from '../../src/lib/plutonium.js';
|
|
|
|
const theater = 't5_zombie_theater_1';
|
|
const moon = 't5_zombie_moon_1';
|
|
const version = (server: string, changes: Partial<PlutoniumVersion> = {}): PlutoniumVersion => ({
|
|
server,
|
|
revision: 4878,
|
|
checked_at: '2026-10-07T12:00:00Z',
|
|
error: null,
|
|
update_status: 'unknown',
|
|
...changes
|
|
});
|
|
const job = (server: string, changes: Partial<MaintenanceJob> = {}): MaintenanceJob => ({
|
|
id: 1,
|
|
server,
|
|
state: 'queued',
|
|
target_revision: 4878,
|
|
requested_at: '2026-10-07T12:00:00Z',
|
|
restarted_at: null,
|
|
provisioning_id: 'restart-1',
|
|
finished_at: null,
|
|
detail: null,
|
|
kind: 'update',
|
|
origin: 'manual',
|
|
...changes
|
|
});
|
|
|
|
describe('maintenance scan summaries', () => {
|
|
it('groups shared failures into one cause while retaining each affected server and diagnostic message', () => {
|
|
const errors = `${theater}: Host agent is offline; /srv/private-one unavailable\n${moon}: Host agent is offline; /srv/private-two unavailable`;
|
|
const groups = groupMaintenanceScanErrors(errors, [theater, moon]);
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]).toMatchObject({
|
|
key: 'host',
|
|
title: 'Host agent unavailable',
|
|
servers: [theater, moon]
|
|
});
|
|
expect(groups[0].details).toHaveLength(2);
|
|
expect(groups[0].description).not.toContain('/srv/');
|
|
expect(groups[0].description).not.toContain(theater);
|
|
expect(groups[0].details[0].message).toContain('/srv/private-one');
|
|
});
|
|
|
|
it('separates file, permission, timeout and unknown causes without exposing raw messages as headings', () => {
|
|
const groups = groupMaintenanceScanErrors(
|
|
[
|
|
`${theater}: ENOENT: no such file /srv/missing`,
|
|
`${moon}: EACCES: permission denied /srv/private`,
|
|
`${theater}: timeout: no reply`,
|
|
`${moon}: Unrecognized diagnostic code 123`
|
|
].join('\n'),
|
|
[theater, moon]
|
|
);
|
|
expect(groups.map((group) => group.key)).toEqual([
|
|
'files',
|
|
'permissions',
|
|
'timeout',
|
|
'other'
|
|
]);
|
|
expect(groups.every((group) => !/ENOENT|EACCES|123/.test(group.title))).toBe(true);
|
|
});
|
|
|
|
it('supports custom names containing colons, avoids linking unknown identifiers and deduplicates identical failures', () => {
|
|
const message = 'Weekend: west: timeout: No reply';
|
|
const groups = groupMaintenanceScanErrors(
|
|
`${message}\n${message}\nretired_server: Agent unavailable`,
|
|
['Weekend', 'Weekend: west']
|
|
);
|
|
expect(groups[0].servers).toEqual(['Weekend: west']);
|
|
expect(groups[0].details).toEqual([
|
|
{ server: 'Weekend: west', message: 'timeout: No reply' }
|
|
]);
|
|
expect(groups[1].servers).toEqual([]);
|
|
expect(groups[1].details[0].server).toBeNull();
|
|
});
|
|
|
|
it('handles setup messages and empty scans without inventing an affected server', () => {
|
|
expect(groupMaintenanceScanErrors(null, [])).toEqual([]);
|
|
expect(groupMaintenanceScanErrors(' \n ', [])).toEqual([]);
|
|
expect(
|
|
groupMaintenanceScanErrors(
|
|
'Register a native host instance before automatic recovery.',
|
|
[]
|
|
)
|
|
).toEqual([expect.objectContaining({ key: 'setup', servers: [] })]);
|
|
});
|
|
});
|
|
|
|
describe('maintenance revision groups', () => {
|
|
it('collapses identical revisions and verification states without claiming that unknown versions are current', () => {
|
|
const versions = Array.from({ length: 12 }, (_, index) =>
|
|
version(`t5_zombie_theater_${index + 1}`)
|
|
);
|
|
const groups = groupMaintenanceVersions(versions, []);
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]).toMatchObject({
|
|
revision: 4878,
|
|
label: 'Unverified',
|
|
tone: 'warning',
|
|
updatable: []
|
|
});
|
|
expect(groups[0].versions).toHaveLength(12);
|
|
});
|
|
|
|
it('retains separate verification states and puts failures and available updates first', () => {
|
|
const groups = groupMaintenanceVersions(
|
|
[
|
|
version('current', { update_status: 'current' }),
|
|
version('unknown'),
|
|
version('outdated', { revision: 4800, update_status: 'outdated' }),
|
|
version('ahead', { revision: 5000, update_status: 'ahead' })
|
|
],
|
|
[]
|
|
);
|
|
expect(groups.map((group) => group.status)).toEqual([
|
|
'unknown',
|
|
'outdated',
|
|
'current',
|
|
'ahead'
|
|
]);
|
|
expect(groups[0].revision).toBe(groups[2].revision);
|
|
});
|
|
|
|
it('excludes pending restarts from group update actions while preserving the pending operation links', () => {
|
|
const pending = job(theater, { state: 'restarting' });
|
|
const [group] = groupMaintenanceVersions(
|
|
[
|
|
version(theater, { update_status: 'outdated' }),
|
|
version(moon, { update_status: 'outdated' })
|
|
],
|
|
[pending, job(moon, { id: 2, state: 'done' })]
|
|
);
|
|
expect(group.updatable).toEqual([moon]);
|
|
expect(group.pending).toEqual([pending]);
|
|
});
|
|
|
|
it('does not let deleted-server jobs suppress a current server update and preserves stopped legacy evidence', () => {
|
|
const groups = groupMaintenanceVersions(
|
|
[
|
|
version(theater, { update_status: 'outdated' }),
|
|
version(moon, { update_status: undefined, stopped: true, revision: null })
|
|
],
|
|
[job(theater, { server_deleted_at: '2026-10-06' })]
|
|
);
|
|
expect(groups[0].updatable).toEqual([theater]);
|
|
expect(groups[0].pending).toEqual([]);
|
|
expect(groups[1]).toMatchObject({
|
|
status: 'stopped',
|
|
label: 'Intentionally stopped',
|
|
revision: null,
|
|
updatable: []
|
|
});
|
|
});
|
|
});
|