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

@@ -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

View File

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

View File

@@ -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,

View File

@@ -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 '휴지통';
}

View File

@@ -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<void>;
loadByView: (view: 'inbox' | 'completed' | 'trash') => Promise<void>;
toggleShowTrash: () => Promise<void>;
loadTrash: () => Promise<void>;
restoreNote: (id: string) => Promise<void>;
@@ -110,7 +110,7 @@ export const useInbox = create<InboxState>((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<InboxState>((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<InboxState>((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<InboxState>((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<InboxState>((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<InboxState>((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 {

View File

@@ -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<Note[]>;
countsByStatus(opts?: { notebookId?: string }): Promise<{ active: number; completed: number; trashed: number }>;
// v0.2.9 Cut B Task 8 — 4분기 status 전이 + AI 자동 분류 추천.

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