feat(retry): CaptureService.retryAllFailed + IPC 2 channels (#2 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,9 @@ export function registerInboxApi(deps: InboxIpcDeps): void {
|
||||
await deps.health.runOnce({ manual: true });
|
||||
return deps.health.lastStatus();
|
||||
});
|
||||
|
||||
ipcMain.handle('inbox:retryAllFailed', async () => deps.capture.retryAllFailed());
|
||||
ipcMain.handle('inbox:failedCount', () => deps.repo.countFailed());
|
||||
}
|
||||
|
||||
export function pushNoteUpdated(getWin: () => BrowserWindow | null, note: Note): void {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface TelemetryEmitter {
|
||||
| { kind: 'empty_trash'; payload: { count: number } }
|
||||
| { kind: 'expired_banner_shown'; payload: { candidateCount: number } }
|
||||
| { kind: 'expired_batch_trash'; payload: { count: number } }
|
||||
| { kind: 'ai_retry_manual'; payload: { failedCount: number } }
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -159,4 +160,23 @@ export class CaptureService {
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 ai_status='failed' (active) 노트를 'pending' 으로 reset + worker.enqueue 재투입.
|
||||
* 빈 결과는 telemetry emit 안 함 (failedCount ≥ 1 invariant).
|
||||
* v0.2.3 #2 retry-all manual trigger.
|
||||
*/
|
||||
async retryAllFailed(): Promise<{ count: number }> {
|
||||
const { ids } = this.repo.retryAllFailed(new Date().toISOString());
|
||||
for (const id of ids) {
|
||||
await this.deps.enqueue(id);
|
||||
}
|
||||
if (ids.length > 0 && this.deps.telemetry) {
|
||||
await this.deps.telemetry.emit({
|
||||
kind: 'ai_retry_manual',
|
||||
payload: { failedCount: ids.length }
|
||||
}).catch(() => {});
|
||||
}
|
||||
return { count: ids.length };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ const api: InklingApi = {
|
||||
const listener = (_e: unknown, status: { ok: boolean; reason?: string }) => cb(status);
|
||||
ipcRenderer.on('ollama:status', listener);
|
||||
return () => ipcRenderer.off('ollama:status', listener);
|
||||
}
|
||||
},
|
||||
retryAllFailed: () => ipcRenderer.invoke('inbox:retryAllFailed'),
|
||||
getFailedCount: () => ipcRenderer.invoke('inbox:failedCount')
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ export interface InboxApi {
|
||||
ollamaRecheck(): Promise<{ ok: boolean; reason?: string }>;
|
||||
onNoteUpdated(cb: (note: Note) => void): () => void;
|
||||
onOllamaStatus(cb: (status: { ok: boolean; reason?: string }) => void): () => void;
|
||||
retryAllFailed(): Promise<{ count: number }>;
|
||||
getFailedCount(): Promise<number>;
|
||||
}
|
||||
|
||||
export interface InklingApi {
|
||||
|
||||
@@ -323,3 +323,53 @@ describe('CaptureService.trashExpiredBatch', () => {
|
||||
expect(calls.filter((c) => c.kind === 'expired_batch_trash')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CaptureService.retryAllFailed', () => {
|
||||
let db: Database.Database;
|
||||
let repo: NoteRepository;
|
||||
let store: MediaStore;
|
||||
let tmp: string;
|
||||
let calls: Array<{ kind: string; payload: any }>;
|
||||
let enqueued: string[];
|
||||
let svc: CaptureService;
|
||||
|
||||
function makeFailed(rawText: string): string {
|
||||
const { id } = repo.create({ rawText });
|
||||
db.prepare(`UPDATE notes SET ai_status='failed', ai_error='boom' WHERE id=?`).run(id);
|
||||
db.prepare(`DELETE FROM pending_jobs WHERE note_id=?`).run(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
repo = new NoteRepository(db);
|
||||
tmp = mkdtempSync(join(tmpdir(), 'inkling-capture-'));
|
||||
store = new MediaStore(tmp);
|
||||
calls = [];
|
||||
enqueued = [];
|
||||
svc = new CaptureService(repo, store, {
|
||||
enqueue: async (id) => { enqueued.push(id); },
|
||||
celebrate: () => {},
|
||||
telemetry: { emit: async (input) => { calls.push(input as any); } }
|
||||
});
|
||||
});
|
||||
|
||||
it('retryAllFailed — enqueue per id + ai_retry_manual emit', async () => {
|
||||
const a = makeFailed('a');
|
||||
const b = makeFailed('b');
|
||||
const r = await svc.retryAllFailed();
|
||||
expect(r.count).toBe(2);
|
||||
expect(enqueued.sort()).toEqual([a, b].sort());
|
||||
expect(calls).toContainEqual(
|
||||
expect.objectContaining({ kind: 'ai_retry_manual', payload: { failedCount: 2 } })
|
||||
);
|
||||
});
|
||||
|
||||
it('retryAllFailed empty — count=0, no emit', async () => {
|
||||
const r = await svc.retryAllFailed();
|
||||
expect(r.count).toBe(0);
|
||||
expect(enqueued).toEqual([]);
|
||||
expect(calls.filter((c) => c.kind === 'ai_retry_manual')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user