feat: MVP-3 Sprint 4 - Co-usage, metrics, centrality, creation source, feature flags
- 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>
This commit is contained in:
171
__tests__/link-suggestions.test.ts
Normal file
171
__tests__/link-suggestions.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
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.')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user