fix(lifecycle): NoteStatus 의 archived 제거 — MoveStatusModal/classifyStatus/store 정리
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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: []
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
@@ -39,7 +39,7 @@ describe('MoveStatusModal', () => {
|
||||
);
|
||||
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(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
@@ -96,31 +96,14 @@ describe('MoveStatusModal', () => {
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
rawText="t"
|
||||
summary=""
|
||||
currentStatus="archived"
|
||||
onClose={vi.fn()}
|
||||
onMoved={onMoved}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<MoveStatusModal
|
||||
noteId="n1"
|
||||
@@ -133,7 +116,7 @@ describe('MoveStatusModal', () => {
|
||||
);
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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: '' });
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user