import { describe, it, expect } from 'vitest' import { D3ROError, ErrorCode } from '@d3ro/core/errors' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getHistoryService } from '../../src/main/services/HistoryService' import { getMemoService } from '../../src/main/services/MemoService' import { registerMemoHandlers } from '../../src/main/ipc/memo-handlers' import { historyInput, invokeIpc, useRedHarness } from './harness' useRedHarness() describe('유스케이스: 메모 태그 추가/삭제/검색/내보내기', () => { function seedHistory(): string { return getHistoryService().create(historyInput()).id } it('새 히스토리에는 태그가 없다', () => { const id = seedHistory() expect(getMemoService().getTagsForEntry(id)).toEqual([]) }) it('태그를 달면 소문자로 정규화된다', () => { const id = seedHistory() const tag = getMemoService().addTag(id, ' IDEA ') expect(tag.tag).toBe('idea') expect(getMemoService().getTagsForEntry(id)).toHaveLength(1) }) it('같은 태그 중복은 MemoTagDuplicate 다', () => { const id = seedHistory() getMemoService().addTag(id, 'work') expect(() => getMemoService().addTag(id, 'WORK')).toThrow(D3ROError) try { getMemoService().addTag(id, 'work') } catch (err) { expect((err as D3ROError).code).toBe(ErrorCode.MemoTagDuplicate) } }) it('빈 태그는 거부된다', () => { const id = seedHistory() expect(() => getMemoService().addTag(id, ' ')).toThrow() }) it('태그 제거 후 목록이 비다', () => { const id = seedHistory() getMemoService().addTag(id, 'tmp') getMemoService().removeTag(id, 'tmp') expect(getMemoService().getTagsForEntry(id)).toEqual([]) }) it('없는 태그 제거는 MemoTagNotFound 다', () => { const id = seedHistory() expect(() => getMemoService().removeTag(id, 'ghost')).toThrow(D3ROError) try { getMemoService().removeTag(id, 'ghost') } catch (err) { expect((err as D3ROError).code).toBe(ErrorCode.MemoTagNotFound) } }) it('전체 태그 카운트가 사용 횟수를 반영한다', () => { const a = seedHistory() const b = seedHistory() getMemoService().addTag(a, 'shared') getMemoService().addTag(b, 'shared') getMemoService().addTag(a, 'solo') const all = getMemoService().getAllTags() const shared = all.find((t) => t.tag === 'shared') expect(shared?.count).toBe(2) }) it('태그로 히스토리를 검색한다', () => { const a = seedHistory() seedHistory() getMemoService().addTag(a, 'findme') const page = getMemoService().searchByTag({ tag: 'findme', page: 0, pageSize: 10 }) expect(page.total).toBe(1) expect(page.entries[0].id).toBe(a) }) it('없는 태그 검색은 0건이다', () => { seedHistory() expect(getMemoService().searchByTag({ tag: 'none', page: 0, pageSize: 10 }).total).toBe(0) }) it('마크다운 내보내기는 파일을 만들고 내용을 담는다', () => { const id = seedHistory() getMemoService().addTag(id, 'export') const filePath = getMemoService().exportMarkdown({ tag: 'export' }) expect(filePath.endsWith('.md')).toBe(true) const fs = require('fs') as typeof import('fs') const body = fs.readFileSync(filePath, 'utf-8') expect(body).toContain('#export') expect(body.length).toBeGreaterThan(10) }) it('매칭 없는 내보내기도 파일을 만들되 비었음을 표시한다', () => { const filePath = getMemoService().exportMarkdown({ tag: 'absent' }) const fs = require('fs') as typeof import('fs') const body = fs.readFileSync(filePath, 'utf-8') expect(body).toMatch(/No memo entries found/i) }) it('IPC memo:addTag / getTags 왕복', async () => { registerMemoHandlers() const id = seedHistory() const add = await invokeIpc(IPC_CHANNELS.MEMO.ADD_TAG, { historyId: id, tag: 'ipc' }) expect(add.success).toBe(true) const tags = await invokeIpc(IPC_CHANNELS.MEMO.GET_TAGS, { historyId: id }) expect(tags.success).toBe(true) if (tags.success) expect(tags.data[0].tag).toBe('ipc') }) it('IPC 중복 태그는 MemoTagDuplicate 다', async () => { registerMemoHandlers() const id = seedHistory() await invokeIpc(IPC_CHANNELS.MEMO.ADD_TAG, { historyId: id, tag: 'dup' }) const res = await invokeIpc(IPC_CHANNELS.MEMO.ADD_TAG, { historyId: id, tag: 'dup' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.MemoTagDuplicate) }) it('IPC 없는 태그 제거는 MemoTagNotFound 다', async () => { registerMemoHandlers() const id = seedHistory() const res = await invokeIpc(IPC_CHANNELS.MEMO.REMOVE_TAG, { historyId: id, tag: 'nope' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.MemoTagNotFound) }) it('IPC getAllTags / searchByTag', async () => { registerMemoHandlers() const id = seedHistory() getMemoService().addTag(id, 'listed') const all = await invokeIpc(IPC_CHANNELS.MEMO.GET_ALL_TAGS) expect(all.success).toBe(true) const search = await invokeIpc(IPC_CHANNELS.MEMO.SEARCH_BY_TAG, { tag: 'listed', page: 0, pageSize: 10, }) expect(search.success).toBe(true) if (search.success) expect(search.data.total).toBe(1) }) })