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>
This commit is contained in:
@@ -66,6 +66,9 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount });
|
||||
},
|
||||
upsertNote(note) {
|
||||
// trashCount 는 server-authoritative. trashNotes 가 cache-loaded (showTrash=true) 일
|
||||
// 때만 trashCount 를 local recompute. 그 외엔 server 값 (refreshMeta) 보존.
|
||||
const showTrash = get().showTrash;
|
||||
if (note.deletedAt !== null) {
|
||||
// trash 노트: notes 에서 제거 + trashNotes 에 upsert
|
||||
const cleanNotes = get().notes.filter((n) => n.id !== note.id);
|
||||
@@ -73,7 +76,11 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
const nextTrash = get().trashNotes.slice();
|
||||
if (ti >= 0) nextTrash[ti] = note;
|
||||
else nextTrash.unshift(note);
|
||||
set({ notes: cleanNotes, trashNotes: nextTrash, trashCount: nextTrash.length });
|
||||
set({
|
||||
notes: cleanNotes,
|
||||
trashNotes: nextTrash,
|
||||
...(showTrash ? { trashCount: nextTrash.length } : {})
|
||||
});
|
||||
} else {
|
||||
// active 노트: trashNotes 에서 제거 + notes 에 upsert (restore 케이스 포함)
|
||||
const cleanTrash = get().trashNotes.filter((n) => n.id !== note.id);
|
||||
@@ -81,13 +88,22 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
const nextNotes = get().notes.slice();
|
||||
if (i >= 0) nextNotes[i] = note;
|
||||
else nextNotes.unshift(note);
|
||||
set({ notes: nextNotes, trashNotes: cleanTrash, trashCount: cleanTrash.length });
|
||||
set({
|
||||
notes: nextNotes,
|
||||
trashNotes: cleanTrash,
|
||||
...(showTrash ? { trashCount: cleanTrash.length } : {})
|
||||
});
|
||||
}
|
||||
},
|
||||
removeNote(id) {
|
||||
const cleanNotes = get().notes.filter((n) => n.id !== id);
|
||||
const cleanTrash = get().trashNotes.filter((n) => n.id !== id);
|
||||
set({ notes: cleanNotes, trashNotes: cleanTrash, trashCount: cleanTrash.length });
|
||||
const showTrash = get().showTrash;
|
||||
set({
|
||||
notes: cleanNotes,
|
||||
trashNotes: cleanTrash,
|
||||
...(showTrash ? { trashCount: cleanTrash.length } : {})
|
||||
});
|
||||
},
|
||||
setTagFilter(tag) {
|
||||
set({ tagFilter: tag });
|
||||
@@ -103,7 +119,9 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
},
|
||||
async restoreNote(id) {
|
||||
await inboxApi.restoreNote(id);
|
||||
// onNoteUpdated 이벤트 미수신 케이스 대비 (renderer 자가 갱신)
|
||||
// 낙관적 갱신: main 은 trash/restore 시 pushNoteUpdated 를 보내지 않음
|
||||
// (현재 AiWorker.onUpdate 만 push). 자가 반영이 primary 메커니즘.
|
||||
// 전제: 호출 시점에 trashNotes 에 노트가 존재 (T14 trash view 한정 호출).
|
||||
const note = get().trashNotes.find((n) => n.id === id);
|
||||
if (note) {
|
||||
get().upsertNote({ ...note, deletedAt: null });
|
||||
|
||||
@@ -73,11 +73,25 @@ describe('useInbox — trash state (v0.2.3 #4)', () => {
|
||||
expect(useInbox.getState().trashNotes).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('restoreNote calls api', async () => {
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user