import { describe, it, expect } from 'vitest'; import type { FileSystemTreeNode } from '../filesystem/types'; import { buildWikiLinkIndex, resolveWikiLink } from './wikiLinkIndex'; const ROOT = '/vault'; const file = (path: string): FileSystemTreeNode => ({ id: `${ROOT}/${path}`, name: path.split('/').pop() ?? path, type: 'file', }); const folder = ( path: string, children: FileSystemTreeNode[], ): FileSystemTreeNode => ({ id: `${ROOT}/${path}`, name: path.split('/').pop() ?? path, type: 'folder', children, }); /** * /vault * Daily.md * logo.png * projects/ Notes.md, Deep/Notes.md * archive/ Daily.md */ const TREE: FileSystemTreeNode[] = [ file('Daily.md'), file('logo.png'), folder('projects', [ file('projects/Notes.md'), folder('projects/Deep', [file('projects/Deep/Notes.md')]), ]), folder('archive', [file('archive/Daily.md')]), ]; const index = buildWikiLinkIndex(ROOT, TREE); const resolveFrom = (target: string, from: string) => resolveWikiLink(index, target, `${ROOT}/${from}`); describe('buildWikiLinkIndex', () => { it('excludes non-text files', () => { expect(resolveFrom('logo', 'Daily.md')?.exists).toBe(false); }); it('records folders for create-on-click', () => { expect(index.dirs).toContain('projects/Deep'); }); it('completes ambiguous stems as paths and unique ones as bare names', () => { const labels = index.candidates.map((c) => c.label); expect(labels).toContain('projects/Notes'); expect(labels).toContain('projects/Deep/Notes'); expect(labels).toContain('Daily'); // ambiguous too — see below }); }); describe('resolveWikiLink — bare names', () => { it('matches a note anywhere in the vault', () => { const r = resolveFrom('Notes', 'Daily.md'); expect(r).toMatchObject({ exists: true, title: 'Notes.md' }); }); it('matches case-insensitively', () => { expect(resolveFrom('notes', 'Daily.md')?.exists).toBe(true); }); it('accepts an explicit `.md`', () => { expect(resolveFrom('Notes.md', 'Daily.md')?.exists).toBe(true); }); it('returns the tree node id verbatim, so tab dedup matches exactly', () => { expect(resolveFrom('Notes', 'Daily.md')?.entryId).toBe( '/vault/projects/Notes.md', ); }); }); describe('resolveWikiLink — ambiguity tie-breaks', () => { it('prefers a sibling of the linking note', () => { // Two `Notes.md` exist; link from within projects/Deep. expect(resolveFrom('Notes', 'projects/Deep/Other.md')?.entryId).toBe( '/vault/projects/Deep/Notes.md', ); }); it('prefers the shallowest path when there is no sibling', () => { // Two `Daily.md` exist (root and archive/); link from projects/. expect(resolveFrom('Daily', 'projects/Notes.md')?.entryId).toBe( '/vault/Daily.md', ); }); it('prefers an exact-case match over a case-folded one', () => { const mixed = buildWikiLinkIndex(ROOT, [ folder('deep', [file('deep/note.md')]), file('Note.md'), ]); expect(resolveWikiLink(mixed, 'Note', `${ROOT}/x.md`)?.entryId).toBe( '/vault/Note.md', ); }); it('falls back to lexicographic order, not readdir order', () => { const forward = buildWikiLinkIndex(ROOT, [ folder('b', [file('b/N.md')]), folder('a', [file('a/N.md')]), ]); const reversed = buildWikiLinkIndex(ROOT, [ folder('a', [file('a/N.md')]), folder('b', [file('b/N.md')]), ]); const pick = (i: typeof forward) => resolveWikiLink(i, 'N', `${ROOT}/x.md`)?.entryId; expect(pick(forward)).toBe('/vault/a/N.md'); expect(pick(forward)).toBe(pick(reversed)); }); }); describe('resolveWikiLink — path-ish targets', () => { it('resolves relative to the linking note first', () => { expect(resolveFrom('Deep/Notes', 'projects/Notes.md')?.entryId).toBe( '/vault/projects/Deep/Notes.md', ); }); it('falls back to the vault root', () => { expect(resolveFrom('projects/Notes', 'Daily.md')?.entryId).toBe( '/vault/projects/Notes.md', ); }); it('normalizes `..`', () => { expect(resolveFrom('../Daily', 'projects/Notes.md')?.entryId).toBe( '/vault/Daily.md', ); }); }); describe('resolveWikiLink — misses', () => { it('would create a bare name beside the linking note', () => { expect(resolveFrom('Brand New', 'projects/Notes.md')).toEqual({ entryId: '/vault/projects/Brand New.md', title: 'Brand New.md', exists: false, }); }); it('would create a path-ish target under the vault root', () => { expect(resolveFrom('inbox/Todo', 'projects/Notes.md')).toEqual({ entryId: '/vault/inbox/Todo.md', title: 'Todo.md', exists: false, }); }); it('keeps a dotted name intact and still adds `.md`', () => { expect(resolveFrom('Release v1.2', 'Daily.md')?.entryId).toBe( '/vault/Release v1.2.md', ); }); it('returns null for an empty target', () => { expect(resolveFrom(' ', 'Daily.md')).toBeNull(); }); });