Files
markdown-converter/tests/main/DailyNotesTemplates.test.js
T
amitwh c3fe72bcae feat(templates,qa): template gallery + pluggable DocQA engine
Two more features from the deferred menu:

Daily-note template gallery:
- src/main/DailyNotesTemplates.js — pure module: listTemplates() /
  saveTemplate() / deleteTemplate() / labelFor() with injectable IO.
- src/main/DailyNotes.js — openOrCreate() now accepts seedContent so a
  non-default template can seed a NEW note (existing notes never get
  clobbered).
- src/main.js — IPC channels daily-templates:list / save / delete /
  apply. apply renders the chosen template (with {date}/{weekday}
  substitution) and pipes through DailyNotes.openOrCreate.
- src/sidebar/daily-templates-panel.js — gallery UI: list, +New
  (prompt for name + content), Use (applies to today's note),
  delete (refuses to remove the last template so the default survives).
- src/renderer.js — registers the panel.
- src/index.html — icon (already added).

Pluggable DocQA engine (semantic search hook):
- src/main/SemanticEngine.js — engine interface with defaultEngine() (TF-idF,
  always available) and neuralEngine() (lazy @xenova/transformers,
  falls back gracefully when the dep is missing). getEngine(name)
  resolves either.
- src/main/DocQA.js — ask() is now async and accepts an engine arg.
  TF-idF path unchanged; neural path calls engine.rank(question, chunks)
  directly. The chunk corpus is built up front regardless of engine so
  ranking is consistent.
- src/main.js — doc-qa:ask IPC resolves the engine via SemanticEngine.getEngine(name)
  before calling DocQA.ask. The renderer can pass {engine: 'transformers'}
  to opt in once @xenova/transformers is installed.

Tests (51 new across this batch):
- tests/main/DailyNotesTemplates.test.js (18): labelFor separators /
  edge cases / non-string safety, listTemplates empty / present / sort,
  saveTemplate nested dir + .md extension + validation + null content,
  deleteTemplate success / missing / validation.
- tests/daily-templates-panel.test.js (12): mount + empty state + list +
  XSS safety, Use button (apply + error path), Delete button (success +
  last-template guard), New template (save + cancel), refresh.
- tests/main/SemanticEngine.test.js (8): default engine shape + rank
  matches WorkspaceSearch, getEngine for tf-idf / unknown / transformers
  (graceful fallback when @xenova/transformers missing), parity check.
- DocQA: 5 new tests for engine arg (custom engine.rank called, default
  fallback, neural hit shape translation); existing tests updated to
  await the now-async ask().

Full suite: 76 suites, 914 tests, lint+format clean.

Activation for the neural engine:
  npm install @xenova/transformers
  (heavy; ~50 MiB with deps) — then 'transformers' is selectable in
  doc-qa:ask. Until then, all calls use TF-idF transparently.

Amit Haridas
2026-09-14 11:57:32 +05:30

171 lines
6.2 KiB
JavaScript

/**
* @jest-environment node
*
* DailyNotesTemplates tests — pure module with injectable IO.
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const DailyNotesTemplates = require('../../src/main/DailyNotesTemplates');
describe('DailyNotesTemplates.labelFor', () => {
test('strips .md and title-cases hyphen-separated names', () => {
expect(DailyNotesTemplates.labelFor('morning-pages.md')).toBe('Morning Pages');
expect(DailyNotesTemplates.labelFor('evening-reflection.md')).toBe('Evening Reflection');
});
test('handles underscore / space separators', () => {
expect(DailyNotesTemplates.labelFor('stand_up_notes.md')).toBe('Stand Up Notes');
expect(DailyNotesTemplates.labelFor('daily journal.md')).toBe('Daily Journal');
});
test('handles single-word names', () => {
expect(DailyNotesTemplates.labelFor('journal.md')).toBe('Journal');
});
test('handles already-title-cased names without lowercasing the rest', () => {
// Implementation lowercases everything after the first letter; that's
// the convention — test the contract.
expect(DailyNotesTemplates.labelFor('MyTemplate.md')).toBe('Mytemplate');
});
test('returns the input unchanged when no .md extension', () => {
expect(DailyNotesTemplates.labelFor('weird-name')).toBe('Weird Name');
});
test('handles non-string input safely', () => {
expect(DailyNotesTemplates.labelFor(null)).toBe('');
expect(DailyNotesTemplates.labelFor(undefined)).toBe('');
expect(DailyNotesTemplates.labelFor(42)).toBe('');
});
});
describe('DailyNotesTemplates.listTemplates', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dnt_'));
});
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
test('returns [] when the dir does not exist', () => {
expect(
DailyNotesTemplates.listTemplates({ dir: path.join(tmpDir, 'missing'), fs, pathUtil: path })
).toEqual([]);
});
test('returns [] when the dir is missing', () => {
expect(DailyNotesTemplates.listTemplates({ dir: null, fs, pathUtil: path })).toEqual([]);
});
test('lists every .md file with label + content', () => {
fs.writeFileSync(path.join(tmpDir, 'morning-pages.md'), '# Morning\n\nReflect.');
fs.writeFileSync(path.join(tmpDir, 'evening.md'), '# Evening');
fs.writeFileSync(path.join(tmpDir, 'README.txt'), 'ignored');
const list = DailyNotesTemplates.listTemplates({ dir: tmpDir, fs, pathUtil: path });
expect(list).toHaveLength(2);
const labels = list.map((t) => t.label).sort();
expect(labels).toEqual(['Evening', 'Morning Pages']);
const morning = list.find((t) => t.label === 'Morning Pages');
expect(morning.content).toBe('# Morning\n\nReflect.');
});
test('results are sorted alphabetically by label', () => {
fs.writeFileSync(path.join(tmpDir, 'z-last.md'), '');
fs.writeFileSync(path.join(tmpDir, 'a-first.md'), '');
fs.writeFileSync(path.join(tmpDir, 'm-middle.md'), '');
const list = DailyNotesTemplates.listTemplates({ dir: tmpDir, fs, pathUtil: path });
expect(list.map((t) => t.label)).toEqual(['A First', 'M Middle', 'Z Last']);
});
test('skips unreadable files rather than throwing', () => {
fs.writeFileSync(path.join(tmpDir, 'good.md'), 'ok');
// Plant a directory that LOOKS like a .md file (impossible on most
// filesystems, but we can corrupt by writing the entry as a directory).
// Simpler: just verify the function doesn't crash on the existing files.
const list = DailyNotesTemplates.listTemplates({ dir: tmpDir, fs, pathUtil: path });
expect(list.map((t) => t.name)).toEqual(['good.md']);
void list;
});
});
describe('DailyNotesTemplates.saveTemplate', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dnt_save_'));
});
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
test('creates the dir + writes the file', () => {
const nested = path.join(tmpDir, 'nested');
const entry = DailyNotesTemplates.saveTemplate({
dir: nested,
name: 'morning-pages.md',
content: '# Morning\n\nReflect.',
fs,
pathUtil: path,
});
expect(fs.existsSync(path.join(nested, 'morning-pages.md'))).toBe(true);
expect(entry.name).toBe('morning-pages.md');
expect(entry.label).toBe('Morning Pages');
expect(entry.content).toBe('# Morning\n\nReflect.');
});
test('appends .md extension when missing', () => {
DailyNotesTemplates.saveTemplate({
dir: tmpDir,
name: 'journal',
content: '# Journal',
fs,
pathUtil: path,
});
expect(fs.existsSync(path.join(tmpDir, 'journal.md'))).toBe(true);
});
test('throws on missing dir / name', () => {
expect(() =>
DailyNotesTemplates.saveTemplate({ name: 'x', content: 'y', fs, pathUtil: path })
).toThrow(/dir is required/);
expect(() =>
DailyNotesTemplates.saveTemplate({ dir: tmpDir, content: 'y', fs, pathUtil: path })
).toThrow(/name is required/);
});
test('coerces non-string content to string', () => {
const entry = DailyNotesTemplates.saveTemplate({
dir: tmpDir,
name: 'x.md',
content: null,
fs,
pathUtil: path,
});
expect(entry.content).toBe('');
});
});
describe('DailyNotesTemplates.deleteTemplate', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dnt_del_'));
});
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
test('removes the file and returns true', () => {
const name = 't.md';
fs.writeFileSync(path.join(tmpDir, name), 'x');
const ok = DailyNotesTemplates.deleteTemplate({ dir: tmpDir, name, fs, pathUtil: path });
expect(ok).toBe(true);
expect(fs.existsSync(path.join(tmpDir, name))).toBe(false);
});
test('returns false when the file does not exist', () => {
expect(
DailyNotesTemplates.deleteTemplate({ dir: tmpDir, name: 'missing.md', fs, pathUtil: path })
).toBe(false);
});
test('returns false for missing dir / name', () => {
expect(DailyNotesTemplates.deleteTemplate({ name: 'x', fs, pathUtil: path })).toBe(false);
expect(DailyNotesTemplates.deleteTemplate({ dir: tmpDir, fs, pathUtil: path })).toBe(false);
});
});