feat(ollama): telemetry 3 events — unreachable/recovered/recheck_manual (#1 v0.2.3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-02 01:30:26 +09:00
parent 12681e431c
commit a68ffe0aeb
6 changed files with 129 additions and 8 deletions

View File

@@ -148,7 +148,7 @@ describe('TelemetryService.readAllRecent', () => {
expect(events).toHaveLength(3);
// discriminant narrowing — noteId 없는 kind(empty_trash/expired_banner_shown/expired_batch_trash) 가 섞이면 명시적으로 실패
expect(events.map((e) =>
(e.kind === 'empty_trash' || e.kind === 'expired_banner_shown' || e.kind === 'expired_batch_trash')
(e.kind === 'empty_trash' || e.kind === 'expired_banner_shown' || e.kind === 'expired_batch_trash' || e.kind === 'ollama_unreachable' || e.kind === 'ollama_recovered' || e.kind === 'ollama_recheck_manual')
? null
: e.payload.noteId
)).toEqual(['a', 'b', 'b']);
@@ -164,7 +164,7 @@ describe('TelemetryService.readAllRecent', () => {
expect(events).toHaveLength(1);
const ev = events[0]!;
expect(ev.kind).toBe('capture');
if (ev.kind !== 'empty_trash' && ev.kind !== 'expired_banner_shown' && ev.kind !== 'expired_batch_trash') expect(ev.payload.noteId).toBe('a');
if (ev.kind !== 'empty_trash' && ev.kind !== 'expired_banner_shown' && ev.kind !== 'expired_batch_trash' && ev.kind !== 'ollama_unreachable' && ev.kind !== 'ollama_recovered' && ev.kind !== 'ollama_recheck_manual') expect(ev.payload.noteId).toBe('a');
});
it('returns [] when dir missing', async () => {

View File

@@ -195,3 +195,58 @@ describe('expired_banner_shown / expired_batch_trash events', () => {
})).toThrow();
});
});
describe('ollama_unreachable / ollama_recovered / ollama_recheck_manual events', () => {
it('parses valid ollama_unreachable', () => {
const ev = validateEvent({
ts: '2026-05-01T00:00:00.000Z',
kind: 'ollama_unreachable',
payload: { reason: 'connection refused' }
});
if (ev.kind !== 'ollama_unreachable') throw new Error('discriminant');
expect(ev.payload.reason).toBe('connection refused');
});
it('parses valid ollama_recovered', () => {
const ev = validateEvent({
ts: '2026-05-01T00:00:00.000Z',
kind: 'ollama_recovered',
payload: { downtimeMs: 60000 }
});
if (ev.kind !== 'ollama_recovered') throw new Error('discriminant');
expect(ev.payload.downtimeMs).toBe(60000);
});
it('parses valid ollama_recheck_manual (empty payload)', () => {
const ev = validateEvent({
ts: '2026-05-01T00:00:00.000Z',
kind: 'ollama_recheck_manual',
payload: {}
});
expect(ev.kind).toBe('ollama_recheck_manual');
});
it('rejects ollama_unreachable with extra payload field (privacy invariant)', () => {
expect(() => validateEvent({
ts: '2026-05-01T00:00:00.000Z',
kind: 'ollama_unreachable',
payload: { reason: 'refused', rawText: 'leak' }
})).toThrow();
});
it('rejects ollama_recovered with negative downtimeMs', () => {
expect(() => validateEvent({
ts: '2026-05-01T00:00:00.000Z',
kind: 'ollama_recovered',
payload: { downtimeMs: -1 }
})).toThrow();
});
it('rejects ollama_recheck_manual with non-empty payload (privacy invariant)', () => {
expect(() => validateEvent({
ts: '2026-05-01T00:00:00.000Z',
kind: 'ollama_recheck_manual',
payload: { foo: 'bar' }
})).toThrow();
});
});

View File

@@ -124,3 +124,30 @@ describe('aggregateStats — expired_banner_shown / expired_batch_trash', () =>
expect(r.md).toMatch(/만료 trash ratio.*N\/A/);
});
});
describe('aggregateStats — ollama_* events', () => {
it('counts 3 kinds per day and computes downtime average', () => {
const events = [
{ ts: '2026-05-01T00:00:00.000Z', kind: 'ollama_unreachable' as const, payload: { reason: 'refused' } },
{ ts: '2026-05-01T01:00:00.000Z', kind: 'ollama_recovered' as const, payload: { downtimeMs: 60000 } },
{ ts: '2026-05-01T02:00:00.000Z', kind: 'ollama_unreachable' as const, payload: { reason: 'timeout' } },
{ ts: '2026-05-01T03:00:00.000Z', kind: 'ollama_recovered' as const, payload: { downtimeMs: 120000 } },
{ ts: '2026-05-01T04:00:00.000Z', kind: 'ollama_recheck_manual' as const, payload: {} as Record<string, never> }
];
const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
expect(r.md).toContain('ollama_unreachable');
expect(r.md).toContain('ollama_recovered');
expect(r.md).toContain('ollama_recheck_manual');
// (60000 + 120000) / 2 = 90000
expect(r.md).toMatch(/평균 downtimeMs.*90000/);
expect(r.md).toMatch(/수동 recheck.*1/);
});
it('shows N/A for downtime when no recovered events', () => {
const events = [
{ ts: '2026-05-01T00:00:00.000Z', kind: 'ollama_unreachable' as const, payload: { reason: 'refused' } }
];
const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
expect(r.md).toMatch(/평균 downtimeMs.*N\/A/);
});
});