// tests/unit/history-clear-all-redteam-r1-28.test.ts // 회귀: 히스토리 '모두 지우기'가 확인 없이 한 번 클릭으로 전체 기록을 지우고 클라우드로 전파하던 결함. // HistoryPage는 이제 ClearAllHistoryFlow를 거쳐서만 deleteAll을 호출한다. import { describe, it, expect, vi } from 'vitest' import fs from 'fs' import path from 'path' import { ClearAllHistoryFlow, isClearAllDialogOpen, isHistoryViewFiltered, type ClearAllHistoryPort, type ClearAllHistoryState, } from '../../src/renderer/pages/history/clear-all-history-flow' function deferred(): { promise: Promise; resolve: (v: T) => void } { let resolve!: (v: T) => void const promise = new Promise((r) => { resolve = r }) return { promise, resolve } } function makePort(count: number | null = 12): ClearAllHistoryPort & { countAll: ReturnType deleteAll: ReturnType } { return { countAll: vi.fn(async () => count), deleteAll: vi.fn(async () => true), } } describe('ClearAllHistoryFlow — 확인 없이는 전체 삭제하지 않는다', () => { it('request()만으로는 deleteAll을 호출하지 않고 전체 건수와 함께 확인 단계로 간다', async () => { const port = makePort(12) const flow = new ClearAllHistoryFlow(port) await flow.request() expect(port.deleteAll).not.toHaveBeenCalled() expect(flow.getState()).toEqual({ phase: 'confirming', count: 12 }) expect(isClearAllDialogOpen(flow.getState())).toBe(true) }) it('confirm() 후에만 deleteAll이 정확히 한 번 실행되고 idle로 돌아온다', async () => { const port = makePort(3) const states: ClearAllHistoryState[] = [] const flow = new ClearAllHistoryFlow(port, (s) => states.push(s)) await flow.request() const ok = await flow.confirm() expect(ok).toBe(true) expect(port.deleteAll).toHaveBeenCalledTimes(1) expect(flow.getState()).toEqual({ phase: 'idle' }) expect(states.map((s) => s.phase)).toEqual(['counting', 'confirming', 'deleting', 'idle']) }) it('cancel()하면 아무것도 지우지 않는다', async () => { const port = makePort(5) const flow = new ClearAllHistoryFlow(port) await flow.request() flow.cancel() expect(await flow.confirm()).toBe(false) expect(port.deleteAll).not.toHaveBeenCalled() expect(flow.getState()).toEqual({ phase: 'idle' }) }) it('확인 단계 없이 confirm()을 불러도 삭제하지 않는다', async () => { const port = makePort(5) const flow = new ClearAllHistoryFlow(port) expect(await flow.confirm()).toBe(false) expect(port.deleteAll).not.toHaveBeenCalled() }) it('confirm을 연타해도 deleteAll은 한 번만 실행된다', async () => { const port = makePort(5) const gate = deferred() port.deleteAll.mockImplementation(() => gate.promise) const flow = new ClearAllHistoryFlow(port) await flow.request() const first = flow.confirm() const second = flow.confirm() gate.resolve(true) expect(await first).toBe(true) expect(await second).toBe(false) expect(port.deleteAll).toHaveBeenCalledTimes(1) }) it('지울 기록이 없거나 개수 조회에 실패하면 확인 단계로 가지 않는다', async () => { for (const count of [0, null]) { const port = makePort(count) const flow = new ClearAllHistoryFlow(port) await flow.request() expect(flow.getState()).toEqual({ phase: 'idle' }) expect(port.deleteAll).not.toHaveBeenCalled() } const throwing = makePort(1) throwing.countAll.mockRejectedValue(new Error('ipc down')) const flow = new ClearAllHistoryFlow(throwing) await flow.request() expect(flow.getState()).toEqual({ phase: 'idle' }) }) it('개수 조회 중 취소되면 늦게 온 결과로 다이얼로그를 다시 열지 않는다', async () => { const port = makePort() const gate = deferred() port.countAll.mockImplementation(() => gate.promise) const flow = new ClearAllHistoryFlow(port) const pending = flow.request() flow.cancel() gate.resolve(9) await pending expect(flow.getState()).toEqual({ phase: 'idle' }) }) it('deleteAll이 실패하거나 throw해도 idle로 복귀하고 false를 돌려준다', async () => { const port = makePort(2) port.deleteAll.mockRejectedValue(new Error('db locked')) const flow = new ClearAllHistoryFlow(port) await flow.request() expect(await flow.confirm()).toBe(false) expect(flow.getState()).toEqual({ phase: 'idle' }) }) }) describe('isHistoryViewFiltered', () => { it('태그 필터나 공백이 아닌 검색어가 있으면 필터된 화면이다', () => { expect(isHistoryViewFiltered(null, '')).toBe(false) expect(isHistoryViewFiltered(null, ' ')).toBe(false) expect(isHistoryViewFiltered('work', '')).toBe(true) expect(isHistoryViewFiltered(null, 'meeting')).toBe(true) }) }) describe('HistoryPage 배선', () => { const pageSource = fs.readFileSync( path.resolve(__dirname, '../../src/renderer/pages/HistoryPage.tsx'), 'utf-8', ) it('페이지가 history.deleteAll을 직접 호출하지 않는다 (확인 흐름을 우회하지 않음)', () => { expect(pageSource).not.toMatch(/electronAPI\.history\.deleteAll\s*\(/) }) it('휴지통 버튼은 삭제가 아니라 확인 요청만 한다', () => { const button = pageSource.slice(pageSource.indexOf('data-testid="clear-all-history-button"')) expect(button).toMatch(/^[^>]*onClick=\{clearAll\.request\}/) }) })