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:
@@ -88,9 +88,14 @@ export class CaptureService {
|
|||||||
|
|
||||||
async restoreNote(noteId: string): Promise<void> {
|
async restoreNote(noteId: string): Promise<void> {
|
||||||
// 이미 active 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지.
|
// 이미 active 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지.
|
||||||
const note = this.repo.findById(noteId);
|
const before = this.repo.findById(noteId);
|
||||||
if (!note || note.deletedAt === null) return;
|
if (!before || before.deletedAt === null) return;
|
||||||
this.repo.restore(noteId);
|
// 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) {
|
if (this.deps.telemetry) {
|
||||||
await this.deps.telemetry.emit({ kind: 'restore', payload: { noteId } }).catch(() => {});
|
await this.deps.telemetry.emit({ kind: 'restore', payload: { noteId } }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', () => {
|
describe('CaptureService.retryAllFailed', () => {
|
||||||
let db: Database.Database;
|
let db: Database.Database;
|
||||||
let repo: NoteRepository;
|
let repo: NoteRepository;
|
||||||
|
|||||||
Reference in New Issue
Block a user