feat(ollama): #1 Ollama 회복 polling (v0.2.3 4/7) #16
Reference in New Issue
Block a user
Delete Branch "feat/v023-ollama"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
v0.2.3 cut 7항목 중 4번째 — HealthChecker 가 60s setInterval polling 으로 ollama 상태 자동 갱신, 회복 시 banner 자동 collapse. 수동 재확인 (OllamaBanner + tray 메뉴). Telemetry 3 events.
선행: PR #13 (#7 telemetry), PR #14 (#4 trash), PR #15 (#5 expiry).
Decisions (mini-brainstorm)
Architecture
Telemetry
Tests
17 신규 단위 (spec §6 의 12개 충족 + 5 over):
Gates
Test plan
Refs
🤖 Generated with Claude Code
§2.1 / §3.2 / §11 보강 — IPC handler 가 직접 telemetry.emit 안 하고 HealthChecker.runOnce({ manual: true }) 호출 → onTelemetry hook 으로 ollama_recheck_manual 발화. 단위 테스트 가능 (HealthChecker 레이어). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>v0.2.3 #1 Ollama recovery polling — spec/plan에 정확히 일치. HealthChecker.start/stop + delta-only onUpdate + 4 상태 전이 + onTelemetry hook + before-quit cleanup + IPC inbox:ollamaRecheck + ollama:status push + tray refreshTrayOllama + OllamaBanner 재확인 버튼이 모두 구현됨. 17 신규 단위 테스트 (HealthChecker 7 + telemetryEvents 6 + telemetryStats 2 + store 2) 가 spec §6 의 ≥12 요구를 초과 충족. 게이트 통과 확인: typecheck 0 errors / npm test 344 PASS / e2e 1/1. spec §10.1 매핑 매트릭스 7항목 모두 검증. 4 상태 전이 (true→true / true→false / false→true / false→false 같은/다른 reason) 단위로 가드. manual flag emit-before-call 의도 보존. zod 3 strict + reason max(500) privacy invariant 단위 회귀 가드. start/stop idempotent + before-quit 첫 statement 위치. note:updated 패턴 mirror 일관 (pushOllamaStatus / onOllamaStatus / 'ollama:status' channel). 머지 가능 — minor/nit 만 v0.2.4 backlog 누적 후보.
@@ -128,6 +143,7 @@ app.whenReady().then(async () => {let backupOnQuitDone = false;app.on('before-quit', (e) => {[minor] app.on('before-quit') 가 두 곳에 등록 (line 145 health/backup, line 349 trayInterval cleanup). 첫 핸들러가 e.preventDefault() 후 backup 완료 시 app.quit() → before-quit 재발화 → health.stop() 두 번 호출 (idempotent OK) + clearInterval(trayInterval) 가 두 번째 firing 에야 동작. 이는 pre-existing 패턴이지만 본 PR 가 health.stop() 을 추가하면서 '두 번째 before-quit 발화에서 무엇이 실행되는가' 의 ���드 리딩 부담이 늘어남. v0.2.4 에서 quit cleanup 을 단일 함수로 모으는 refactor 후보.
@@ -3,2 +15,4 @@const DEFAULT_INTERVAL_MS = 60_000;export class HealthChecker {private last: HealthResult = { ok: true };[minor] 초기 last = { ok: true } 가 첫 runOnce 에서 healthCheck 가 { ok: true, model: 'gemma4:e4b' } 반환 시 'no transition' 으로 분류되어 onUpdate 가 한 번도 fire 안 함 (okChanged=false, reasonChanged=undefined===undefined). renderer 는 startup 에서 inboxApi.getOllamaStatus() 로 fetch 해서 fine 하지만, push 채널만 의존하는 미래의 consumer 가 model 정보를 놓침. 의도라면 명시 주석 1줄 추가 권장 ('initial last is sentinel — first successful runOnce intentionally suppresses onUpdate when status remains ok'). v0.2.4 deferred OK.
@@ -10,0 +30,4 @@}async runOnce(opts?: { manual?: boolean }): Promise<HealthResult> {if (opts?.manual === true) {[nit] manual flag 의 ollama_recheck_manual emit 이 healthCheck 호출 전에 fire — 의도적이지만 (provider 실패해도 manual 카운트 보장), 만약 healthCheck 가 throw 하면 (현재 LocalOllamaProvider impl 는 안 함) recheck_manual 은 emit 됐지만 onUpdate 는 fire 안 한 inconsistent state. 주석 1줄 ('emit BEFORE healthCheck — manual count must survive provider exception') 으로 의도 문서화 권장.
@@ -10,0 +56,4 @@start(): void {if (this.timer !== null) return;void this.runOnce();[minor] start() 가 runOnce() 를 fire-and-forget (void) 로 즉시 호출. 첫 healthCheck 가 늦게 끝나는 동안 intervalMs 후 두 번째 runOnce 가 시작되면 둘 다 동시에 provider.healthCheck() 호출 → this.last 가 race 로 잘못된 순서로 저장 가능 (HTTP latency 가 intervalMs=60s 근접 시). 실사용에서는 /api/tags 가 ms 단위라 거의 발생 안 하지만, 테스트에서 fakeTimers 와 결합 시 발생 가능. 단위 테스트는 sequential await 사용하므로 가드 안 됨. v0.2.4 에서 'inFlight' guard 또는 last write wins 명시 주석 1줄 권장.
@@ -30,3 +33,3 @@window.addEventListener('focus', onFocus);return () => { unsub(); window.removeEventListener('focus', onFocus); };return () => { unsubNote(); unsubOllama(); window.removeEventListener('focus', onFocus); };}, [loadInitial, refreshMeta, upsertNote]);[nit] useEffect deps array 가 [loadInitial, refreshMeta, upsertNote] 만 — onOllamaStatus 의 setState 는 useInbox.setState 직접 호출이라 deps 영향 없으나, eslint exhaustive-deps 가 다음 추가 시 false positive 낼 수 있음. zustand 의 setState 는 stable reference 라 add 도 noop. 향후 다른 ev 추가 시 patterns 일관성 유지 권장.
@@ -14,0 +14,4 @@<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}><span style={{ flex: 1 }}>⚠ {message}</span><buttononClick={() => { void recheckOllama(); }}[nit] 재확인 button 의 onClick 이
void recheckOllama()사용 — recheckOllama 가 throw 하면 unhandled rejection. 현재 inboxApi.ollamaRecheck → ipcRenderer.invoke 는 IPC 채널이 없어진 케이스 외에는 reject 없고, IPC handler 자체가 healthCheck 가 throw 안 함을 가정. 안전하지만 try/catch 로 명시적 silent swallow 가 더 방어적. UI noise 없으니 nit 수준.@@ -0,0 +83,4 @@now: () => { nowCounter += 1; return nowCounter * 1000; }});await hc.runOnce();await hc.runOnce();[nit] start() idempotent 테스트가
idx <= 2만 검증. 만약 두 timer 가 동시에 1000ms 마다 fire 하면 idx 가 3 (즉시 1 + 1초 후 2개) 이 되겠지만, vi.advanceTimersByTimeAsync(1000) 가 두 timer 의 첫 fire 모두 실행하므로 idx 검증이 약함. private timer reference 직접 검증 (e.g., via reflection) 또는 5000ms advance 후 idx <= 6 같은 더 엄격한 invariant 권장. 본 테스트로도 명백한 leak 은 잡힘 — nit 수준.m1 — HealthChecker.last={ok:true} sentinel 의도 주석 (line 17). 첫 healthy=ok=true 면 transition 으로 인식 안 됨, ok=false 면 unreachable transition 으로 정상 인식. telemetry 누락 0. m2 — runOnce in-flight guard 추가. polling 첫 호출이 늦게 끝나는 동안 setInterval 가 두 번째 호출 시작하면 같은 promise 반환. healthCheck 가 idempotent HTTP 라 race 안전하지만, 이중 onUpdate/telemetry emit 회피. m3 — main.ts before-quit 핸들러 통합. trayInterval cleanup 별도 핸들러 (line 349) 제거하고 health.stop() 핸들러 안에 흡수. 모든 cleanup 한 곳. n1 — OllamaBanner 재확인 button 의 onClick 에 .catch 추가. recheckOllama Promise rejection 시 console.warn (silent swallow 회피). n2 — App.tsx useEffect deps array 의도 주석 1줄. onOllamaStatus 콜백이 useInbox.setState 직접 호출 — store reference 안정적이라 deps 불필요. n3 — HealthChecker idempotent test 강화. <=2 → ===2 (정확). 두 timer 등록되면 4 (각 timer 마다 즉시+1s) 가 됨. n4 — runOnce 의 manual emit 이 healthCheck *전에* fire 인 의도 주석. provider 실패 시에도 manual 카운트 1:1 보장. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>round 2: 7개 round 1 이슈 모두
a94c757단일 커밋에서 정확히 해결. typecheck 통과, unit 344/344, e2e 1/1. m1/n2/n4 의도 주석 명확. m2 in-flight guard (Promise field + finally 리셋) 정확. m3 trayInterval cleanup 첫 핸들러 통합. n1 Promise rejection .catch + console.warn. n3 idx===2 정확 비교.b6c3071은 artifact cleanup. 신규 regression 없음.