From a991008689c95b0c341a7139a0543c973b91bfae Mon Sep 17 00:00:00 2001 From: altair823 Date: Tue, 5 May 2026 01:58:27 +0900 Subject: [PATCH] =?UTF-8?q?fix(v026):=20PR=20#24=20round=201=20Critical=20?= =?UTF-8?q?=E2=80=94=20B1=20production=20path=20activation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 1 reviewer 발견: B1 (#10) fix 가 dead code. NoteRepository.restoreNote 새 메서드는 unit test 만 호출, production path (CaptureService.restoreNote) 는 옛 repo.restore() 호출 → ai_status reset + pending_jobs INSERT 우회. Fix: - CaptureService.restoreNote 가 repo.restoreNote 호출 - before 의 ai_status 가 'failed' or 'pending' 이면 worker.enqueue(id) 도 호출 (in-memory queue 갱신 — restoreNote 가 DB 만 갱신하면 다음 app start 까지 처리 안 됨) Round 1 Important 도 함께 처리. 단위 +2 cases (failed → enqueue, done → skip enqueue). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/services/CaptureService.ts | 11 +++++-- tests/unit/CaptureService.test.ts | 46 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/services/CaptureService.ts b/src/main/services/CaptureService.ts index e67ae3e..4dc03e2 100644 --- a/src/main/services/CaptureService.ts +++ b/src/main/services/CaptureService.ts @@ -88,9 +88,14 @@ export class CaptureService { async restoreNote(noteId: string): Promise { // 이미 active 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지. - const note = this.repo.findById(noteId); - if (!note || note.deletedAt === null) return; - this.repo.restore(noteId); + const before = this.repo.findById(noteId); + if (!before || before.deletedAt === null) return; + // v0.2.6 #10 — production path: repo.restoreNote (ai_status reset + pending_jobs 재생성) + this.repo.restoreNote(noteId); + // v0.2.6 #10 — in-memory AiWorker queue 갱신: DB 갱신만으로는 다음 앱 실행 시까지 처리 X + if (before.aiStatus === 'failed' || before.aiStatus === 'pending') { + await this.deps.enqueue(noteId); + } if (this.deps.telemetry) { await this.deps.telemetry.emit({ kind: 'restore', payload: { noteId } }).catch(() => {}); } diff --git a/tests/unit/CaptureService.test.ts b/tests/unit/CaptureService.test.ts index 51e19e8..513f9fc 100644 --- a/tests/unit/CaptureService.test.ts +++ b/tests/unit/CaptureService.test.ts @@ -324,6 +324,52 @@ describe('CaptureService.trashExpiredBatch', () => { }); }); +describe('CaptureService.restoreNote — enqueue on failed/pending (#10 production path)', () => { + let db: Database.Database; + let repo: NoteRepository; + let store: MediaStore; + let tmp: string; + let enqueued: string[]; + let svc: CaptureService; + + beforeEach(() => { + db = new Database(':memory:'); + runMigrations(db); + repo = new NoteRepository(db); + tmp = mkdtempSync(join(tmpdir(), 'inkling-restore-')); + store = new MediaStore(tmp); + enqueued = []; + svc = new CaptureService(repo, store, { + enqueue: async (id) => { enqueued.push(id); }, + celebrate: () => {} + }); + }); + + it('restoreNote calls worker.enqueue when restoring failed note', async () => { + const { id } = repo.create({ rawText: 'x' }); + repo.markAiFailed(id, 'unreachable'); + repo.trash(id, new Date().toISOString()); + enqueued.length = 0; // reset + + await svc.restoreNote(id); + + expect(repo.findById(id)!.aiStatus).toBe('pending'); + expect(enqueued).toContain(id); + }); + + it('restoreNote does not enqueue done note', async () => { + const { id } = repo.create({ rawText: 'x' }); + repo.updateAiResult(id, { title: 't', summary: 'a\nb\nc', tags: ['x'], provider: 'p' }); + repo.trash(id, new Date().toISOString()); + enqueued.length = 0; // reset + + await svc.restoreNote(id); + + expect(repo.findById(id)!.aiStatus).toBe('done'); + expect(enqueued).not.toContain(id); + }); +}); + describe('CaptureService.retryAllFailed', () => { let db: Database.Database; let repo: NoteRepository;