import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { HealthChecker, type HealthTelemetryEvent } from '@main/services/HealthChecker.js'; import type { InferenceProvider, HealthResult, GenerateInput } from '@main/ai/InferenceProvider.js'; import type { AiResponse } from '@main/ai/schema.js'; class FakeProvider implements InferenceProvider { readonly name = 'fake'; results: HealthResult[] = []; private idx = 0; async healthCheck(): Promise { const r = this.results[Math.min(this.idx, this.results.length - 1)] ?? { ok: true }; this.idx += 1; return r; } async generate(_input: GenerateInput): Promise { throw new Error('not used'); } } describe('HealthChecker — start/stop polling', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('start() runs runOnce immediately + every intervalMs', async () => { const provider = new FakeProvider(); provider.results = [{ ok: true }, { ok: true }, { ok: true }]; const hc = new HealthChecker(provider, { intervalMs: 1000 }); hc.start(); await vi.runOnlyPendingTimersAsync(); await vi.advanceTimersByTimeAsync(1000); await vi.advanceTimersByTimeAsync(1000); expect((provider as any).idx).toBeGreaterThanOrEqual(3); hc.stop(); }); it('start() is idempotent — second call does not duplicate timer', async () => { const provider = new FakeProvider(); provider.results = [{ ok: true }]; const hc = new HealthChecker(provider, { intervalMs: 1000 }); hc.start(); hc.start(); // 즉시 1회 + 1s 후 1회 = 정확히 2. 두 timer 가 잘못 등록됐으면 4 (각 timer 마다 즉시+1s). await vi.advanceTimersByTimeAsync(1000); expect((provider as any).idx).toBe(2); hc.stop(); }); it('stop() clears timer (no further runOnce)', async () => { const provider = new FakeProvider(); provider.results = [{ ok: true }, { ok: true }]; const hc = new HealthChecker(provider, { intervalMs: 1000 }); hc.start(); await vi.runOnlyPendingTimersAsync(); const before = (provider as any).idx; hc.stop(); await vi.advanceTimersByTimeAsync(5000); expect((provider as any).idx).toBe(before); }); }); describe('HealthChecker — delta transitions + telemetry', () => { it('ok=true → ok=false 전이 시 onUpdate + ollama_unreachable emit', async () => { const provider = new FakeProvider(); provider.results = [{ ok: true }, { ok: false, reason: 'connection refused' }]; const updates: HealthResult[] = []; const events: HealthTelemetryEvent[] = []; const hc = new HealthChecker(provider, { onUpdate: (s) => updates.push(s), onTelemetry: (e) => events.push(e) }); await hc.runOnce(); await hc.runOnce(); expect(updates).toEqual([{ ok: false, reason: 'connection refused' }]); expect(events).toEqual([{ kind: 'ollama_unreachable', reason: 'connection refused' }]); }); it('ok=false → ok=true 전이 시 onUpdate + ollama_recovered emit (downtimeMs 정확)', async () => { const provider = new FakeProvider(); provider.results = [{ ok: false, reason: 'refused' }, { ok: true }]; const events: HealthTelemetryEvent[] = []; let nowCounter = 0; const hc = new HealthChecker(provider, { onTelemetry: (e) => events.push(e), now: () => { nowCounter += 1; return nowCounter * 1000; } }); await hc.runOnce(); await hc.runOnce(); const recovered = events.find((e) => e.kind === 'ollama_recovered'); expect(recovered).toEqual({ kind: 'ollama_recovered', downtimeMs: 1000 }); }); it('reason 변경만 (ok=false 유지) 시 onUpdate fire 하지만 telemetry emit 안 함', async () => { const provider = new FakeProvider(); provider.results = [ { ok: false, reason: 'refused' }, { ok: false, reason: 'timeout' } ]; const updates: HealthResult[] = []; const events: HealthTelemetryEvent[] = []; const hc = new HealthChecker(provider, { onUpdate: (s) => updates.push(s), onTelemetry: (e) => events.push(e) }); await hc.runOnce(); await hc.runOnce(); expect(updates).toHaveLength(2); expect(events).toEqual([{ kind: 'ollama_unreachable', reason: 'refused' }]); }); it('runOnce({manual:true}) 가 ollama_recheck_manual 1회 fire', async () => { const provider = new FakeProvider(); provider.results = [{ ok: true }]; const events: HealthTelemetryEvent[] = []; const hc = new HealthChecker(provider, { onTelemetry: (e) => events.push(e) }); await hc.runOnce({ manual: true }); expect(events).toEqual([{ kind: 'ollama_recheck_manual' }]); }); });