Files
inkling/tests/unit/store.trash.test.ts
altair823 df85b88424 fix(trash): T13 review — trashCount clobber guard + restoreNote test (review I1+I2+M5)
- I1: trashCount 가 upsertNote 안에서 항상 trashNotes.length 로 덮어써져
  server 값 (refreshMeta) 손상. showTrash=true (trashNotes cache-loaded)
  일 때만 local recompute.
- I2: restoreNote 의 "fallback for missed event" 주석 부정확 — main 은
  trash/restore 시 pushNoteUpdated 안 보냄. 자가 갱신이 primary mechanism.
  주석 정정.
- M5: restoreNote 테스트가 IPC 호출만 검증, 노트 이동 미검증. trashNotes
  → notes 라우팅 + deletedAt=null 어설션 추가. + I1 회귀 가드 테스트 신규.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:43:59 +09:00

105 lines
5.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Note } from '@shared/types';
const mockApi = {
listNotes: vi.fn(async () => [] as Note[]),
listTrash: vi.fn(async () => [] as Note[]),
getTrashCount: vi.fn(async () => 0),
getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null })),
getPendingCount: vi.fn(async () => 0),
getOllamaStatus: vi.fn(async () => ({ ok: true })),
getTodayCount: vi.fn(async () => 0),
restoreNote: vi.fn(async () => {}),
permanentDeleteNote: vi.fn(async () => ({ confirmed: true })),
emptyTrash: vi.fn(async () => ({ confirmed: true, count: 0 })),
deleteNote: vi.fn(async () => {}),
onNoteUpdated: vi.fn(() => () => {}),
updateAiFields: vi.fn(async () => {}),
setDueDate: vi.fn(async () => {}),
setIntent: vi.fn(async () => {}),
dismissIntent: vi.fn(async () => {})
};
vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: mockApi }));
const noteStub = (id: string, deletedAt: string | null = null): Note => ({
id, rawText: 'x',
aiTitle: null, aiSummary: null, aiStatus: 'done', aiError: null,
aiProvider: null, aiGeneratedAt: null,
titleEditedByUser: false, summaryEditedByUser: false,
userIntent: null, intentPromptedAt: null,
dueDate: null, dueDateEditedByUser: false,
deletedAt, lastRecalledAt: null, recallDismissedAt: null,
createdAt: '2026-05-01T00:00:00Z', updatedAt: '2026-05-01T00:00:00Z',
tags: [], media: []
});
describe('useInbox — trash state (v0.2.3 #4)', () => {
beforeEach(async () => {
const { useInbox } = await import('../../src/renderer/inbox/store.js');
useInbox.setState({
notes: [], trashNotes: [], trashCount: 0, showTrash: false,
loading: false, tagFilter: null, pendingCount: 0, todayCount: 0,
ollamaStatus: { ok: true },
continuity: { weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null }
});
Object.values(mockApi).forEach((fn) => 'mockClear' in fn && (fn as any).mockClear());
});
it('toggleShowTrash flips state and triggers loadTrash on enter', async () => {
mockApi.listTrash.mockResolvedValueOnce([noteStub('t1', '2026-05-01T00:00:00Z')]);
const { useInbox } = await import('../../src/renderer/inbox/store.js');
await useInbox.getState().toggleShowTrash();
expect(useInbox.getState().showTrash).toBe(true);
expect(useInbox.getState().trashNotes).toHaveLength(1);
expect(mockApi.listTrash).toHaveBeenCalled();
await useInbox.getState().toggleShowTrash();
expect(useInbox.getState().showTrash).toBe(false);
});
it('upsertNote routes to trashNotes when deletedAt IS NOT NULL', async () => {
const { useInbox } = await import('../../src/renderer/inbox/store.js');
useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z'));
expect(useInbox.getState().notes).toHaveLength(0);
expect(useInbox.getState().trashNotes).toHaveLength(1);
});
it('upsertNote moves note from notes to trashNotes when trashed', async () => {
const { useInbox } = await import('../../src/renderer/inbox/store.js');
useInbox.getState().upsertNote(noteStub('a'));
expect(useInbox.getState().notes).toHaveLength(1);
useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z'));
expect(useInbox.getState().notes).toHaveLength(0);
expect(useInbox.getState().trashNotes).toHaveLength(1);
});
it('restoreNote calls api + moves note from trashNotes to notes (낙관적 갱신)', async () => {
const { useInbox } = await import('../../src/renderer/inbox/store.js');
useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z'));
expect(useInbox.getState().trashNotes).toHaveLength(1);
await useInbox.getState().restoreNote('a');
expect(mockApi.restoreNote).toHaveBeenCalledWith('a');
// main 은 restore 시 pushNoteUpdated 안 보냄 — store 자가 갱신 검증
expect(useInbox.getState().trashNotes).toHaveLength(0);
expect(useInbox.getState().notes).toHaveLength(1);
expect(useInbox.getState().notes[0]!.deletedAt).toBeNull();
});
it('upsertNote with showTrash=false preserves server trashCount (regression I1)', async () => {
const { useInbox } = await import('../../src/renderer/inbox/store.js');
// server 가 trashCount=5 알려줬는데 trashNotes 는 미로드 (showTrash=false 기본)
useInbox.setState({ trashCount: 5, trashNotes: [] });
useInbox.getState().upsertNote(noteStub('active-1'));
expect(useInbox.getState().trashCount).toBe(5); // server 값 보존
expect(useInbox.getState().notes).toHaveLength(1);
});
it('emptyTrash with cancelled confirm leaves trashNotes intact', async () => {
mockApi.emptyTrash.mockResolvedValueOnce({ confirmed: false, count: 0 });
const { useInbox } = await import('../../src/renderer/inbox/store.js');
useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z'));
await useInbox.getState().emptyTrash();
expect(useInbox.getState().trashNotes).toHaveLength(1);
});
});