import { describe, expect, it, vi } from 'vitest'; import { SubscriptionHealthMonitor } from '../../../src/ha/connection/subscription-health-monitor'; import type { HASSWebSocketSubscriptionStatus } from '../../../src/ha/connection/subscription-manager'; interface TestRequest { id: string; } type State = HASSWebSocketSubscriptionStatus['state']; const status = ( state: State, request: TestRequest, key: string, extra?: { error?: unknown; failureCount?: number }, ): HASSWebSocketSubscriptionStatus => ({ state, request, key, ...extra, }); describe('SubscriptionHealthMonitor', () => { it('should report a failing key with its error and failure count', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const error = new Error('boom'); monitor.update( status('failing', { id: 'a' }, 'zha_event', { error, failureCount: 2 }), ); expect(monitor.getFailures()).toEqual([ { key: 'zha_event', error, failureCount: 2 }, ]); }); it('should ignore waiting so it never clears a failing key', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const request = { id: 'a' }; monitor.update(status('failing', request, 'zha_event', { failureCount: 1 })); monitor.update(status('waiting', request, 'zha_event')); expect(monitor.getFailures().map((f) => f.key)).toEqual(['zha_event']); }); it('should clear a key once it subscribes', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const request = { id: 'a' }; monitor.update(status('failing', request, 'zha_event', { failureCount: 1 })); monitor.update(status('subscribed', request, 'zha_event')); expect(monitor.getFailures()).toEqual([]); }); it('should clear a key once its request unsubscribes', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const request = { id: 'a' }; monitor.update(status('failing', request, 'zha_event', { failureCount: 1 })); monitor.update(status('unsubscribed', request, 'zha_event')); expect(monitor.getFailures()).toEqual([]); }); it('should report each failing key once regardless of subscriber count', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); monitor.update(status('failing', { id: 'a' }, 'zha_event', { failureCount: 1 })); monitor.update(status('failing', { id: 'b' }, 'zha_event', { failureCount: 1 })); expect(monitor.getFailures().map((f) => f.key)).toEqual(['zha_event']); }); it('should notify listeners only when a key changes failing state', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const listener = vi.fn(); monitor.addListener(listener); const request = { id: 'a' }; // Healthy -> failing: one notification. monitor.update(status('failing', request, 'zha_event', { failureCount: 1 })); expect(listener).toHaveBeenCalledTimes(1); // Still failing (next attempt, same key): no membership change, no notify. monitor.update(status('failing', request, 'zha_event', { failureCount: 2 })); expect(listener).toHaveBeenCalledTimes(1); // Failing -> healthy: one more notification. monitor.update(status('subscribed', request, 'zha_event')); expect(listener).toHaveBeenCalledTimes(2); }); it('should stop notifying after the returned unsubscribe is called', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const listener = vi.fn(); const remove = monitor.addListener(listener); remove(); monitor.update(status('failing', { id: 'a' }, 'zha_event', { failureCount: 1 })); expect(listener).not.toHaveBeenCalled(); }); it('should retry one request per failing key and leave healthy keys alone', () => { const retry = vi.fn(); const monitor = new SubscriptionHealthMonitor(retry); const failingA = { id: 'a' }; const failingB = { id: 'b' }; const healthy = { id: 'c' }; // Two requests on a failing key, plus a separate healthy key. monitor.update(status('failing', failingA, 'zha_event', { failureCount: 1 })); monitor.update(status('failing', failingB, 'zha_event', { failureCount: 1 })); monitor.update(status('subscribed', healthy, 'deconz_event')); monitor.retry(); // Exactly one retry, for one of the failing key's requests; never the // healthy key. expect(retry).toHaveBeenCalledTimes(1); expect([failingA, failingB]).toContainEqual(retry.mock.calls[0][0]); }); it('should retry the failing request even when a subscribed sibling is stored first', () => { const retry = vi.fn(); const monitor = new SubscriptionHealthMonitor(retry); const subscribed = { id: 'a' }; const failing = { id: 'b' }; // Subscribed sibling recorded before the failing one on the same key. monitor.update(status('subscribed', subscribed, 'zha_event')); monitor.update(status('failing', failing, 'zha_event', { failureCount: 1 })); monitor.retry(); expect(retry).toHaveBeenCalledTimes(1); expect(retry).toHaveBeenCalledWith(failing); }); it('should retry one request for each distinct failing key', () => { const retry = vi.fn(); const monitor = new SubscriptionHealthMonitor(retry); const a = { id: 'a' }; const b = { id: 'b' }; monitor.update(status('failing', a, 'zha_event', { failureCount: 1 })); monitor.update(status('failing', b, 'deconz_event', { failureCount: 1 })); monitor.retry(); expect(retry).toHaveBeenCalledTimes(2); expect(retry.mock.calls.map((c) => c[0])).toEqual(expect.arrayContaining([a, b])); }); it('should not notify when a never-failed request unsubscribes', () => { const monitor = new SubscriptionHealthMonitor(vi.fn()); const listener = vi.fn(); monitor.addListener(listener); monitor.update(status('unsubscribed', { id: 'a' }, 'zha_event')); expect(listener).not.toHaveBeenCalled(); expect(monitor.getFailures()).toEqual([]); }); });