feat: MVP-3 Sprint 1 - Usage tracking, smart dashboard, scoring boost
## Registro de Uso - Nuevo modelo NoteUsage en Prisma - Tipos de eventos: view, search_click, related_click, link_click, copy_command, copy_snippet - Funciones: trackNoteUsage, getUsageStats, getRecentlyUsedNotes - localStorage: recentlyViewed (últimas 10 notas) - Rastreo de copias en markdown-content.tsx ## Dashboard Rediseñado - 5 bloques: Recientes, Más usadas, Comandos recientes, Snippets recientes, Según actividad - Nuevo src/lib/dashboard.ts con getDashboardData() - Recomendaciones basadas en recentlyViewed ## Scoring con Uso Real - search.ts: +1 per 5 views (max +3), +2 recency boost - related.ts: mismo sistema de usage boost - No eclipsa match textual fuerte ## Tests - 110 tests pasando (usage, dashboard, related, search) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
223
__tests__/usage.test.ts
Normal file
223
__tests__/usage.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { trackNoteUsage, getNoteUsageCount, getRecentlyUsedNotes, getUsageStats, UsageEventType } from '@/lib/usage'
|
||||
|
||||
// Mock prisma before importing usage module
|
||||
jest.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
noteUsage: {
|
||||
create: jest.fn(),
|
||||
count: jest.fn(),
|
||||
groupBy: jest.fn(),
|
||||
},
|
||||
note: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
describe('usage.ts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('trackNoteUsage', () => {
|
||||
it('should create a usage event with all fields', async () => {
|
||||
;(prisma.noteUsage.create as jest.Mock).mockResolvedValue({ id: '1' })
|
||||
|
||||
await trackNoteUsage({
|
||||
noteId: 'note-1',
|
||||
eventType: 'view',
|
||||
query: 'docker commands',
|
||||
metadata: { source: 'search' },
|
||||
})
|
||||
|
||||
expect(prisma.noteUsage.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
noteId: 'note-1',
|
||||
eventType: 'view',
|
||||
query: 'docker commands',
|
||||
metadata: JSON.stringify({ source: 'search' }),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a usage event without optional fields', async () => {
|
||||
;(prisma.noteUsage.create as jest.Mock).mockResolvedValue({ id: '1' })
|
||||
|
||||
await trackNoteUsage({
|
||||
noteId: 'note-1',
|
||||
eventType: 'view',
|
||||
})
|
||||
|
||||
expect(prisma.noteUsage.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
noteId: 'note-1',
|
||||
eventType: 'view',
|
||||
query: null,
|
||||
metadata: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should silently fail on database error', async () => {
|
||||
;(prisma.noteUsage.create as jest.Mock).mockRejectedValue(new Error('DB error'))
|
||||
|
||||
// Should not throw
|
||||
await expect(
|
||||
trackNoteUsage({ noteId: 'note-1', eventType: 'view' })
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNoteUsageCount', () => {
|
||||
it('should return count for a specific note', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(5)
|
||||
|
||||
const result = await getNoteUsageCount('note-1')
|
||||
|
||||
expect(result).toBe(5)
|
||||
expect(prisma.noteUsage.count).toHaveBeenCalledWith({
|
||||
where: expect.objectContaining({ noteId: 'note-1' }),
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter by event type when specified', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(3)
|
||||
|
||||
const result = await getNoteUsageCount('note-1', 'search_click')
|
||||
|
||||
expect(result).toBe(3)
|
||||
expect(prisma.noteUsage.count).toHaveBeenCalledWith({
|
||||
where: expect.objectContaining({
|
||||
noteId: 'note-1',
|
||||
eventType: 'search_click',
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply default 30 days filter', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(0)
|
||||
|
||||
await getNoteUsageCount('note-1')
|
||||
|
||||
const call = (prisma.noteUsage.count as jest.Mock).mock.calls[0][0]
|
||||
expect(call.where.createdAt.gte).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
it('should return 0 on database error', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock).mockRejectedValue(new Error('DB error'))
|
||||
|
||||
const result = await getNoteUsageCount('note-1')
|
||||
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRecentlyUsedNotes', () => {
|
||||
it('should return recently used notes with counts', async () => {
|
||||
const mockGroupBy = [
|
||||
{ noteId: 'note-1', _count: { id: 10 } },
|
||||
{ noteId: 'note-2', _count: { id: 5 } },
|
||||
]
|
||||
;(prisma.noteUsage.groupBy as jest.Mock).mockResolvedValue(mockGroupBy)
|
||||
;(prisma.note.findMany as jest.Mock).mockResolvedValue([
|
||||
{ id: 'note-1', updatedAt: new Date('2024-01-15') },
|
||||
{ id: 'note-2', updatedAt: new Date('2024-01-14') },
|
||||
])
|
||||
|
||||
const result = await getRecentlyUsedNotes('user-1', 10, 30)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toEqual({
|
||||
noteId: 'note-1',
|
||||
count: 10,
|
||||
lastUsed: expect.any(Date),
|
||||
})
|
||||
})
|
||||
|
||||
it('should return empty array on database error', async () => {
|
||||
;(prisma.noteUsage.groupBy as jest.Mock).mockRejectedValue(new Error('DB error'))
|
||||
|
||||
const result = await getRecentlyUsedNotes('user-1')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should respect limit parameter', async () => {
|
||||
;(prisma.noteUsage.groupBy as jest.Mock).mockResolvedValue([])
|
||||
;(prisma.note.findMany as jest.Mock).mockResolvedValue([])
|
||||
|
||||
await getRecentlyUsedNotes('user-1', 5)
|
||||
|
||||
expect(prisma.noteUsage.groupBy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ take: 5 })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getUsageStats', () => {
|
||||
it('should return usage stats for a note', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock)
|
||||
.mockResolvedValueOnce(15) // views
|
||||
.mockResolvedValueOnce(8) // clicks
|
||||
.mockResolvedValueOnce(3) // relatedClicks
|
||||
|
||||
const result = await getUsageStats('note-1', 30)
|
||||
|
||||
expect(result).toEqual({
|
||||
views: 15,
|
||||
clicks: 8,
|
||||
relatedClicks: 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return zeros on database error', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock).mockRejectedValue(new Error('DB error'))
|
||||
|
||||
const result = await getUsageStats('note-1')
|
||||
|
||||
expect(result).toEqual({
|
||||
views: 0,
|
||||
clicks: 0,
|
||||
relatedClicks: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('should query with correct date filter', async () => {
|
||||
;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(0)
|
||||
|
||||
await getUsageStats('note-1', 7)
|
||||
|
||||
// All three counts should use the same date filter
|
||||
for (const call of (prisma.noteUsage.count as jest.Mock).mock.calls) {
|
||||
expect(call[0].where.createdAt.gte).toBeInstanceOf(Date)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return zeros when any count fails', async () => {
|
||||
// Promise.all will reject on first failure, so we can't get partial results
|
||||
;(prisma.noteUsage.count as jest.Mock)
|
||||
.mockResolvedValueOnce(10) // views
|
||||
.mockRejectedValueOnce(new Error('DB error')) // clicks fails
|
||||
.mockResolvedValueOnce(5) // relatedClicks never called
|
||||
|
||||
const result = await getUsageStats('note-1')
|
||||
|
||||
// All zeros because the whole operation fails
|
||||
expect(result.views).toBe(0)
|
||||
expect(result.clicks).toBe(0)
|
||||
expect(result.relatedClicks).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UsageEventType', () => {
|
||||
it('should accept all valid event types', () => {
|
||||
const eventTypes: UsageEventType[] = ['view', 'search_click', 'related_click', 'link_click']
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
expect(['view', 'search_click', 'related_click', 'link_click']).toContain(eventType)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user