Wave 2 of the 2026-09-28 design overhaul ("records are the main
character, a quiet instrument"). Decisions: design.md.
- Shell: sidebar with primary Home / Records / Meetings (Ctrl+1..3) and a
secondary "Tools" group; every item is a button with aria-current.
The status bar is removed — its "100% LOCAL PRIVACY · ZERO CLOUD SYNC"
claim contradicted device sync; the sidebar shows only queried state.
- Navigation context (routes, params, back, useRouteRequest) so Home can
open a specific meeting or record.
- Home replaces the dashboard: dictation key, start actions, only real
problems, one-line summary, recent records and meetings, file
transcription, free-tier limits.
- Settings become a full-screen route with vertical tabs (tabs no longer
clip) and a shared row grammar; long-standing bugs fixed: team and
enterprise tiers crashed the license tab (currentTier undefined),
first-run onboarding could not be left, Pro+ saw "upgrade to Pro+".
- Records: date-grouped single list with in-place expansion, search and
tags together, mode/favorite filters, load more past 50, undoable
delete. Meetings: list with search/rename/delete, summary-first detail
(summary | transcript | documents | my notes), notes persisted locally
instead of being lost. Tools pages cleaned up; stale-closure example
chips and a 1550x mouse-distance bug fixed.
- Shared pieces: PageHeader, EmptyStateCard, SearchInput ("/" works),
ConfirmDialog, notify with undo; display-labels maps domain values to
typed i18n keys so missing keys fail to compile.
- Mono font stack falls back to Pretendard for Hangul (Korean labels no
longer render spaced out).
- i18n: new ko/en keys via scripts/i18n-add-keys.mjs (one read-merge-
write so parallel editors cannot clobber locale files).
Some renderer files also carried pre-existing in-flight changes (error
recovery deep links, meeting document hooks); they are included as-is.
97 lines
4.6 KiB
TypeScript
97 lines
4.6 KiB
TypeScript
// tests/unit/renderer-tools-model.test.ts
|
|
// 도구 화면(명령어·AI 대화·지식 베이스·사전)의 순수 판단:
|
|
// - 명령어 요약·라디오 선택은 main 의 경로 규칙과 같다(지시문은 기본 액션 'custom' 일 때만)
|
|
// - 시험 실행은 실제 경로를 따른다(원문 삽입 설정에서 'refine' 을 돌리지 않는다)
|
|
// - AI 대화 상태 표지는 세션이 꺼져 있으면 없다
|
|
// - 지연 삭제 중인 항목은 목록에서 빠진다
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
COMMAND_NONE,
|
|
commandSelection,
|
|
commandTestRequest,
|
|
conversationBadge,
|
|
documentIndexLabelKey,
|
|
llmActionLabelKey,
|
|
resolveCommandPipeline,
|
|
withoutHidden,
|
|
} from '../../src/renderer/pages/tools/tools-model'
|
|
|
|
const polish = { id: 'i1', name: '정중하게', prompt: '{{text}} 를 정중하게' }
|
|
const list = [polish, { id: 'i2', name: '요약', prompt: '{{text}} 요약' }]
|
|
|
|
describe('resolveCommandPipeline', () => {
|
|
it("기본 액션 'none' 이면 원문 그대로 — 활성 지시문 id 가 남아 있어도", () => {
|
|
const p = resolveCommandPipeline({ defaultLLMAction: 'none', activeInstructionId: 'i1' }, list)
|
|
expect(p.kind).toBe('raw')
|
|
expect(commandSelection(p)).toBe(COMMAND_NONE)
|
|
})
|
|
|
|
it("'custom' + 활성 지시문이면 그 지시문", () => {
|
|
const p = resolveCommandPipeline({ defaultLLMAction: 'custom', activeInstructionId: 'i1' }, list)
|
|
expect(p).toEqual({ kind: 'instruction', instruction: polish })
|
|
expect(commandSelection(p)).toBe('i1')
|
|
})
|
|
|
|
it("'custom' 인데 지시문이 지워졌으면 '선택 안 됨'으로 정직하게", () => {
|
|
const p = resolveCommandPipeline({ defaultLLMAction: 'custom', activeInstructionId: 'gone' }, list)
|
|
expect(p.kind).toBe('instruction-missing')
|
|
expect(commandSelection(p)).toBe('')
|
|
})
|
|
|
|
it('일반 액션이 기본이면 활성 지시문이 있어도 그 액션이다(main 규칙과 같다) — 라디오는 아무것도 고르지 않는다', () => {
|
|
const p = resolveCommandPipeline({ defaultLLMAction: 'refine', activeInstructionId: 'i1' }, list)
|
|
expect(p).toEqual({ kind: 'action', action: 'refine' })
|
|
expect(commandSelection(p)).toBe('')
|
|
expect(llmActionLabelKey('refine')).toBe('settings.action.refine')
|
|
})
|
|
})
|
|
|
|
describe('commandTestRequest', () => {
|
|
it('지시문 경로는 custom + 지시문 원문으로 요청한다(치환은 main 이 한다)', () => {
|
|
const r = commandTestRequest({ kind: 'instruction', instruction: polish }, ' 안녕 ')
|
|
expect(r).toEqual({ ok: true, params: { text: '안녕', action: 'custom', customPrompt: polish.prompt } })
|
|
})
|
|
|
|
it('원문 삽입 설정에서는 실행하지 않는다 — 예전엔 refine 결과를 보여 줬다', () => {
|
|
expect(commandTestRequest({ kind: 'raw' }, '안녕')).toEqual({ ok: false, reason: 'raw' })
|
|
})
|
|
|
|
it('체인은 한 번의 LLM 호출로 흉내 내지 않는다', () => {
|
|
expect(commandTestRequest({ kind: 'action', action: 'chain' }, '안녕')).toEqual({ ok: false, reason: 'chain' })
|
|
})
|
|
|
|
it('빈 입력은 실행하지 않는다', () => {
|
|
expect(commandTestRequest({ kind: 'action', action: 'refine' }, ' ')).toEqual({ ok: false, reason: 'empty' })
|
|
})
|
|
})
|
|
|
|
describe('conversationBadge', () => {
|
|
it('세션이 꺼져 있으면 표지가 없다 — 켜지지 않은 대화를 대기라 쓰지 않는다', () => {
|
|
expect(conversationBadge({ active: false, state: 'idle', connecting: false })).toBeNull()
|
|
})
|
|
|
|
it('강조색은 듣는 중에만', () => {
|
|
expect(conversationBadge({ active: true, state: 'listening', connecting: false })?.tone).toBe('accent')
|
|
expect(conversationBadge({ active: true, state: 'thinking', connecting: false })?.tone).toBe('default')
|
|
})
|
|
|
|
it('연결 중이면 세션 상태보다 먼저 연결 중이라 쓴다', () => {
|
|
expect(conversationBadge({ active: true, state: 'idle', connecting: true })?.labelKey).toBe(
|
|
'conversation.realtime.connecting',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('withoutHidden / documentIndexLabelKey', () => {
|
|
it('지연 삭제 중인 id 를 목록에서 뺀다', () => {
|
|
expect(withoutHidden(list, new Set(['i1'])).map((i) => i.id)).toEqual(['i2'])
|
|
expect(withoutHidden(list, new Set())).toHaveLength(2)
|
|
})
|
|
|
|
it('색인이 끝나지 않은 문서는 진행 중일 때만 색인 중, 아니면 색인 안 됨', () => {
|
|
expect(documentIndexLabelKey({ id: 'd', indexed: false }, 'd')).toBe('rag.indexing')
|
|
expect(documentIndexLabelKey({ id: 'd', indexed: false }, null)).toBe('tools.knowledge.notIndexed')
|
|
expect(documentIndexLabelKey({ id: 'd', indexed: true }, null)).toBe('rag.indexed')
|
|
})
|
|
})
|