feat(retry): telemetry ai_retry_manual + stats AI 수동 재시도 (#2 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 === 'ollama_unreachable' || e.kind === 'ollama_recovered' || e.kind === 'ollama_recheck_manual')
|
||||
(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' || e.kind === 'ai_retry_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' && ev.kind !== 'ollama_unreachable' && ev.kind !== 'ollama_recovered' && ev.kind !== 'ollama_recheck_manual') 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' && ev.kind !== 'ai_retry_manual') expect(ev.payload.noteId).toBe('a');
|
||||
});
|
||||
|
||||
it('returns [] when dir missing', async () => {
|
||||
|
||||
@@ -250,3 +250,31 @@ describe('ollama_unreachable / ollama_recovered / ollama_recheck_manual events',
|
||||
})).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ai_retry_manual event', () => {
|
||||
it('parses valid ai_retry_manual', () => {
|
||||
const ev = validateEvent({
|
||||
ts: '2026-05-01T00:00:00.000Z',
|
||||
kind: 'ai_retry_manual',
|
||||
payload: { failedCount: 5 }
|
||||
});
|
||||
if (ev.kind !== 'ai_retry_manual') throw new Error('discriminant');
|
||||
expect(ev.payload.failedCount).toBe(5);
|
||||
});
|
||||
|
||||
it('rejects ai_retry_manual with failedCount=0 (≥1 invariant)', () => {
|
||||
expect(() => validateEvent({
|
||||
ts: '2026-05-01T00:00:00.000Z',
|
||||
kind: 'ai_retry_manual',
|
||||
payload: { failedCount: 0 }
|
||||
})).toThrow();
|
||||
});
|
||||
|
||||
it('rejects ai_retry_manual with extra payload field (privacy invariant)', () => {
|
||||
expect(() => validateEvent({
|
||||
ts: '2026-05-01T00:00:00.000Z',
|
||||
kind: 'ai_retry_manual',
|
||||
payload: { failedCount: 5, rawText: 'leak' }
|
||||
})).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,3 +151,16 @@ describe('aggregateStats — ollama_* events', () => {
|
||||
expect(r.md).toMatch(/평균 downtimeMs.*N\/A/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateStats — ai_retry_manual', () => {
|
||||
it('counts events and sums failedCount', () => {
|
||||
const events = [
|
||||
{ ts: '2026-05-01T00:00:00.000Z', kind: 'ai_retry_manual' as const, payload: { failedCount: 3 } },
|
||||
{ ts: '2026-05-01T01:00:00.000Z', kind: 'ai_retry_manual' as const, payload: { failedCount: 7 } }
|
||||
];
|
||||
const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
|
||||
expect(r.md).toContain('ai_retry_manual');
|
||||
// 2회 / 누적 10건
|
||||
expect(r.md).toMatch(/AI 수동 재시도.*2회.*10건/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user