diff --git a/src/main/ai/AiWorker.ts b/src/main/ai/AiWorker.ts index 7eef0a8..fe1df6e 100644 --- a/src/main/ai/AiWorker.ts +++ b/src/main/ai/AiWorker.ts @@ -118,7 +118,7 @@ export class AiWorker { private async processJob(job: Job): Promise { const max = this.backoffsMs.length; for (let attempt = job.attempts; attempt < max; attempt++) { - const startMs = Date.now(); + const startMs = this.now().getTime(); try { const note = this.repo.findById(job.noteId); if (!note || note.aiStatus !== 'pending') return; @@ -150,8 +150,8 @@ export class AiWorker { kind: 'ai_succeeded', payload: { noteId: job.noteId, - durationMs: Date.now() - startMs, - attempts: attempt + durationMs: this.now().getTime() - startMs, + attempts: attempt + 1 } }).catch(() => {}); } diff --git a/src/main/services/TelemetryService.ts b/src/main/services/TelemetryService.ts index 2df5b45..9b2778a 100644 --- a/src/main/services/TelemetryService.ts +++ b/src/main/services/TelemetryService.ts @@ -55,9 +55,12 @@ export class TelemetryService { } async emit(input: EmitInput): Promise { - const ts = this.now().toISOString(); + // 회차 1 review (PR #13) — `now()` 한 번만 호출. KST 자정 경계에서 ts 와 파일명 일자가 + // 어긋나는 것을 방지. + const nowDate = this.now(); + const ts = nowDate.toISOString(); const event = validateEvent({ ts, kind: input.kind, payload: input.payload }); - const filePath = join(this.dir, `events-${todayKstIso(this.now())}.jsonl`); + const filePath = join(this.dir, `events-${todayKstIso(nowDate)}.jsonl`); try { await mkdir(this.dir, { recursive: true }); await appendFile(filePath, JSON.stringify(event) + '\n', 'utf8'); @@ -77,9 +80,14 @@ export class TelemetryService { } const cutoffMs = this.now().getTime() - this.retentionDays * 24 * 60 * 60 * 1000; const cutoffIso = todayKstIso(new Date(cutoffMs)); + // 회차 1 review (PR #13) — 매직 슬라이스 `n.slice(7, 17)` 대신 정규식 capture 그룹으로 + // 일자를 추출. prefix 변경 시 정규식 한 곳만 고치면 됨. + const datePattern = /^events-(\d{4}-\d{2}-\d{2})\.jsonl$/; const fileNames = entries - .filter((n) => /^events-\d{4}-\d{2}-\d{2}\.jsonl$/.test(n)) - .filter((n) => n.slice(7, 17) >= cutoffIso) + .filter((n) => { + const m = datePattern.exec(n); + return m !== null && m[1]! >= cutoffIso; + }) .sort(); for (const name of fileNames) { let raw: string; diff --git a/tests/unit/AiWorker.test.ts b/tests/unit/AiWorker.test.ts index ed490be..7e3803f 100644 --- a/tests/unit/AiWorker.test.ts +++ b/tests/unit/AiWorker.test.ts @@ -222,7 +222,9 @@ describe('AiWorker telemetry emit', () => { const succeeded = events.find((e) => e.kind === 'ai_succeeded'); expect(succeeded).toBeDefined(); expect(succeeded!.payload.noteId).toBe(id); - expect(succeeded!.payload.attempts).toBe(0); + // attempts = 시도한 횟수 (count, 1-based). 첫 시도 성공이므로 1. + // 회차 1 review (PR #13) 의 비대칭 의미 통일 결과 — 실패 경로의 `attempt + 1` 과 동일 의미. + expect(succeeded!.payload.attempts).toBe(1); expect(succeeded!.payload.durationMs).toBeGreaterThanOrEqual(0); });