fix(v026): PR #24 round 1 Critical — B1 production path activation

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) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-05 01:58:27 +09:00
parent 54e2f5b10f
commit a991008689
2 changed files with 54 additions and 3 deletions

View File

@@ -88,9 +88,14 @@ export class CaptureService {
async restoreNote(noteId: string): Promise<void> {
// 이미 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(() => {});
}

View File

@@ -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;