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 => ({ server, revision: 4878, checked_at: '2026-10-07T12:00:00Z', error: null, update_status: 'unknown', ...changes }); const job = (server: string, changes: Partial = {}): 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: [] }); }); });