mirror of
https://github.com/wiseguru/ReWrite-Voice-Notes.git
synced 2026-07-22 07:49:19 +00:00
Real-time dictation (AssemblyAI/Deepgram, own provider/key/model), auto-ingest folders, record-in-background, per-invocation diarization, manage built-in templates (enable/track), local whisper.cpp auto-start/idle-stop, and dev tooling (local review + release-checklist skill). Voxtral realtime was reverse-engineered but pulled (not WebView-reachable); its adapter is kept on disk, unwired. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
6.3 KiB
TypeScript
155 lines
6.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { mergeTemplate, parseTemplateContent, renderTemplateFile } from '../src/templates-folder';
|
|
import { NoteTemplate } from '../src/types';
|
|
import { TFile } from '../test/mocks/obsidian';
|
|
|
|
function baseTemplate(overrides: Partial<NoteTemplate> = {}): NoteTemplate {
|
|
return {
|
|
id: 'general-cleanup',
|
|
name: 'General cleanup',
|
|
prompt: 'Clean up the transcript.',
|
|
insertMode: 'cursor',
|
|
newFileFolder: '',
|
|
newFileNameTemplate: 'ReWrite {{date}} {{time}}',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function fakeFile(basename: string): TFile {
|
|
const file = new TFile();
|
|
file.basename = basename;
|
|
return file;
|
|
}
|
|
|
|
describe('renderTemplateFile / parseTemplateContent round trip', () => {
|
|
it('round-trips a template with no optional fields', () => {
|
|
const template = baseTemplate();
|
|
const rendered = renderTemplateFile(template);
|
|
const parsed = parseTemplateContent(fakeFile('General cleanup'), rendered);
|
|
expect(parsed).not.toBeNull();
|
|
expect(parsed).toMatchObject({
|
|
id: template.id,
|
|
name: template.name,
|
|
prompt: template.prompt,
|
|
insertMode: template.insertMode,
|
|
newFileFolder: template.newFileFolder,
|
|
newFileNameTemplate: template.newFileNameTemplate,
|
|
disableSharedCore: false,
|
|
enableContextHint: false,
|
|
diarize: false,
|
|
titleFromContent: false,
|
|
});
|
|
});
|
|
|
|
it('round-trips the four boolean flags and noteProperties', () => {
|
|
const template = baseTemplate({
|
|
disableSharedCore: true,
|
|
enableContextHint: true,
|
|
diarize: true,
|
|
titleFromContent: true,
|
|
noteProperties: [
|
|
{ name: 'subject', instruction: 'the meeting subject' },
|
|
{ name: 'participants', instruction: 'who attended' },
|
|
],
|
|
});
|
|
const rendered = renderTemplateFile(template);
|
|
const parsed = parseTemplateContent(fakeFile('x'), rendered);
|
|
expect(parsed?.disableSharedCore).toBe(true);
|
|
expect(parsed?.enableContextHint).toBe(true);
|
|
expect(parsed?.diarize).toBe(true);
|
|
expect(parsed?.titleFromContent).toBe(true);
|
|
expect(parsed?.noteProperties).toEqual(template.noteProperties);
|
|
});
|
|
|
|
it('falls back to the file basename when frontmatter name is blank', () => {
|
|
const rendered = renderTemplateFile(baseTemplate({ name: '' }));
|
|
const parsed = parseTemplateContent(fakeFile('Fallback Name'), rendered);
|
|
expect(parsed?.name).toBe('Fallback Name');
|
|
});
|
|
|
|
it('returns null when the frontmatter has no id', () => {
|
|
const content = '---\nname: No id\n---\nBody.';
|
|
expect(parseTemplateContent(fakeFile('x'), content)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('mergeTemplate', () => {
|
|
it('keeps an on-disk prompt matching the current default unchanged, no conflict', () => {
|
|
const def = baseTemplate({ prompt: 'Current default prompt.' });
|
|
const onDisk = baseTemplate({ prompt: 'Current default prompt.' });
|
|
const { merged, conflicts, changes } = mergeTemplate(onDisk, def, []);
|
|
expect(merged.prompt).toBe('Current default prompt.');
|
|
expect(conflicts).toHaveLength(0);
|
|
expect(changes).toHaveLength(0);
|
|
});
|
|
|
|
it('silently adopts the current default when the on-disk prompt matches a prior shipped version', () => {
|
|
const priorPrompt = 'Old shipped prompt.';
|
|
const def = baseTemplate({ prompt: 'New default prompt.' });
|
|
const onDisk = baseTemplate({ prompt: priorPrompt });
|
|
const priors = [baseTemplate({ prompt: priorPrompt })];
|
|
const { merged, conflicts, changes } = mergeTemplate(onDisk, def, priors);
|
|
expect(merged.prompt).toBe('New default prompt.');
|
|
expect(conflicts).toHaveLength(0);
|
|
expect(changes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('keeps a genuinely edited prompt and reports it as a body conflict', () => {
|
|
const def = baseTemplate({ prompt: 'New default prompt.' });
|
|
const onDisk = baseTemplate({ prompt: 'My own custom prompt that matches no known version.' });
|
|
const { merged, conflicts } = mergeTemplate(onDisk, def, []);
|
|
expect(merged.prompt).toBe(onDisk.prompt);
|
|
expect(conflicts).toEqual([{ kind: 'body', defaultValue: def.prompt, userValue: onDisk.prompt }]);
|
|
});
|
|
|
|
it('never deletes a user property the default has dropped', () => {
|
|
const def = baseTemplate({ noteProperties: [] });
|
|
const onDisk = baseTemplate({ noteProperties: [{ name: 'custom', instruction: 'user added this' }] });
|
|
const { merged, conflicts } = mergeTemplate(onDisk, def, []);
|
|
expect(merged.noteProperties).toEqual([{ name: 'custom', instruction: 'user added this' }]);
|
|
expect(conflicts.some((c) => c.kind === 'removedProperty')).toBe(true);
|
|
});
|
|
|
|
it('adds a newly-introduced default property the user file lacks', () => {
|
|
const def = baseTemplate({ noteProperties: [{ name: 'subject', instruction: 'fill this in' }] });
|
|
const onDisk = baseTemplate({ noteProperties: [] });
|
|
const { merged, changes } = mergeTemplate(onDisk, def, []);
|
|
expect(merged.noteProperties).toEqual([{ name: 'subject', instruction: 'fill this in' }]);
|
|
expect(changes.some((c) => c.includes('subject'))).toBe(true);
|
|
});
|
|
|
|
// Update only ever merges tracked default-derived files, so the reconciled
|
|
// output is stamped managed: true (back-filling pre-flag files too).
|
|
it('stamps managed: true on the merged result', () => {
|
|
const def = baseTemplate();
|
|
const onDisk = baseTemplate(); // no managed key, like a pre-flag file
|
|
const { merged } = mergeTemplate(onDisk, def, []);
|
|
expect(merged.managed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('managed flag round trip', () => {
|
|
it('round-trips managed: true', () => {
|
|
const rendered = renderTemplateFile(baseTemplate({ managed: true }));
|
|
expect(parseTemplateContent(fakeFile('x'), rendered)?.managed).toBe(true);
|
|
});
|
|
|
|
it('round-trips managed: false (untracked)', () => {
|
|
const rendered = renderTemplateFile(baseTemplate({ managed: false }));
|
|
expect(parseTemplateContent(fakeFile('x'), rendered)?.managed).toBe(false);
|
|
});
|
|
|
|
// Tri-state: an absent/empty key must stay undefined, NOT collapse to false,
|
|
// because absent means "tracked when the id matches a built-in" (old files)
|
|
// while explicit false means "user untracked it; never touch".
|
|
it('parses an absent/empty managed key as undefined, not false', () => {
|
|
const rendered = renderTemplateFile(baseTemplate());
|
|
expect(rendered).toContain('managed:');
|
|
expect(parseTemplateContent(fakeFile('x'), rendered)?.managed).toBeUndefined();
|
|
});
|
|
|
|
it('tolerates the string forms written by the Properties UI', () => {
|
|
const content = '---\nid: t\nmanaged: "false"\n---\nBody.';
|
|
expect(parseTemplateContent(fakeFile('x'), content)?.managed).toBe(false);
|
|
});
|
|
});
|