- Add NoteCoUsage model and co-usage tracking when viewing notes - Add creationSource field to notes (form/quick/import) - Add dashboard metrics API (/api/metrics) - Add centrality calculation (/api/centrality) - Add feature flags system for toggling features - Add multiline QuickAdd with smart paste type detection - Add internal link suggestions while editing notes - Add type inference for automatic note type detection - Add comprehensive tests for type-inference and link-suggestions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
import { findLinkSuggestions, applyWikiLinks } from '@/lib/link-suggestions'
|
|
|
|
// Mock prisma
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
note: {
|
|
findMany: jest.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
describe('link-suggestions.ts', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
describe('findLinkSuggestions', () => {
|
|
it('returns empty array for short content', async () => {
|
|
const result = await findLinkSuggestions('Hi')
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('returns empty array for empty content', async () => {
|
|
const result = await findLinkSuggestions('')
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('finds matching note titles in content', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
|
{ id: '1', title: 'Docker Commands' },
|
|
{ id: '2', title: 'Git Tutorial' },
|
|
])
|
|
|
|
const content = 'I use Docker Commands for containers and Git Tutorial for version control.'
|
|
const result = await findLinkSuggestions(content)
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result.map(r => r.noteTitle)).toContain('Docker Commands')
|
|
expect(result.map(r => r.noteTitle)).toContain('Git Tutorial')
|
|
})
|
|
|
|
it('excludes current note from suggestions', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
|
{ id: '1', title: 'Current Note' },
|
|
{ id: '2', title: 'Related Note' },
|
|
])
|
|
|
|
const content = 'See Related Note for details.'
|
|
const result = await findLinkSuggestions(content, '1')
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].noteTitle).toBe('Related Note')
|
|
})
|
|
|
|
it('sorts by title length (longer first)', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
|
{ id: '1', title: 'Short' },
|
|
{ id: '2', title: 'Very Long Title' },
|
|
{ id: '3', title: 'Medium Title' },
|
|
])
|
|
|
|
const content = 'Short and Medium Title and Very Long Title'
|
|
const result = await findLinkSuggestions(content)
|
|
|
|
expect(result[0].noteTitle).toBe('Very Long Title')
|
|
expect(result[1].noteTitle).toBe('Medium Title')
|
|
expect(result[2].noteTitle).toBe('Short')
|
|
})
|
|
|
|
it('returns empty when no matches found', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
|
{ id: '1', title: 'Docker' },
|
|
{ id: '2', title: 'Git' },
|
|
])
|
|
|
|
const content = 'Python and JavaScript are programming languages.'
|
|
const result = await findLinkSuggestions(content)
|
|
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('handles case-insensitive matching', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
|
{ id: '1', title: 'Docker Commands' },
|
|
])
|
|
|
|
const content = 'I use DOCKER COMMANDS for my project.'
|
|
const result = await findLinkSuggestions(content)
|
|
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].noteTitle).toBe('Docker Commands')
|
|
})
|
|
|
|
it('matches whole words only', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
|
{ id: '1', title: 'Git' },
|
|
])
|
|
|
|
const content = 'GitHub uses Git internally.'
|
|
const result = await findLinkSuggestions(content)
|
|
|
|
// Should match standalone 'Git' but not 'Git' within 'GitHub'
|
|
// Note: the regex \bGit\b matches standalone 'Git', not 'Git' in 'GitHub'
|
|
expect(result.some(r => r.noteTitle === 'Git')).toBe(true)
|
|
})
|
|
|
|
it('returns empty when no notes exist', async () => {
|
|
;(prisma.note.findMany as jest.Mock).mockResolvedValue([])
|
|
|
|
const result = await findLinkSuggestions('Some content with potential matches')
|
|
|
|
expect(result).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('applyWikiLinks', () => {
|
|
it('replaces terms with wiki-links', () => {
|
|
const content = 'I use Docker and Git for projects.'
|
|
const replacements = [
|
|
{ term: 'Docker', noteId: '1' },
|
|
{ term: 'Git', noteId: '2' },
|
|
]
|
|
|
|
const result = applyWikiLinks(content, replacements)
|
|
|
|
expect(result).toBe('I use [[Docker]] and [[Git]] for projects.')
|
|
})
|
|
|
|
it('handles multiple occurrences', () => {
|
|
const content = 'Docker is great. Docker is fast.'
|
|
const replacements = [{ term: 'Docker', noteId: '1' }]
|
|
|
|
const result = applyWikiLinks(content, replacements)
|
|
|
|
expect(result).toBe('[[Docker]] is great. [[Docker]] is fast.')
|
|
})
|
|
|
|
it('handles case-insensitive matching and replaces with link term', () => {
|
|
const content = 'DOCKER and docker and Docker'
|
|
const replacements = [{ term: 'Docker', noteId: '1' }]
|
|
|
|
const result = applyWikiLinks(content, replacements)
|
|
|
|
// All variations matched and replaced with the link text
|
|
expect(result).toBe('[[Docker]] and [[Docker]] and [[Docker]]')
|
|
})
|
|
|
|
it('returns original content when no replacements', () => {
|
|
const content = 'Original content'
|
|
const replacements: { term: string; noteId: string }[] = []
|
|
|
|
const result = applyWikiLinks(content, replacements)
|
|
|
|
expect(result).toBe('Original content')
|
|
})
|
|
|
|
it('replaces multiple different terms', () => {
|
|
const content = 'Use React and TypeScript together.'
|
|
const replacements = [
|
|
{ term: 'React', noteId: '1' },
|
|
{ term: 'TypeScript', noteId: '2' },
|
|
]
|
|
|
|
const result = applyWikiLinks(content, replacements)
|
|
|
|
expect(result).toBe('Use [[React]] and [[TypeScript]] together.')
|
|
})
|
|
})
|
|
})
|