import { describe, it, expect } from 'vitest' import { isHealthEvent, healthFromEvent, applyHealthEvent, applyHealthEventTo } from './health' import type { Entity } from './api' import type { OikosEvent } from './stores/events' function ev(partial: Partial): OikosEvent { return { id: 1, ts: '2026-07-28T12:00:00Z', type: 'health.changed', entity_id: 'e1', severity: 'info', source: 'scheduler', data: { to: 'degraded' }, correlation_id: null, ...partial } as OikosEvent } function entity(partial: Partial = {}): Entity { return { id: 'e1', slug: 'lxc:apps', type: 'lxc', name: 'apps', attributes: {}, version: 1, created_at: '', updated_at: '', health: 'healthy', ...partial } as Entity } describe('health event application', () => { it('reads the new health out of the payload', () => { expect(healthFromEvent(ev({}))).toBe('degraded') expect(healthFromEvent(ev({ type: 'health.stale', data: { to: 'stale' } }))).toBe('stale') }) it('ignores events that are not health events', () => { expect(isHealthEvent(ev({ type: 'signal.raised' }))).toBe(false) expect(healthFromEvent(ev({ type: 'execution.output' }))).toBeNull() }) it('ignores a health event with no usable payload', () => { expect(healthFromEvent(ev({ data: {} }))).toBeNull() expect(healthFromEvent(ev({ entity_id: null }))).toBeNull() }) it('patches the matching entity in a list and leaves the rest alone', () => { const list = [entity(), entity({ id: 'e2', slug: 'lxc:dns', health: 'healthy' })] const next = applyHealthEvent(list, ev({})) expect(next[0].health).toBe('degraded') expect(next[0].last_check_at).toBe('2026-07-28T12:00:00Z') // untouched entities keep their identity, so rows that did not change do // not re-render expect(next[1]).toBe(list[1]) }) // Returning the same array reference matters: assigning a fresh array on // every unrelated event would churn the whole table. it('returns the original array when the event does not apply', () => { const list = [entity()] expect(applyHealthEvent(list, ev({ entity_id: 'nobody' }))).toBe(list) expect(applyHealthEvent(list, ev({ type: 'signal.raised' }))).toBe(list) expect(applyHealthEvent(list, ev({ data: { to: 'healthy' } }))).toBe(list) // already healthy }) it('patches a single entity for a detail view', () => { const e = entity() const next = applyHealthEventTo(e, ev({})) expect(next?.health).toBe('degraded') expect(applyHealthEventTo(e, ev({ entity_id: 'other' }))).toBe(e) expect(applyHealthEventTo(null, ev({}))).toBeNull() }) // The scheduler emits health.changed with entity_id = the observed entity // but data.slug = the *check's* slug, so slug must never be used to match. it('matches on entity_id, never on the payload slug', () => { const list = [entity()] const next = applyHealthEvent(list, ev({ data: { to: 'down', slug: 'check:ping:whatever:0' } })) expect(next[0].health).toBe('down') }) })