feat(v029): inbox:set-status + ai:classify-status (stub) IPC
Cut B Task 8 — Modal/NoteCard 메뉴 path 의 IPC backbone. - inbox:set-status: id + status + reason → repo.setStatus, invalid status 거부 - ai:classify-status: stub (Task 9 에서 Ollama provider 호출로 정식 구현) - types.ts InboxApi.setStatus / classifyStatus 시그니처 + preload wire-up - 4 단위 테스트 (valid/null reason/invalid status/stub shape)
This commit is contained in:
@@ -190,6 +190,31 @@ export function registerInboxApi(deps: InboxIpcDeps): void {
|
||||
trashed: deps.repo.countByStatus('trashed')
|
||||
}));
|
||||
|
||||
// v0.2.9 Cut B Task 8 — status 4분기 직접 전이 (사유 포함).
|
||||
// Modal 의 "완료/보관/휴지통" 버튼 path. backward compat 동기화는
|
||||
// NoteRepository.setStatus 내부에서 처리 (deleted_at sync).
|
||||
ipcMain.handle(
|
||||
'inbox:set-status',
|
||||
async (_e, id: string, status: NoteStatus, reason: string | null) => {
|
||||
const VALID: readonly NoteStatus[] = ['active', 'completed', 'archived', 'trashed'];
|
||||
if (!VALID.includes(status)) {
|
||||
return { ok: false as const, reason: 'invalid status' as const };
|
||||
}
|
||||
deps.repo.setStatus(id, status, reason);
|
||||
return { ok: true as const };
|
||||
}
|
||||
);
|
||||
|
||||
// v0.2.9 Cut B Task 8 — AI 자동 분류 stub. Task 9 에서 본격 구현
|
||||
// (Ollama provider 호출 + structured 출력). 본 task 는 Modal 의
|
||||
// 호출 path 만 working state 로 만들기 위한 안전 default.
|
||||
ipcMain.handle('ai:classify-status', async (_e, _id: string, _reason: string) => {
|
||||
return {
|
||||
recommended: 'archived' as const,
|
||||
rationale: '본 task 는 stub. Task 9 에서 정식 구현.'
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('inbox:saveOllamaSettings', async (_e, value: { endpoint: string; model: string }) => {
|
||||
// 검증: 새 인스턴스로 healthCheck
|
||||
const trial = new LocalOllamaProvider({ endpoint: value.endpoint, model: value.model });
|
||||
|
||||
@@ -71,6 +71,9 @@ const api: InklingApi = {
|
||||
// v0.2.9 Cut B Task 4 — status 별 list + counts.
|
||||
listByStatus: (status, opts) => ipcRenderer.invoke('inbox:list-by-status', status, opts ?? {}),
|
||||
countsByStatus: () => ipcRenderer.invoke('inbox:counts-by-status'),
|
||||
// v0.2.9 Cut B Task 8 — 4분기 status 전이 + AI 자동 분류 추천.
|
||||
setStatus: (id, status, reason) => ipcRenderer.invoke('inbox:set-status', id, status, reason),
|
||||
classifyStatus: (id, reason) => ipcRenderer.invoke('ai:classify-status', id, reason),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -138,6 +138,13 @@ export interface InboxApi {
|
||||
// v0.2.9 Cut B Task 4 — status 별 노트 목록 + status 별 count.
|
||||
listByStatus(status: NoteStatus, opts?: { limit?: number }): Promise<Note[]>;
|
||||
countsByStatus(): Promise<{ active: number; completed: number; archived: number; trashed: number }>;
|
||||
// v0.2.9 Cut B Task 8 — 4분기 status 전이 + AI 자동 분류 추천.
|
||||
setStatus(
|
||||
id: string,
|
||||
status: NoteStatus,
|
||||
reason: string | null
|
||||
): Promise<{ ok: true } | { ok: false; reason: string }>;
|
||||
classifyStatus(id: string, reason: string): Promise<{ recommended: NoteStatus; rationale: string }>;
|
||||
}
|
||||
|
||||
export interface InklingApi {
|
||||
|
||||
95
tests/unit/inboxApi-setStatus.test.ts
Normal file
95
tests/unit/inboxApi-setStatus.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { handlers, mockSetStatus } = vi.hoisted(() => ({
|
||||
handlers: {} as Record<string, (...args: unknown[]) => unknown>,
|
||||
mockSetStatus: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
default: {
|
||||
ipcMain: {
|
||||
handle: (ch: string, fn: (...args: unknown[]) => unknown) => {
|
||||
handlers[ch] = fn;
|
||||
}
|
||||
},
|
||||
dialog: {},
|
||||
shell: {}
|
||||
}
|
||||
}));
|
||||
|
||||
import { registerInboxApi } from '../../src/main/ipc/inboxApi';
|
||||
|
||||
function makeDeps(): Parameters<typeof registerInboxApi>[0] {
|
||||
// Minimal stub — `inbox:set-status` 핸들러는 deps.repo.setStatus 만 참조.
|
||||
return {
|
||||
repo: {
|
||||
setStatus: mockSetStatus,
|
||||
list: vi.fn(),
|
||||
listByStatus: vi.fn(),
|
||||
countByStatus: vi.fn(() => 0)
|
||||
} as never,
|
||||
continuity: {} as never,
|
||||
capture: {} as never,
|
||||
health: {} as never,
|
||||
intent: {} as never,
|
||||
getInboxWindow: () => null,
|
||||
settings: {} as never,
|
||||
providerHolder: {} as never,
|
||||
paths: { profileDir: '/profile' }
|
||||
};
|
||||
}
|
||||
|
||||
describe('inbox:set-status IPC', () => {
|
||||
beforeEach(() => {
|
||||
Object.keys(handlers).forEach((k) => delete handlers[k]);
|
||||
mockSetStatus.mockReset();
|
||||
});
|
||||
|
||||
it('forwards valid status + reason to repo.setStatus', async () => {
|
||||
registerInboxApi(makeDeps());
|
||||
const handler = handlers['inbox:set-status'];
|
||||
if (handler === undefined) throw new Error('handler not registered');
|
||||
const r = await handler(null, 'n1', 'completed', '결재 끝');
|
||||
expect(r).toEqual({ ok: true });
|
||||
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'completed', '결재 끝');
|
||||
});
|
||||
|
||||
it('forwards null reason as-is', async () => {
|
||||
registerInboxApi(makeDeps());
|
||||
const handler = handlers['inbox:set-status'];
|
||||
if (handler === undefined) throw new Error('handler not registered');
|
||||
const r = await handler(null, 'n1', 'archived', null);
|
||||
expect(r).toEqual({ ok: true });
|
||||
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'archived', null);
|
||||
});
|
||||
|
||||
it('rejects invalid status without calling repo', async () => {
|
||||
registerInboxApi(makeDeps());
|
||||
const handler = handlers['inbox:set-status'];
|
||||
if (handler === undefined) throw new Error('handler not registered');
|
||||
const r = (await handler(null, 'n1', 'invalid', null)) as { ok: boolean; reason?: string };
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.reason).toBe('invalid status');
|
||||
expect(mockSetStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ai:classify-status IPC (stub)', () => {
|
||||
beforeEach(() => {
|
||||
Object.keys(handlers).forEach((k) => delete handlers[k]);
|
||||
});
|
||||
|
||||
it('returns recommendation shape (stub default)', async () => {
|
||||
registerInboxApi(makeDeps());
|
||||
const handler = handlers['ai:classify-status'];
|
||||
if (handler === undefined) throw new Error('handler not registered');
|
||||
const r = (await handler(null, 'n1', '결재 끝')) as {
|
||||
recommended: string;
|
||||
rationale: string;
|
||||
};
|
||||
expect(typeof r.recommended).toBe('string');
|
||||
expect(['active', 'completed', 'archived', 'trashed']).toContain(r.recommended);
|
||||
expect(typeof r.rationale).toBe('string');
|
||||
expect(r.rationale.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user