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:
th-kim0823
2026-05-15 11:03:09 +09:00
parent 96174f84c9
commit 274c171ee8
13 changed files with 78 additions and 94 deletions

View File

@@ -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: []

View File

@@ -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');
});

View File

@@ -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));
});
});

View File

@@ -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');

View File

@@ -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: '' });

View File

@@ -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');
});
});

View File

@@ -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 () => {