obsidian-voice-note/tests/note-body.test.ts
Andrey Epifancev d579ffd4a1 feat(core): note body with status region and transcript insertion
Refs #1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:24:30 +04:00

109 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
});