import { describe, expect, it } from 'vitest'; import { initialNote, readVoiceFields, renderNote } from '../src/core/note'; import { NO_SPEECH, type VoiceRecord } from '../src/core/record'; const saved: VoiceRecord = { id: '20260927-111503-0000', status: 'saved', device: 'd1', audio: 'voice/source/20260927-111503-0000.m4a', mime: 'audio/mp4', attempts: 0, }; const FM = '---\n' + 'voice-id: "20260927-111503-0000"\n' + 'voice-status: "saved"\n' + 'voice-device: "d1"\n' + 'voice-audio: "[[voice/source/20260927-111503-0000.m4a]]"\n' + 'voice-mime: "audio/mp4"\n' + 'voice-attempts: 0\n' + '---\n'; const REGION = '%%voice-note:20260927-111503-0000%%\n> [!info] Распознаётся…\n%%/voice-note%%\n'; const inserted: VoiceRecord = { ...saved, status: 'inserted' }; describe('initialNote', () => { it('builds frontmatter, embed and status region', () => { expect(initialNote(saved, 47_000)).toBe(`${FM}![[voice/source/20260927-111503-0000.m4a]]\n\n${REGION}`); }); it('adds interrupted line above the region', () => { const note = initialNote({ ...saved, interrupted: true }, 47_000); expect(note).toContain('voice-interrupted: true\n---\n'); expect(note).toContain( '![[voice/source/20260927-111503-0000.m4a]]\n\n> [!note] Запись прервана, сохранено 0:47\n\n%%voice-note:', ); }); }); describe('renderNote: status region', () => { it('retry shows local time and attempt', () => { const r: VoiceRecord = { ...saved, status: 'retry', attempts: 2, nextAttempt: '2026-09-27T11:17:03.000Z', error: 't' }; const out = renderNote(initialNote(saved, 0), r); expect(out).toContain('> [!info] Не удалось отправить, повтор в 14:17 (попытка 2/5)\n'); expect(out).not.toContain('Распознаётся'); expect(readVoiceFields(out)['voice-status']).toBe('retry'); }); it('failed shows reason and the command', () => { const out = renderNote(initialNote(saved, 0), { ...saved, status: 'failed', attempts: 1, error: '401 Unauthorized' }); expect(out).toContain('> [!warning] Не распознано: 401 Unauthorized. Команда «Повторить распознавание»\n'); }); it('no-speech is not an error', () => { const out = renderNote(initialNote(saved, 0), { ...saved, status: 'failed', attempts: 1, error: NO_SPEECH }); expect(out).toContain('> [!info] В записи не распознано речи\n'); expect(out).not.toMatch(/ошибк|Не распознано:|Повторить/); }); it('collapses multi-line error into one callout line', () => { const out = renderNote(initialNote(saved, 0), { ...saved, status: 'failed', attempts: 1, error: 'a\n\nb' }); expect(out).toContain('> [!warning] Не распознано: a b. Команда'); }); it('without markers updates frontmatter only', () => { const content = `${FM}моя заметка\n`; const out = renderNote(content, { ...saved, status: 'sent', sentAt: 'x' }); expect(out).toBe(content.replace('voice-status: "saved"', 'voice-status: "sent"').replace('voice-attempts: 0\n', 'voice-attempts: 0\nvoice-sent-at: "x"\n')); }); }); describe('renderNote: transcript', () => { it('replaces the region and keeps user text around it', () => { const content = `${initialNote(saved, 0)}купить молоко\n`; expect(renderNote(content, inserted, ' Привет, мир.\n')).toBe( `${FM.replace('"saved"', '"inserted"')}![[voice/source/20260927-111503-0000.m4a]]\n\nПривет, мир.\nкупить молоко\n`, ); }); it('keeps the interrupted line', () => { const r = { ...saved, interrupted: true }; const out = renderNote(initialNote(r, 47_000), { ...r, status: 'inserted' }, 'текст'); expect(out).toContain('> [!note] Запись прервана, сохранено 0:47\n\nтекст\n'); }); it('appends at the end when markers were removed', () => { const content = `${FM}заметка без маркеров\n\n\n`; expect(renderNote(content, inserted, 'текст')).toBe( `${FM.replace('"saved"', '"inserted"')}заметка без маркеров\n\nтекст\n`, ); }); it('is a no-op when already inserted', () => { const once = renderNote(initialNote(saved, 0), inserted, 'первый'); expect(renderNote(once, inserted, 'второй')).toBe(once); }); it('inserts special replacement patterns verbatim', () => { const text = "цена $& и $' и $1 и %%voice-note%% и ---"; expect(renderNote(initialNote(saved, 0), inserted, text)).toContain(`\n\n${text}\n`); }); it('keeps user properties in place', () => { const content = initialNote(saved, 0).replace('---\nvoice-id', '---\ntags: [idea]\nvoice-id'); const out = renderNote(content, inserted, 'x'); expect(out.startsWith('---\ntags: [idea]\nvoice-id: "20260927-111503-0000"\nvoice-status: "inserted"\n')).toBe(true); }); });