From 274c171ee80892ddc04ef83d1647affbe30faaae Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Fri, 15 May 2026 11:03:09 +0900 Subject: [PATCH] =?UTF-8?q?fix(lifecycle):=20NoteStatus=20=EC=9D=98=20arch?= =?UTF-8?q?ived=20=EC=A0=9C=EA=B1=B0=20=E2=80=94=20MoveStatusModal/classif?= =?UTF-8?q?yStatus/store=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NoteStatus 에서 'archived' 제거 (active | completed | trashed 3분기) - MoveStatusModal ALL_STATUSES 에서 'archived' 제거 + statusLabel switch 정리 - classifyStatus VALID/FALLBACK/PROMPT 에서 archived 제거 → completed fallback - inboxApi IPC set-status VALID 배열에서 archived 제거, classify-status fallback → completed - store InboxView 에서 'archived' 제거, InboxCounts.archived 제거, archived: 0 spread 제거 - ImportService.applySyncFromDir — 기존 파일의 status=archived 를 completed 로 coerce - 영향 받는 tests 13개 파일 모두 update (archived → completed, 없어진 UI 옵션 제거) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/ai/classifyStatus.ts | 16 +++++---- src/main/ipc/inboxApi.ts | 10 +++--- src/main/services/ImportService.ts | 4 ++- .../inbox/components/MoveStatusModal.tsx | 5 ++- src/renderer/inbox/store.ts | 25 ++++++------- src/shared/types.ts | 7 ++-- tests/unit/App.test.tsx | 8 ++--- .../ImportService.applySyncFromDir.test.ts | 5 +-- tests/unit/MoveStatusModal.test.tsx | 35 +++++-------------- tests/unit/NoteRepository.test.ts | 25 +++++++------ tests/unit/classifyStatus.test.ts | 14 ++++---- tests/unit/inboxApi-setStatus.test.ts | 12 +++---- tests/unit/store.view.test.ts | 6 ++-- 13 files changed, 78 insertions(+), 94 deletions(-) diff --git a/src/main/ai/classifyStatus.ts b/src/main/ai/classifyStatus.ts index 8f87ac1..cee35fb 100644 --- a/src/main/ai/classifyStatus.ts +++ b/src/main/ai/classifyStatus.ts @@ -13,15 +13,16 @@ export interface ClassifyStatusOutput { rationale: string; } -const VALID: readonly NoteStatus[] = ['completed', 'archived', 'trashed']; +// v0.4 Task 16 — 'archived' 제거. completed / trashed 만 유효 추천값. +// AI 응답이 'archived' 를 반환해도 VALID 에 없으므로 FALLBACK(completed) 로 coerce됨. +const VALID: readonly NoteStatus[] = ['completed', 'trashed']; const PROMPT_TEMPLATE = `다음 메모를 분류하세요. 가능한 status: - completed: 작업이 끝났고 더 이상 행동 불필요 -- archived: 장기 보관 — 회수 가능, 지금은 보지 않음 - trashed: 불필요, 의미 없는 메모 -JSON 출력만 하세요: { "recommended": "completed|archived|trashed", "rationale": "<한 문장 한국어>" } +JSON 출력만 하세요: { "recommended": "completed|trashed", "rationale": "<한 문장 한국어>" } 메모 본문: {{rawText}} @@ -32,17 +33,18 @@ JSON 출력만 하세요: { "recommended": "completed|archived|trashed", "ration 사용자 이동 사유: {{reason}}`; +// v0.4 Task 16 — fallback 을 'completed' 로 변경 ('archived' 제거). const FALLBACK: ClassifyStatusOutput = { - recommended: 'archived', - rationale: '판단 실패 — 안전하게 보관 추천' + recommended: 'completed', + rationale: '판단 실패 — 완료 처리 추천' }; /** * v0.2.9 Cut B Task 9 — AI 자동 분류 (status 추천). * * provider.generateRaw 가 있으면 raw JSON 응답 사용, 없으면 generate() 재사용 시도 - * (그 경우 응답 형태 불일치로 보통 fallback). 에러/parse 실패 시 'archived' 안전 default - * (사용자 데이터 보존 우선). + * (그 경우 응답 형태 불일치로 보통 fallback). 에러/parse 실패 시 'completed' fallback + * (v0.4 Task 16 — 'archived' 제거, 안전 default 를 completed 로 변경). */ export async function classifyStatus( input: ClassifyStatusInput diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index 1eb1d07..24b7d13 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -230,7 +230,7 @@ export function registerInboxApi(deps: InboxIpcDeps): void { ipcMain.handle( 'inbox:set-status', async (_e, id: string, status: NoteStatus, reason: string | null) => { - const VALID: readonly NoteStatus[] = ['active', 'completed', 'archived', 'trashed']; + const VALID: readonly NoteStatus[] = ['active', 'completed', 'trashed']; if (!VALID.includes(status)) { return { ok: false as const, reason: 'invalid status' as const }; } @@ -242,14 +242,14 @@ export function registerInboxApi(deps: InboxIpcDeps): void { ); // v0.2.9 Cut B Task 9 — AI 자동 분류 (status 추천). - // Ollama provider.generateRaw 호출 + JSON 응답 파싱. 에러/실패 시 archived fallback - // (사용자 데이터 보존 우선). 자세한 prompt + parse 로직은 src/main/ai/classifyStatus.ts. + // Ollama provider.generateRaw 호출 + JSON 응답 파싱. 에러/실패 시 completed fallback + // (v0.4 Task 16 — 'archived' 제거). 자세한 prompt + parse 로직은 src/main/ai/classifyStatus.ts. ipcMain.handle('ai:classify-status', async (_e, id: string, reason: string) => { const note = deps.repo.findById(id); if (note === null) { return { - recommended: 'archived' as const, - rationale: '메모를 찾을 수 없음 — 안전하게 보관 추천' + recommended: 'completed' as const, + rationale: '메모를 찾을 수 없음 — 완료 처리 추천' }; } const provider = deps.providerHolder.get(); diff --git a/src/main/services/ImportService.ts b/src/main/services/ImportService.ts index d13eaac..676c524 100644 --- a/src/main/services/ImportService.ts +++ b/src/main/services/ImportService.ts @@ -150,7 +150,9 @@ export class ImportService { userIntent: parsed.userIntent, intentPromptedAt: parsed.intentPromptedAt, tags: parsed.tags, - status: parsed.status, + // v0.4 Task 16 — 'archived' 는 NoteStatus 에서 제거됨. 기존 내보내기 파일의 + // status=archived 를 읽을 때 completed 로 coerce (m008 과 동일 정책). + status: parsed.status === 'archived' ? 'completed' : parsed.status, statusChangedAt: parsed.statusChangedAt, moveReason: parsed.moveReason, dueDate: parsed.dueDate, diff --git a/src/renderer/inbox/components/MoveStatusModal.tsx b/src/renderer/inbox/components/MoveStatusModal.tsx index 77c276f..12907e8 100644 --- a/src/renderer/inbox/components/MoveStatusModal.tsx +++ b/src/renderer/inbox/components/MoveStatusModal.tsx @@ -18,8 +18,9 @@ interface Props { * 사유 입력 + AI 자동 분류 + 수동 status 선택. 버튼은 currentStatus 를 제외한 * 나머지 status 만 노출 (v0.3.6 까지는 완료/보관/휴지통 hardcode 라 완료/보관 노트가 * inbox 로 못 돌아오던 버그를 v0.3.7 에서 정정). + * v0.4 Task 16 — 'archived' 제거. active/completed/trashed 3개 옵션만 노출. */ -const ALL_STATUSES: readonly NoteStatus[] = ['active', 'completed', 'archived', 'trashed']; +const ALL_STATUSES: readonly NoteStatus[] = ['active', 'completed', 'trashed']; export function MoveStatusModal({ noteId, @@ -150,8 +151,6 @@ export function statusLabel(s: NoteStatus): string { return 'Inbox'; case 'completed': return '완료'; - case 'archived': - return '보관'; case 'trashed': return '휴지통'; } diff --git a/src/renderer/inbox/store.ts b/src/renderer/inbox/store.ts index b0c941e..601b230 100644 --- a/src/renderer/inbox/store.ts +++ b/src/renderer/inbox/store.ts @@ -9,16 +9,16 @@ function toTitleCase(s: string): string { export { selectFilteredNotes } from './selectFilteredNotes.js'; -// v0.2.9 Cut B Task 4 — 4탭 view enum + settings. -// 'inbox' = active, 'completed'/'archived' = NoteStatus 그대로, 'trash' = trashed (mirror), 'settings' = SettingsPage. +// v0.2.9 Cut B Task 4 — 3탭 view enum + settings. +// v0.4 Task 16 — 'archived' view 제거 (NoteStatus 에서 archived 삭제됨). +// 'inbox' = active, 'completed' = NoteStatus 그대로, 'trash' = trashed (mirror), 'settings' = SettingsPage. export type InboxView = - | 'inbox' | 'completed' | 'archived' | 'trash' | 'settings' + | 'inbox' | 'completed' | 'trash' | 'settings' | 'review-daily' | 'review-weekly' | 'review-monthly'; export interface InboxCounts { active: number; completed: number; - archived: number; trashed: number; } @@ -63,7 +63,7 @@ interface InboxState { setTagFilter: (tag: string | null) => void; setShowSettings: (open: boolean) => void; setView: (view: InboxView) => void; - loadByView: (view: 'inbox' | 'completed' | 'archived' | 'trash') => Promise; + loadByView: (view: 'inbox' | 'completed' | 'trash') => Promise; toggleShowTrash: () => Promise; loadTrash: () => Promise; restoreNote: (id: string) => Promise; @@ -110,7 +110,7 @@ export const useInbox = create((set, get) => ({ showTrash: false, showSettings: false, view: 'inbox', - counts: { active: 0, completed: 0, archived: 0, trashed: 0 }, + counts: { active: 0, completed: 0, trashed: 0 }, continuity: emptyContinuity, pendingCount: 0, ollamaStatus: { ok: true }, @@ -150,8 +150,7 @@ export const useInbox = create((set, get) => ({ inboxApi.countsByStatus(), inboxApi.getSettings() ]); - // v0.4 — countsByStatus 응답에서 archived 제거됨 (Task 16 UI 정리 예정). 0 fallback 유지. - set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts: { ...counts, archived: 0 }, ai_enabled: settings.ai_enabled ?? true, loading: false }); + set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false }); } catch (e) { // 첫 launch 의 IPC 실패 (DB migration 실패 / main process 비정상) 시 무한 loading 회피. // 빈 데이터로 진입하면 사용자가 캡처 시도 → 실제 fail 이 표면화 → 재시도 가능. @@ -173,8 +172,7 @@ export const useInbox = create((set, get) => ({ inboxApi.countsByStatus(), inboxApi.getSettings() ]); - // v0.4 — countsByStatus 응답에서 archived 제거됨 (Task 16 UI 정리 예정). 0 fallback 유지. - set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts: { ...counts, archived: 0 }, ai_enabled: settings.ai_enabled ?? true }); + set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true }); } catch (e) { // refreshMeta 는 background poll/event 에서 자주 호출 → fail 무시 (다음 호출에 회복). console.error('[inbox] refreshMeta failed', e); @@ -192,10 +190,9 @@ export const useInbox = create((set, get) => ({ const state = get(); const view = state.view; const showTrash = state.showTrash; - const viewStatus: 'active' | 'completed' | 'archived' | 'trashed' | null = + const viewStatus: 'active' | 'completed' | 'trashed' | null = view === 'inbox' ? 'active' : view === 'completed' ? 'completed' : - view === 'archived' ? 'archived' : view === 'trash' ? 'trashed' : null; // trashNotes — note.status='trashed' 면 upsert, 아니면 제거. @@ -276,7 +273,7 @@ export const useInbox = create((set, get) => ({ }); // status view 면 해당 status fetch. inbox 도 포함 — 다른 탭에서 돌아올 때 notes 가 // 이전 status 로 stale 한 상태이므로 재로드 필요. - if (view === 'inbox' || view === 'completed' || view === 'archived' || view === 'trash') { + if (view === 'inbox' || view === 'completed' || view === 'trash') { void get().loadByView(view); } // v0.2.11 Cut D — review-* view 진입 시 aggregate 로드. @@ -393,7 +390,7 @@ export const useInbox = create((set, get) => ({ } const view = get().view; // 회고/설정 view 일 때는 status filter 무의미 → 그대로 전체 검색 - const status = view === 'completed' || view === 'archived' || view === 'trash' + const status = view === 'completed' || view === 'trash' ? (view === 'trash' ? 'trashed' : view) : view === 'inbox' ? 'active' : undefined; try { diff --git a/src/shared/types.ts b/src/shared/types.ts index a5f2aa7..2fb75b1 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -13,8 +13,9 @@ export interface NoteMedia { export type AiStatus = 'pending' | 'done' | 'failed' | 'disabled'; -// v0.2.9 Cut B — 노트 status 4분기 (사용자 액션). m004 마이그레이션 + setStatus. -export type NoteStatus = 'active' | 'completed' | 'archived' | 'trashed'; +// v0.2.9 Cut B — 노트 status 3분기 (사용자 액션). m004 마이그레이션 + setStatus. +// v0.4 Task 16 — 'archived' 제거. m008 마이그레이션이 DB 의 archived 를 completed 로 통합. +export type NoteStatus = 'active' | 'completed' | 'trashed'; export interface NoteTag { name: string; @@ -205,7 +206,7 @@ export interface InboxApi { // v0.2.8 Cut A — 첨부 이미지를 OS 기본 뷰어로 열기 (Task 3). openMedia(relPath: string): Promise<{ ok: true } | { ok: false; reason: string }>; // v0.2.9 Cut B Task 4 — status 별 노트 목록 + status 별 count. - // v0.4 — notebookId 옵션 추가. archived 는 counts 응답에서 제거 (Task 16 UI 정리 예정). + // v0.4 — notebookId 옵션 추가. archived 제거 (Task 16 — NoteStatus 에서 제거됨). listByStatus(status: NoteStatus, opts?: { limit?: number; notebookId?: string }): Promise; countsByStatus(opts?: { notebookId?: string }): Promise<{ active: number; completed: number; trashed: number }>; // v0.2.9 Cut B Task 8 — 4분기 status 전이 + AI 자동 분류 추천. diff --git a/tests/unit/App.test.tsx b/tests/unit/App.test.tsx index 1079965..0111a18 100644 --- a/tests/unit/App.test.tsx +++ b/tests/unit/App.test.tsx @@ -86,7 +86,7 @@ describe('App — settings view', () => { cleanup(); useInbox.setState({ view: 'inbox', - counts: { active: 0, completed: 0, archived: 0, trashed: 0 }, + counts: { active: 0, completed: 0, trashed: 0 }, showSettings: false, showTrash: false, notes: [], trashNotes: [], trashCount: 0, sidebarVisible: false, notebooks: [], promotionCandidates: [] @@ -127,14 +127,14 @@ describe('App header — 3 tabs (v0.4)', () => { cleanup(); useInbox.setState({ view: 'inbox', - counts: { active: 5, completed: 3, archived: 2, trashed: 1 }, + counts: { active: 5, completed: 3, trashed: 1 }, notes: [], trashNotes: [], trashCount: 0, showTrash: false, showSettings: false, sidebarVisible: false, notebooks: [], promotionCandidates: [] }); // loadInitial 이 비동기로 counts 를 덮어씀 — onboarding wizard async gate (Task 12) 도입 // 후 render 가 await 후 발생하므로 mock 의 countsByStatus 가 테스트 기대값을 반환하도록 갱신. - // v0.4 — countsByStatus 응답에서 archived 제거 (store 가 archived:0 fallback 추가). + // v0.4 Task 16 — countsByStatus 응답에서 archived 제거 (NoteStatus 에서 삭제됨). vi.mocked(inboxApi.countsByStatus).mockResolvedValue({ active: 5, completed: 3, trashed: 1 }); }); @@ -187,7 +187,7 @@ describe('App — onboarding wizard', () => { cleanup(); useInbox.setState({ view: 'inbox', - counts: { active: 0, completed: 0, archived: 0, trashed: 0 }, + counts: { active: 0, completed: 0, trashed: 0 }, showSettings: false, showTrash: false, notes: [], trashNotes: [], trashCount: 0, sidebarVisible: false, notebooks: [], promotionCandidates: [] diff --git a/tests/unit/ImportService.applySyncFromDir.test.ts b/tests/unit/ImportService.applySyncFromDir.test.ts index 544bf3a..0e613da 100644 --- a/tests/unit/ImportService.applySyncFromDir.test.ts +++ b/tests/unit/ImportService.applySyncFromDir.test.ts @@ -77,7 +77,7 @@ describe('ImportService.applySyncFromDir', () => { expect(note?.rawText).toBe('new body'); }); - it('preserves status field from frontmatter', async () => { + it('preserves status field from frontmatter (archived coerced to completed — v0.4 Task 16)', async () => { const notesDir = join(workDir, 'notes'); await mkdir(notesDir, { recursive: true }); await writeFile( @@ -86,7 +86,8 @@ describe('ImportService.applySyncFromDir', () => { ); await svc.applySyncFromDir(workDir); const note = repo.findById('00000000-0000-0000-0000-000000000002'); - expect(note?.status).toBe('archived'); + // archived → completed coerce (m008 와 동일 정책, NoteStatus 에서 archived 삭제됨). + expect(note?.status).toBe('completed'); expect(note?.statusChangedAt).toBe('2026-05-08T00:00:00Z'); expect(note?.moveReason).toBe('done'); }); diff --git a/tests/unit/MoveStatusModal.test.tsx b/tests/unit/MoveStatusModal.test.tsx index 606f068..15e8fc5 100644 --- a/tests/unit/MoveStatusModal.test.tsx +++ b/tests/unit/MoveStatusModal.test.tsx @@ -26,7 +26,7 @@ describe('MoveStatusModal', () => { cleanup(); }); - it('renders reason textarea + 4 buttons + AI classify button', () => { + it('renders reason textarea + 3 buttons + AI classify button (v0.4 — 보관 제거)', () => { render( { ); expect(screen.getByRole('textbox')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '보관' })).toBeNull(); expect(screen.getByRole('button', { name: '휴지통' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /AI 자동 분류/ })).toBeInTheDocument(); }); @@ -84,7 +84,7 @@ describe('MoveStatusModal', () => { await waitFor(() => expect(onMoved).toHaveBeenCalledWith('completed', '결재 끝')); }); - it('currentStatus=completed → Inbox/보관/휴지통 노출, 완료 미노출', () => { + it('currentStatus=completed → Inbox/휴지통 노출, 완료/보관 미노출 (v0.4 — 보관 제거)', () => { render( { /> ); expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '보관' })).toBeNull(); expect(screen.getByRole('button', { name: '휴지통' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: '완료' })).toBeNull(); }); - it('currentStatus=archived → Inbox 버튼 클릭 시 setStatus("active") 호출', async () => { - const onMoved = vi.fn(); - render( - - ); - fireEvent.click(screen.getByRole('button', { name: 'Inbox' })); - await waitFor(() => { - expect(mockSetStatus).toHaveBeenCalledWith('n1', 'active', null); - expect(onMoved).toHaveBeenCalledWith('active', null); - }); - }); + // v0.4 Task 16 — currentStatus=archived 는 NoteStatus 에서 제거됨. 테스트 제거. - it('currentStatus=trashed → Inbox/완료/보관 노출, 휴지통 미노출', () => { + it('currentStatus=trashed → Inbox/완료 노출, 휴지통/보관 미노출 (v0.4 — 보관 제거)', () => { render( { ); expect(screen.getByRole('button', { name: 'Inbox' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '완료' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '보관' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '보관' })).toBeNull(); expect(screen.queryByRole('button', { name: '휴지통' })).toBeNull(); }); @@ -185,7 +168,7 @@ describe('MoveStatusModal', () => { onMoved={onMoved} /> ); - fireEvent.click(screen.getByRole('button', { name: '보관' })); - await waitFor(() => expect(mockSetStatus).toHaveBeenCalledWith('n1', 'archived', null)); + fireEvent.click(screen.getByRole('button', { name: '완료' })); + await waitFor(() => expect(mockSetStatus).toHaveBeenCalledWith('n1', 'completed', null)); }); }); diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 420881f..f123e9d 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -940,9 +940,9 @@ describe('NoteRepository — setStatus + listByStatus', () => { it('setStatus accepts null reason', () => { const { id } = repo.create({ rawText: 'test' }); - repo.setStatus(id, 'archived', null, new Date('2026-05-10T00:00:00.000Z')); + repo.setStatus(id, 'completed', null, new Date('2026-05-10T00:00:00.000Z')); const note = repo.findById(id)!; - expect(note.status).toBe('archived'); + expect(note.status).toBe('completed'); expect(note.moveReason).toBeNull(); }); @@ -960,14 +960,14 @@ describe('NoteRepository — setStatus + listByStatus', () => { it('listByStatus filters correctly', () => { const idA = repo.create({ rawText: 'a' }).id; const idB = repo.create({ rawText: 'b' }).id; - repo.setStatus(idB, 'archived', null, new Date('2026-05-10T00:00:00.000Z')); + repo.setStatus(idB, 'completed', null, new Date('2026-05-10T00:00:00.000Z')); const active = repo.listByStatus('active', { limit: 10 }); - const archived = repo.listByStatus('archived', { limit: 10 }); + const completed = repo.listByStatus('completed', { limit: 10 }); expect(active.map((n) => n.id)).toContain(idA); expect(active.map((n) => n.id)).not.toContain(idB); - expect(archived.map((n) => n.id)).toContain(idB); - expect(archived.map((n) => n.id)).not.toContain(idA); + expect(completed.map((n) => n.id)).toContain(idB); + expect(completed.map((n) => n.id)).not.toContain(idA); }); it('listByStatus orders by status_changed_at DESC (NULL falls back to created_at)', () => { @@ -984,10 +984,10 @@ describe('NoteRepository — setStatus + listByStatus', () => { it('listByStatus respects limit (cap 200)', () => { for (let i = 0; i < 5; i++) { const id = repo.create({ rawText: `n${i}` }).id; - repo.setStatus(id, 'archived', null, new Date(`2026-05-${10 + i}T00:00:00.000Z`)); + repo.setStatus(id, 'completed', null, new Date(`2026-05-${10 + i}T00:00:00.000Z`)); } - expect(repo.listByStatus('archived', { limit: 3 })).toHaveLength(3); - expect(repo.listByStatus('archived', { limit: 100 })).toHaveLength(5); + expect(repo.listByStatus('completed', { limit: 3 })).toHaveLength(3); + expect(repo.listByStatus('completed', { limit: 100 })).toHaveLength(5); }); it('listByStatus default limit 200', () => { @@ -1016,7 +1016,7 @@ describe('NoteRepository — setStatus + listByStatus', () => { expect(repo.findById(id)!.deletedAt).toBeNull(); }); - it('setStatus("completed"/"archived") also clears deleted_at', () => { + it('setStatus("completed") also clears deleted_at', () => { const { id } = repo.create({ rawText: 'r' }); repo.setStatus(id, 'trashed', null, new Date('2026-05-15T00:00:00.000Z')); repo.setStatus(id, 'completed', null, new Date('2026-05-16T00:00:00.000Z')); @@ -1037,13 +1037,12 @@ describe('NoteRepository — setStatus + listByStatus', () => { const c = repo.create({ rawText: 'c' }).id; repo.setStatus(c, 'completed', null, new Date('2026-05-10T00:00:00.000Z')); const d = repo.create({ rawText: 'd' }).id; - repo.setStatus(d, 'archived', null, new Date('2026-05-10T00:00:00.000Z')); + repo.setStatus(d, 'completed', null, new Date('2026-05-10T00:00:00.000Z')); const e = repo.create({ rawText: 'e' }).id; repo.setStatus(e, 'trashed', null, new Date('2026-05-10T00:00:00.000Z')); expect(repo.countByStatus('active')).toBe(2); - expect(repo.countByStatus('completed')).toBe(1); - expect(repo.countByStatus('archived')).toBe(1); + expect(repo.countByStatus('completed')).toBe(2); expect(repo.countByStatus('trashed')).toBe(1); // sanity — a 가 여전히 active. expect(repo.findById(a)!.status).toBe('active'); diff --git a/tests/unit/classifyStatus.test.ts b/tests/unit/classifyStatus.test.ts index 358bf43..b4fad10 100644 --- a/tests/unit/classifyStatus.test.ts +++ b/tests/unit/classifyStatus.test.ts @@ -28,7 +28,7 @@ describe('classifyStatus', () => { expect(r.rationale).toBe('처리됨'); }); - it('falls back to archived on parse failure (invalid JSON)', async () => { + it('falls back to completed on parse failure (invalid JSON)', async () => { const provider = makeProvider(vi.fn(async () => 'not json')); const r = await classifyStatus({ provider, @@ -36,11 +36,11 @@ describe('classifyStatus', () => { summary: '', reason: 'r' }); - expect(r.recommended).toBe('archived'); + expect(r.recommended).toBe('completed'); expect(r.rationale).toMatch(/판단 실패|보관/); }); - it('falls back to archived on invalid status value', async () => { + it('falls back to completed on invalid status value', async () => { const provider = makeProvider( vi.fn(async () => '{"recommended":"unknown","rationale":"x"}') ); @@ -50,7 +50,7 @@ describe('classifyStatus', () => { summary: '', reason: 'r' }); - expect(r.recommended).toBe('archived'); + expect(r.recommended).toBe('completed'); }); it('handles provider throw', async () => { @@ -65,7 +65,7 @@ describe('classifyStatus', () => { summary: '', reason: 'r' }); - expect(r.recommended).toBe('archived'); + expect(r.recommended).toBe('completed'); expect(r.rationale).toMatch(/판단 실패|보관/); }); @@ -77,13 +77,13 @@ describe('classifyStatus', () => { summary: '', reason: 'r' }); - expect(r.recommended).toBe('archived'); + expect(r.recommended).toBe('completed'); expect(r.rationale).toMatch(/판단 실패|보관/); }); it('substitutes empty inputs with placeholder text in prompt', async () => { const generateRaw = vi.fn( - async (_p: string) => '{"recommended":"archived","rationale":"ok"}' + async (_p: string) => '{"recommended":"completed","rationale":"ok"}' ); const provider = makeProvider(generateRaw); await classifyStatus({ provider, rawText: '', summary: '', reason: '' }); diff --git a/tests/unit/inboxApi-setStatus.test.ts b/tests/unit/inboxApi-setStatus.test.ts index a74c4fd..36b6542 100644 --- a/tests/unit/inboxApi-setStatus.test.ts +++ b/tests/unit/inboxApi-setStatus.test.ts @@ -69,9 +69,9 @@ describe('inbox:set-status IPC', () => { 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); + const r = await handler(null, 'n1', 'trashed', null); expect(r).toEqual({ ok: true }); - expect(mockSetStatus).toHaveBeenCalledWith('n1', 'archived', null); + expect(mockSetStatus).toHaveBeenCalledWith('n1', 'trashed', null); }); it('rejects invalid status without calling repo', async () => { @@ -130,7 +130,7 @@ describe('ai:classify-status IPC', () => { expect(prompt).toContain('결재'); }); - it('returns archived fallback when note not found', async () => { + it('returns completed fallback when note not found (v0.4 — archived 제거)', async () => { mockFindById.mockReturnValue(null); registerInboxApi(makeDeps()); const handler = handlers['ai:classify-status']; @@ -139,12 +139,12 @@ describe('ai:classify-status IPC', () => { recommended: string; rationale: string; }; - expect(r.recommended).toBe('archived'); + expect(r.recommended).toBe('completed'); expect(r.rationale.length).toBeGreaterThan(0); expect(mockGenerateRaw).not.toHaveBeenCalled(); }); - it('returns archived fallback when AI throws', async () => { + it('returns completed fallback when AI throws (v0.4 — archived 제거)', async () => { mockFindById.mockReturnValue({ id: 'n1', rawText: 't', @@ -158,6 +158,6 @@ describe('ai:classify-status IPC', () => { recommended: string; rationale: string; }; - expect(r.recommended).toBe('archived'); + expect(r.recommended).toBe('completed'); }); }); diff --git a/tests/unit/store.view.test.ts b/tests/unit/store.view.test.ts index 4615275..68f6742 100644 --- a/tests/unit/store.view.test.ts +++ b/tests/unit/store.view.test.ts @@ -5,7 +5,7 @@ const mockApi = { listNotes: vi.fn(async () => [] as Note[]), listTrash: vi.fn(async () => [] as Note[]), listByStatus: vi.fn(async () => [] as Note[]), - countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 })), + countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, trashed: 0 })), getTrashCount: vi.fn(async () => 0), getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, @@ -40,7 +40,7 @@ describe('inbox store — view enum', () => { const { useInbox } = await import('../../src/renderer/inbox/store.js'); useInbox.setState({ view: 'inbox', - counts: { active: 0, completed: 0, archived: 0, trashed: 0 }, + counts: { active: 0, completed: 0, trashed: 0 }, notes: [], trashNotes: [], trashCount: 0, showTrash: false, showSettings: false, loading: false, tagFilter: null, pendingCount: 0, todayCount: 0, @@ -65,7 +65,7 @@ describe('inbox store — view enum', () => { it('counts initialized to zero per status', async () => { const { useInbox } = await import('../../src/renderer/inbox/store.js'); - expect(useInbox.getState().counts).toEqual({ active: 0, completed: 0, archived: 0, trashed: 0 }); + expect(useInbox.getState().counts).toEqual({ active: 0, completed: 0, trashed: 0 }); }); it('backward-compat: showTrash mirrors view==="trash"', async () => {