mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-02 01:29:32 +05:30
Three more features from the brainstorm menu, built on a shared search
algorithm so the codebase stays small.
- src/main/DailyNotes.js — Zettelkasten-style helper. One YYYY-MM-DD.md
per local date under <userData>/notes/daily/; loads skeleton from
<userData>/notes/templates/daily.md when present (built-in default
otherwise). openOrCreate never clobbers existing content.
- src/main/WorkspaceSearch.js — tag/wikilink-aware content search.
Pure module, injectable-IO tested. Query grammar: bare words,
#tag, @wikilink, "quoted phrases". Facets weight +3 each; prose
terms +1/occurrence capped at 5; edits within 7 days get a recency
nudge. Returns ranked results with snippets.
- src/main/DocQA.js — chunk-level Q&A wrapper over WorkspaceSearch.
cleanQuestion strips question words (what/how/why/...) and verb
noise (write/read/show/tell/...) so they don't drown the ranking.
Returns top-K passages instead of whole-file hits — multiple chunks
from the same file can appear in the answer.
- src/main.js — IPC: daily-notes:open-today, daily-notes:list,
workspace-search:query, doc-qa:ask. Path validation through the
existing validatePath gate; a global Ctrl+Alt+D shortcut creates
today's daily note from anywhere.
- src/preload.js — all four channels added to ALLOWED_SEND_CHANNELS.
Tests (56 new across the three modules):
- tests/main/DailyNotes.test.js (15): dateKey formatting, pathFor,
template load + {date}/{weekday} substitution, openOrCreate +
no-clobber, nested-dir creation, listExisting filtering,
isValidDir rejects NUL/non-string.
- tests/main/WorkspaceSearch.test.js (25): parseQuery grammar,
hasTag/hasWikilink word boundaries, scoreDocument scoring,
per-term spam cap, recency nudge, search ranking + limit +
empty-query short-circuit, bad-input safety.
- tests/main/DocQA.test.js (16): cleanQuestion stripping + facet
preservation, chunkDocument paragraph + hard-split, ask()
top-K, recency tiebreaker, missing-files fallback.
Full suite: 748 tests pass, 66 suites, lint+format clean.
Amit Haridas
182 lines
5.7 KiB
JavaScript
182 lines
5.7 KiB
JavaScript
/**
|
|
* @jest-environment node
|
|
*
|
|
* DailyNotes tests — pure module, injectable IO.
|
|
*/
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const DailyNotes = require('../../src/main/DailyNotes');
|
|
|
|
describe('DailyNotes.dateKey', () => {
|
|
test('formats a Date as YYYY-MM-DD in local time', () => {
|
|
expect(DailyNotes.dateKey(new Date(2026, 8, 13))).toBe('2026-09-13');
|
|
});
|
|
|
|
test('zero-pads single-digit month and day', () => {
|
|
expect(DailyNotes.dateKey(new Date(2026, 0, 5))).toBe('2026-01-05');
|
|
});
|
|
|
|
test('defaults to today when called with no args', () => {
|
|
const now = new Date();
|
|
expect(DailyNotes.dateKey()).toBe(DailyNotes.dateKey(now));
|
|
});
|
|
});
|
|
|
|
describe('DailyNotes.pathFor', () => {
|
|
test('joins dir + YYYY-MM-DD.md', () => {
|
|
const d = new Date(2026, 8, 13);
|
|
expect(DailyNotes.pathFor(d, '/tmp/notes', path)).toBe('/tmp/notes/2026-09-13.md');
|
|
});
|
|
});
|
|
|
|
describe('DailyNotes.loadTemplate', () => {
|
|
let tmpDir;
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'daily_template_'));
|
|
});
|
|
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
|
|
test('returns built-in default when no template dir given', () => {
|
|
const d = new Date(2026, 8, 13);
|
|
const body = DailyNotes.loadTemplate({ date: d, fs, pathUtil: path });
|
|
expect(body).toContain('# 2026-09-13');
|
|
expect(body).toContain('## Notes');
|
|
});
|
|
|
|
test('returns built-in default when template file is missing', () => {
|
|
const d = new Date(2026, 8, 13);
|
|
const body = DailyNotes.loadTemplate({
|
|
date: d,
|
|
templateDir: tmpDir,
|
|
templateName: 'nope.md',
|
|
fs,
|
|
pathUtil: path,
|
|
});
|
|
expect(body).toContain('# 2026-09-13');
|
|
});
|
|
|
|
test('substitutes {date} and {weekday} in a custom template', () => {
|
|
const tplPath = path.join(tmpDir, 'daily.md');
|
|
fs.writeFileSync(tplPath, '# {date} ({weekday})\n\nReflecting.', 'utf-8');
|
|
const d = new Date(2026, 8, 13); // a Sunday
|
|
const body = DailyNotes.loadTemplate({
|
|
date: d,
|
|
templateDir: tmpDir,
|
|
fs,
|
|
pathUtil: path,
|
|
now: d,
|
|
});
|
|
expect(body).toContain('# 2026-09-13');
|
|
expect(body).toContain('Sunday');
|
|
});
|
|
});
|
|
|
|
describe('DailyNotes.openOrCreate', () => {
|
|
let tmpDir;
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'daily_notes_'));
|
|
});
|
|
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
|
|
test('creates a new note from the default template', () => {
|
|
const d = new Date(2026, 8, 13);
|
|
const result = DailyNotes.openOrCreate({
|
|
date: d,
|
|
dir: tmpDir,
|
|
fs,
|
|
pathUtil: path,
|
|
now: d,
|
|
});
|
|
expect(result.created).toBe(true);
|
|
expect(result.path).toBe(path.join(tmpDir, '2026-09-13.md'));
|
|
expect(result.content).toContain('# 2026-09-13');
|
|
expect(fs.existsSync(result.path)).toBe(true);
|
|
});
|
|
|
|
test('returns existing content when the note already exists (no clobber)', () => {
|
|
const notePath = path.join(tmpDir, '2026-09-13.md');
|
|
fs.writeFileSync(notePath, '# Pre-existing content\n\nPreserve me.', 'utf-8');
|
|
|
|
const d = new Date(2026, 8, 13);
|
|
const result = DailyNotes.openOrCreate({
|
|
date: d,
|
|
dir: tmpDir,
|
|
fs,
|
|
pathUtil: path,
|
|
now: d,
|
|
});
|
|
expect(result.created).toBe(false);
|
|
expect(result.content).toBe('# Pre-existing content\n\nPreserve me.');
|
|
// The file was not modified — existing content survives.
|
|
expect(fs.readFileSync(notePath, 'utf-8')).toBe('# Pre-existing content\n\nPreserve me.');
|
|
});
|
|
|
|
test('creates the dir if it does not exist', () => {
|
|
const nestedDir = path.join(tmpDir, 'daily', 'nested');
|
|
expect(fs.existsSync(nestedDir)).toBe(false);
|
|
|
|
const d = new Date(2026, 8, 13);
|
|
DailyNotes.openOrCreate({
|
|
date: d,
|
|
dir: nestedDir,
|
|
fs,
|
|
pathUtil: path,
|
|
now: d,
|
|
});
|
|
expect(fs.existsSync(nestedDir)).toBe(true);
|
|
expect(fs.existsSync(path.join(nestedDir, '2026-09-13.md'))).toBe(true);
|
|
});
|
|
|
|
test('rejects when dir is missing', () => {
|
|
expect(() =>
|
|
DailyNotes.openOrCreate({
|
|
date: new Date(2026, 8, 13),
|
|
dir: null,
|
|
fs,
|
|
pathUtil: path,
|
|
})
|
|
).toThrow(/dir is required/);
|
|
});
|
|
});
|
|
|
|
describe('DailyNotes.listExisting', () => {
|
|
let tmpDir;
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'daily_list_'));
|
|
});
|
|
afterEach(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
|
|
test('returns [] when the dir does not exist', () => {
|
|
expect(
|
|
DailyNotes.listExisting({ dir: path.join(tmpDir, 'missing'), fs, pathUtil: path })
|
|
).toEqual([]);
|
|
});
|
|
|
|
test('lists only YYYY-MM-DD.md entries, newest first', () => {
|
|
fs.writeFileSync(path.join(tmpDir, '2026-09-10.md'), 'a');
|
|
fs.writeFileSync(path.join(tmpDir, '2026-09-13.md'), 'b');
|
|
fs.writeFileSync(path.join(tmpDir, '2026-09-12.md'), 'c');
|
|
fs.writeFileSync(path.join(tmpDir, 'readme.md'), 'ignore');
|
|
fs.writeFileSync(path.join(tmpDir, '2026-13-09.md'), 'ignore (bad month)');
|
|
|
|
const list = DailyNotes.listExisting({ dir: tmpDir, fs, pathUtil: path });
|
|
expect(list).toEqual(['2026-09-13.md', '2026-09-12.md', '2026-09-10.md']);
|
|
});
|
|
});
|
|
|
|
describe('DailyNotes.isValidDir', () => {
|
|
test('accepts non-empty strings without NUL bytes', () => {
|
|
expect(DailyNotes.isValidDir('/home/me/notes')).toBe(true);
|
|
expect(DailyNotes.isValidDir('C:\\Users\\me\\notes')).toBe(true);
|
|
});
|
|
|
|
test('rejects empty / null / non-string / NUL-containing input', () => {
|
|
expect(DailyNotes.isValidDir('')).toBe(false);
|
|
expect(DailyNotes.isValidDir(null)).toBe(false);
|
|
expect(DailyNotes.isValidDir(undefined)).toBe(false);
|
|
expect(DailyNotes.isValidDir(42)).toBe(false);
|
|
expect(DailyNotes.isValidDir('/etc/\0passwd')).toBe(false);
|
|
});
|
|
});
|