import fs from 'fs' import path from 'path' import { describe, expect, it } from 'vitest' import type { CaptionSegment } from '@d3ro/core/types' import { applyTranscriptSegmentEdit, parseTranscriptSegments, transcriptSegmentIndex, } from '../../src/renderer/components/meeting/transcript-segments' import { isActiveMeetingState, recordingStartFromElapsed, replaceSegmentText, rollbackSegmentText, } from '../../src/renderer/pages/meeting/live-session' describe('상세 뷰 세그먼트 편집 — 전사 재조립', () => { const raw = '[00:01] [화자 1] 안녕하세요\n[00:05] 오늘 안건은\n[01:10] 예산입니다' it('세그먼트 id 는 줄 순번이다', () => { const segs = parseTranscriptSegments(raw, null) expect(segs.map((s) => s.id)).toEqual(['seg-0', 'seg-1', 'seg-2']) expect(segs[0].speaker).toBe('화자 1') expect(transcriptSegmentIndex('seg-12')).toBe(12) expect(transcriptSegmentIndex('x-1')).toBeNull() expect(transcriptSegmentIndex('seg-')).toBeNull() }) it('고친 줄만 바뀐 전사를 돌려준다(시각·화자 유지)', () => { const next = applyTranscriptSegmentEdit(raw, 'seg-1', '오늘 안건은 두 가지') expect(next).toBe('[00:01] [화자 1] 안녕하세요\n[00:05] 오늘 안건은 두 가지\n[01:10] 예산입니다') // 재조립한 전사를 다시 나누면 수정이 그대로 보인다 — 원문으로 되돌아가지 않는다 expect(parseTranscriptSegments(raw, next)[1].text).toBe('오늘 안건은 두 가지') }) it('기존 수정본 위에 이어서 고친다', () => { const first = applyTranscriptSegmentEdit(raw, 'seg-0', '반갑습니다') const second = applyTranscriptSegmentEdit(first, 'seg-2', '예산 확정') const texts = parseTranscriptSegments(raw, second).map((s) => s.text) expect(texts).toEqual(['반갑습니다', '오늘 안건은', '예산 확정']) }) it('없는 세그먼트·빈 내용·같은 내용이면 null', () => { expect(applyTranscriptSegmentEdit(raw, 'seg-9', 'x')).toBeNull() expect(applyTranscriptSegmentEdit(raw, 'bogus', 'x')).toBeNull() expect(applyTranscriptSegmentEdit(raw, 'seg-0', ' ')).toBeNull() expect(applyTranscriptSegmentEdit(raw, 'seg-0', '안녕하세요')).toBeNull() }) it('줄바꿈은 한 줄로 접어 다른 세그먼트를 만들지 않는다', () => { const next = applyTranscriptSegmentEdit(raw, 'seg-1', '둘\n셋') expect(parseTranscriptSegments(raw, next)).toHaveLength(3) }) }) describe('녹음 뷰 — 세션 복원과 실시간 편집 롤백', () => { const segs: CaptionSegment[] = [ { id: 'a', text: '하나', timestamp: 0, isFinal: true }, { id: 'b', text: '둘', timestamp: 0, isFinal: true }, ] it('녹음·후처리 중이면 녹음 뷰를 복원한다', () => { expect(isActiveMeetingState('recording')).toBe(true) expect(isActiveMeetingState('processing')).toBe(true) expect(isActiveMeetingState('idle')).toBe(false) expect(isActiveMeetingState(null)).toBe(false) }) it('경과 시간으로 시작 시각을 되짚는다', () => { expect(recordingStartFromElapsed(5_000, 100_000)).toBe(95_000) expect(recordingStartFromElapsed(-1, 100_000)).toBe(100_000) expect(recordingStartFromElapsed(Number.NaN, 100_000)).toBe(100_000) }) it('실패한 편집만 되돌리고, 그 사이 다시 고친 내용은 덮어쓰지 않는다', () => { const edited = replaceSegmentText(segs, 'b', '둘 수정') expect(edited[1].text).toBe('둘 수정') expect(rollbackSegmentText(edited, 'b', '둘 수정', '둘')[1].text).toBe('둘') const reEdited = replaceSegmentText(edited, 'b', '둘 재수정') expect(rollbackSegmentText(reEdited, 'b', '둘 수정', '둘')[1].text).toBe('둘 재수정') }) }) describe('MeetingModePage 구조 회귀', () => { const source = fs.readFileSync( path.join(__dirname, '../../src/renderer/pages/MeetingModePage.tsx'), 'utf-8', ) it('목록 뷰도 오류 다이얼로그(강제 초기화 버튼)를 렌더한다', () => { const start = source.search(/if \(view === 'list'\) \{\s*return \(/) expect(start).toBeGreaterThan(-1) const end = source.indexOf("if (view === 'recording')", start) expect(end).toBeGreaterThan(start) const listBranch = source.slice(start, end) expect(listBranch).toContain('{errorDialog}') }) it('마운트 시 main 의 회의 상태를 물어 진행 중 녹음을 복원한다', () => { expect(source).toMatch(/meetingMode\s*\.getState\(\)/) }) it('설정 딥링크에 숫자 인덱스를 쓰지 않는다', () => { expect(source).not.toMatch(/detail:\s*\{\s*tab:\s*\d/) }) })