import { describe, it, expect } from 'vitest' import type { VaultEntry } from '../types' import { slugify, buildNewEntry, generateUntitledName, entryMatchesTarget, buildNoteContent, resolveNewNote, resolveNewType, todayDateString, buildDailyNoteContent, resolveDailyNote, findDailyNote, DEFAULT_TEMPLATES, resolveTemplate, } from './useNoteCreation' const makeEntry = (overrides: Partial = {}): VaultEntry => ({ path: '/Users/luca/Laputa/test.md', filename: 'test.md', title: 'Test Note', isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: 'Active', archived: false, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, outgoingLinks: [], template: null, sort: null, sidebarLabel: null, view: null, visible: null, properties: {}, organized: false, favorite: false, favoriteIndex: null, listPropertiesDisplay: [], hasH1: false, ...overrides, }) describe('slugify', () => { it('converts text to lowercase kebab-case', () => { expect(slugify('Hello World')).toBe('hello-world') }) it('removes special characters', () => { expect(slugify('My Project! @#$%')).toBe('my-project') }) it('strips leading and trailing hyphens', () => { expect(slugify('--hello--')).toBe('hello') }) it('handles empty string with fallback', () => { expect(slugify('')).toBe('untitled') }) it('collapses multiple separators into one hyphen', () => { expect(slugify('hello world---foo')).toBe('hello-world-foo') }) it('returns fallback for strings with only special characters', () => { // slugify('+++') should not return empty string — it causes invalid paths expect(slugify('+++')).not.toBe('') expect(slugify('!!!')).not.toBe('') expect(slugify('---')).not.toBe('') expect(slugify('@#$')).not.toBe('') }) }) describe('buildNewEntry', () => { it('creates a VaultEntry with correct fields', () => { const entry = buildNewEntry({ path: '/vault/my-note.md', slug: 'my-note', title: 'My Note', type: 'Note', status: 'Active', }) expect(entry.path).toBe('/vault/my-note.md') expect(entry.filename).toBe('my-note.md') expect(entry.title).toBe('My Note') expect(entry.isA).toBe('Note') expect(entry.status).toBe('Active') expect(entry.archived).toBe(false) expect(entry.modifiedAt).toBeGreaterThan(0) expect(entry.createdAt).toBe(entry.modifiedAt) }) it('sets null status when provided', () => { const entry = buildNewEntry({ path: '/vault/topic/ai.md', slug: 'ai', title: 'AI', type: 'Topic', status: null, }) expect(entry.status).toBeNull() }) }) describe('generateUntitledName', () => { it('returns base name when no conflicts', () => { expect(generateUntitledName([], 'Note')).toBe('Untitled note') }) it('appends counter when base name exists', () => { const entries = [makeEntry({ title: 'Untitled note' })] expect(generateUntitledName(entries, 'Note')).toBe('Untitled note 2') }) it('increments counter past existing numbered entries', () => { const entries = [ makeEntry({ title: 'Untitled note' }), makeEntry({ title: 'Untitled note 2' }), makeEntry({ title: 'Untitled note 3' }), ] expect(generateUntitledName(entries, 'Note')).toBe('Untitled note 4') }) it('uses type name in lowercase', () => { expect(generateUntitledName([], 'Project')).toBe('Untitled project') }) it('avoids names in the pending set', () => { const pending = new Set(['Untitled note']) expect(generateUntitledName([], 'Note', pending)).toBe('Untitled note 2') }) it('avoids both existing and pending names', () => { const entries = [makeEntry({ title: 'Untitled note' })] const pending = new Set(['Untitled note 2']) expect(generateUntitledName(entries, 'Note', pending)).toBe('Untitled note 3') }) }) describe('entryMatchesTarget', () => { it('matches by exact title (case-insensitive)', () => { const entry = makeEntry({ title: 'My Project' }) expect(entryMatchesTarget(entry, 'my project')).toBe(true) }) it('matches by alias', () => { const entry = makeEntry({ aliases: ['MP', 'TheProject'] }) expect(entryMatchesTarget(entry, 'mp')).toBe(true) }) it('matches legacy path-style target via filename stem', () => { const entry = makeEntry({ filename: 'my-project.md' }) expect(entryMatchesTarget(entry, 'project/my-project')).toBe(true) }) it('matches by filename stem', () => { const entry = makeEntry({ filename: 'my-project.md' }) expect(entryMatchesTarget(entry, 'my-project')).toBe(true) }) it('matches when target as words matches title', () => { const entry = makeEntry({ title: 'my project' }) expect(entryMatchesTarget(entry, 'my-project')).toBe(true) }) it('returns false when nothing matches', () => { const entry = makeEntry({ title: 'Something Else', aliases: [], filename: 'else.md' }) expect(entryMatchesTarget(entry, 'nonexistent')).toBe(false) }) it('handles pipe syntax targets', () => { const entry = makeEntry({ path: '/vault/project/alpha.md', filename: 'alpha.md', title: 'Alpha' }) expect(entryMatchesTarget(entry, 'project/alpha|Alpha Project')).toBe(true) }) }) describe('buildNoteContent', () => { it('generates frontmatter with title and status for regular types', () => { const content = buildNoteContent('My Note', 'Note', 'Active') expect(content).toBe('---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n') }) it('omits title when null', () => { const content = buildNoteContent(null, 'Note', 'Active') expect(content).toBe('---\ntype: Note\nstatus: Active\n---\n') }) it('omits status when null', () => { const content = buildNoteContent('AI', 'Topic', null) expect(content).toBe('---\ntitle: AI\ntype: Topic\n---\n') }) it('includes template body when provided', () => { const content = buildNoteContent('My Project', 'Project', 'Active', '## Objective\n\n## Notes\n\n') expect(content).not.toContain('# My Project') expect(content).toContain('## Objective') expect(content).toContain('## Notes') }) it('ignores null template', () => { const content = buildNoteContent('My Note', 'Note', 'Active', null) expect(content).toBe('---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n') }) }) describe('resolveTemplate', () => { it('returns template from type entry when set', () => { const typeEntry = makeEntry({ isA: 'Type', title: 'Recipe', template: '## Ingredients\n\n## Steps\n\n' }) expect(resolveTemplate([typeEntry], 'Recipe')).toBe('## Ingredients\n\n## Steps\n\n') }) it('falls back to DEFAULT_TEMPLATES for built-in types', () => { expect(resolveTemplate([], 'Project')).toBe(DEFAULT_TEMPLATES.Project) }) it('returns null when no template and no default', () => { expect(resolveTemplate([], 'CustomType')).toBeNull() }) it('type entry template overrides default', () => { const typeEntry = makeEntry({ isA: 'Type', title: 'Project', template: '## Custom\n\n' }) expect(resolveTemplate([typeEntry], 'Project')).toBe('## Custom\n\n') }) }) describe('DEFAULT_TEMPLATES', () => { it('has templates for Project, Person, Responsibility, Experiment', () => { expect(DEFAULT_TEMPLATES.Project).toBeDefined() expect(DEFAULT_TEMPLATES.Person).toBeDefined() expect(DEFAULT_TEMPLATES.Responsibility).toBeDefined() expect(DEFAULT_TEMPLATES.Experiment).toBeDefined() }) }) describe('resolveNewNote', () => { it('creates note at vault root', () => { const { entry, content } = resolveNewNote('My Project', 'Project', '/my/vault') expect(entry.path).toBe('/my/vault/my-project.md') expect(entry.isA).toBe('Project') expect(entry.status).toBe('Active') expect(content).toContain('type: Project') expect(content).toContain('status: Active') }) it('creates custom type note at vault root', () => { const { entry } = resolveNewNote('First Recipe', 'Recipe', '/my/vault') expect(entry.path).toBe('/my/vault/first-recipe.md') }) it('omits status for Topic type', () => { const { entry, content } = resolveNewNote('Machine Learning', 'Topic', '/my/vault') expect(entry.status).toBeNull() expect(content).not.toContain('status:') }) it('omits status for Person type', () => { const { entry } = resolveNewNote('John Doe', 'Person', '/my/vault') expect(entry.status).toBeNull() }) it('uses provided vault path', () => { const { entry } = resolveNewNote('Test', 'Note', '/other/vault') expect(entry.path).toBe('/other/vault/test.md') }) it('produces a valid path for custom types with special characters', () => { const { entry } = resolveNewNote('My Note', 'Q&A', '/vault') expect(entry.path).not.toContain('//') expect(entry.path).toMatch(/\.md$/) expect(entry.filename).not.toBe('.md') }) it('produces a valid path when type is all special characters', () => { const { entry } = resolveNewNote('My Note', '+++', '/vault') expect(entry.path).not.toContain('//') expect(entry.path).toMatch(/\.md$/) }) }) describe('resolveNewType', () => { it('creates a type entry at vault root', () => { const { entry, content } = resolveNewType('Recipe', '/my/vault') expect(entry.path).toBe('/my/vault/recipe.md') expect(entry.isA).toBe('Type') expect(entry.status).toBeNull() expect(content).toContain('type: Type') expect(content).not.toContain('# Recipe') }) it('uses provided vault path instead of hardcoded path', () => { const { entry } = resolveNewType('Responsibility', '/other/vault') expect(entry.path).toBe('/other/vault/responsibility.md') expect(entry.path).not.toContain('/Users/luca/Laputa') }) }) describe('todayDateString', () => { it('returns date in YYYY-MM-DD format', () => { const result = todayDateString() expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/) }) }) describe('buildDailyNoteContent', () => { it('generates frontmatter with Journal type and date', () => { const content = buildDailyNoteContent('2026-03-02') expect(content).toContain('type: Journal') expect(content).toContain('date: 2026-03-02') expect(content).toContain('title: 2026-03-02') }) it('includes Intentions and Reflections sections', () => { const content = buildDailyNoteContent('2026-03-02') expect(content).toContain('## Intentions') expect(content).toContain('## Reflections') }) it('does not include H1 heading with the date', () => { const content = buildDailyNoteContent('2026-03-02') expect(content).not.toMatch(/^# /m) }) }) describe('resolveDailyNote', () => { it('creates entry at vault root with date as filename', () => { const { entry } = resolveDailyNote('2026-03-02', '/my/vault') expect(entry.path).toBe('/my/vault/2026-03-02.md') expect(entry.filename).toBe('2026-03-02.md') expect(entry.title).toBe('2026-03-02') expect(entry.isA).toBe('Journal') expect(entry.status).toBeNull() }) it('returns content with daily note template', () => { const { content } = resolveDailyNote('2026-03-02', '/my/vault') expect(content).toContain('type: Journal') expect(content).toContain('## Intentions') }) it('uses provided vault path', () => { const { entry } = resolveDailyNote('2026-03-02', '/other/vault') expect(entry.path).toBe('/other/vault/2026-03-02.md') }) }) describe('findDailyNote', () => { it('finds entry by filename and Journal type', () => { const entries = [ makeEntry({ path: '/Users/luca/Laputa/2026-03-02.md', filename: '2026-03-02.md', isA: 'Journal' }), makeEntry({ path: '/Users/luca/Laputa/other.md' }), ] const found = findDailyNote(entries, '2026-03-02') expect(found).toBeDefined() expect(found!.path).toBe('/Users/luca/Laputa/2026-03-02.md') }) it('returns undefined when no matching entry exists', () => { const entries = [makeEntry({ path: '/Users/luca/Laputa/other.md' })] expect(findDailyNote(entries, '2026-03-02')).toBeUndefined() }) it('does not match non-Journal notes with date filename', () => { const entries = [ makeEntry({ path: '/vault/2026-03-02.md', filename: '2026-03-02.md', isA: 'Note' }), ] expect(findDailyNote(entries, '2026-03-02')).toBeUndefined() }) })