- Unified localStorage key to 'theme-preference' across all components
- Fixed header.tsx using wrong localStorage key ('theme' instead of 'theme-preference')
- Added localStorage hybrid persistence for instant theme changes
- Removed router.refresh() which was causing stale data revert
- Replaced Blue theme with Sepia
- Consolidated auth() calls to prevent race conditions
- Updated UserSettingsData types to include all themes
632 lines
19 KiB
TypeScript
632 lines
19 KiB
TypeScript
/**
|
|
* Data Migration Tests
|
|
* Validates that data migration scripts work correctly
|
|
* Tests data transformation, integrity, and edge cases
|
|
*/
|
|
|
|
import { PrismaClient } from '@prisma/client'
|
|
import {
|
|
setupTestEnvironment,
|
|
createTestPrismaClient,
|
|
initializeTestDatabase,
|
|
cleanupTestDatabase,
|
|
createSampleNotes,
|
|
createSampleAINotes,
|
|
verifyDataIntegrity,
|
|
measureExecutionTime
|
|
} from './setup'
|
|
|
|
describe('Data Migration Tests', () => {
|
|
let prisma: PrismaClient
|
|
|
|
beforeAll(async () => {
|
|
await setupTestEnvironment()
|
|
prisma = createTestPrismaClient()
|
|
await initializeTestDatabase(prisma)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await cleanupTestDatabase(prisma)
|
|
})
|
|
|
|
describe('Empty Database Migration', () => {
|
|
test('should migrate empty database successfully', async () => {
|
|
// Verify database is empty
|
|
const noteCount = await prisma.note.count()
|
|
expect(noteCount).toBe(0)
|
|
|
|
// Data migration should handle empty database gracefully
|
|
// No data should be created or lost
|
|
expect(noteCount).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('Basic Data Migration', () => {
|
|
beforeEach(async () => {
|
|
// Clean up before each test
|
|
await prisma.note.deleteMany({})
|
|
await prisma.aiFeedback.deleteMany({})
|
|
})
|
|
|
|
test('should migrate basic notes without AI fields', async () => {
|
|
// Create sample notes (simulating pre-migration data)
|
|
await createSampleNotes(prisma, 10)
|
|
|
|
// Verify notes are created
|
|
const noteCount = await prisma.note.count()
|
|
expect(noteCount).toBe(10)
|
|
|
|
// All notes should have null AI fields (backward compatibility)
|
|
const notes = await prisma.note.findMany()
|
|
notes.forEach(note => {
|
|
expect(note.autoGenerated).toBeNull()
|
|
expect(note.aiProvider).toBeNull()
|
|
expect(note.aiConfidence).toBeNull()
|
|
expect(note.language).toBeNull()
|
|
expect(note.languageConfidence).toBeNull()
|
|
expect(note.lastAiAnalysis).toBeNull()
|
|
})
|
|
})
|
|
|
|
test('should preserve existing note data during migration', async () => {
|
|
// Create a note with all fields
|
|
const originalNote = await prisma.note.create({
|
|
data: {
|
|
title: 'Original Note',
|
|
content: 'Original content',
|
|
color: 'blue',
|
|
isPinned: true,
|
|
isArchived: false,
|
|
type: 'text',
|
|
size: 'medium',
|
|
userId: 'test-user-id',
|
|
order: 0
|
|
}
|
|
})
|
|
|
|
// Simulate migration by querying the note
|
|
const noteAfterMigration = await prisma.note.findUnique({
|
|
where: { id: originalNote.id }
|
|
})
|
|
|
|
// Verify all original fields are preserved
|
|
expect(noteAfterMigration?.title).toBe('Original Note')
|
|
expect(noteAfterMigration?.content).toBe('Original content')
|
|
expect(noteAfterMigration?.color).toBe('blue')
|
|
expect(noteAfterMigration?.isPinned).toBe(true)
|
|
expect(noteAfterMigration?.isArchived).toBe(false)
|
|
expect(noteAfterMigration?.type).toBe('text')
|
|
expect(noteAfterMigration?.size).toBe('medium')
|
|
expect(noteAfterMigration?.order).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('AI Fields Data Migration', () => {
|
|
test('should handle notes with all AI fields populated', async () => {
|
|
const testNote = await prisma.note.create({
|
|
data: {
|
|
title: 'AI Test Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id',
|
|
autoGenerated: true,
|
|
aiProvider: 'openai',
|
|
aiConfidence: 95,
|
|
language: 'en',
|
|
languageConfidence: 0.98,
|
|
lastAiAnalysis: new Date()
|
|
}
|
|
})
|
|
|
|
// Verify AI fields are correctly stored
|
|
expect(testNote.autoGenerated).toBe(true)
|
|
expect(testNote.aiProvider).toBe('openai')
|
|
expect(testNote.aiConfidence).toBe(95)
|
|
expect(testNote.language).toBe('en')
|
|
expect(testNote.languageConfidence).toBe(0.98)
|
|
expect(testNote.lastAiAnalysis).toBeDefined()
|
|
})
|
|
|
|
test('should handle notes with partial AI fields', async () => {
|
|
// Create note with only some AI fields
|
|
const note1 = await prisma.note.create({
|
|
data: {
|
|
title: 'Partial AI Note 1',
|
|
content: 'Test content',
|
|
userId: 'test-user-id',
|
|
autoGenerated: true,
|
|
aiProvider: 'ollama'
|
|
// Other AI fields are null
|
|
}
|
|
})
|
|
|
|
expect(note1.autoGenerated).toBe(true)
|
|
expect(note1.aiProvider).toBe('ollama')
|
|
expect(note1.aiConfidence).toBeNull()
|
|
expect(note1.language).toBeNull()
|
|
|
|
// Create note with different partial fields
|
|
const note2 = await prisma.note.create({
|
|
data: {
|
|
title: 'Partial AI Note 2',
|
|
content: 'Test content',
|
|
userId: 'test-user-id',
|
|
aiConfidence: 87,
|
|
language: 'fr',
|
|
languageConfidence: 0.92
|
|
// Other AI fields are null
|
|
}
|
|
})
|
|
|
|
expect(note2.autoGenerated).toBeNull()
|
|
expect(note2.aiProvider).toBeNull()
|
|
expect(note2.aiConfidence).toBe(87)
|
|
expect(note2.language).toBe('fr')
|
|
expect(note2.languageConfidence).toBe(0.92)
|
|
})
|
|
|
|
test('should handle null values in AI fields correctly', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Null AI Fields Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
// All AI fields are null by default
|
|
}
|
|
})
|
|
|
|
// Verify all AI fields are null
|
|
expect(note.autoGenerated).toBeNull()
|
|
expect(note.aiProvider).toBeNull()
|
|
expect(note.aiConfidence).toBeNull()
|
|
expect(note.language).toBeNull()
|
|
expect(note.languageConfidence).toBeNull()
|
|
expect(note.lastAiAnalysis).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('AiFeedback Data Migration', () => {
|
|
test('should create and retrieve AiFeedback entries', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Feedback Test Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const feedback = await prisma.aiFeedback.create({
|
|
data: {
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: 'thumbs_up',
|
|
feature: 'title_suggestion',
|
|
originalContent: 'AI suggested title',
|
|
metadata: JSON.stringify({
|
|
aiProvider: 'openai',
|
|
confidence: 95,
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
}
|
|
})
|
|
|
|
// Verify feedback is correctly stored
|
|
expect(feedback.noteId).toBe(note.id)
|
|
expect(feedback.feedbackType).toBe('thumbs_up')
|
|
expect(feedback.feature).toBe('title_suggestion')
|
|
expect(feedback.originalContent).toBe('AI suggested title')
|
|
expect(feedback.metadata).toBeDefined()
|
|
})
|
|
|
|
test('should handle different feedback types', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Feedback Types Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const feedbackTypes = [
|
|
{ type: 'thumbs_up', feature: 'title_suggestion', content: 'Good suggestion' },
|
|
{ type: 'thumbs_down', feature: 'semantic_search', content: 'Bad result' },
|
|
{ type: 'correction', feature: 'title_suggestion', content: 'Wrong', corrected: 'Correct' }
|
|
]
|
|
|
|
for (const fb of feedbackTypes) {
|
|
const feedback = await prisma.aiFeedback.create({
|
|
data: {
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: fb.type,
|
|
feature: fb.feature,
|
|
originalContent: fb.content,
|
|
correctedContent: fb.corrected
|
|
}
|
|
})
|
|
|
|
expect(feedback.feedbackType).toBe(fb.type)
|
|
}
|
|
})
|
|
|
|
test('should store and retrieve metadata JSON correctly', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Metadata Test Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const metadata = {
|
|
aiProvider: 'ollama',
|
|
model: 'llama2-7b',
|
|
confidence: 87,
|
|
timestamp: new Date().toISOString(),
|
|
additional: {
|
|
latency: 234,
|
|
tokens: 456
|
|
}
|
|
}
|
|
|
|
const feedback = await prisma.aiFeedback.create({
|
|
data: {
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: 'thumbs_up',
|
|
feature: 'title_suggestion',
|
|
originalContent: 'Test suggestion',
|
|
metadata: JSON.stringify(metadata)
|
|
}
|
|
})
|
|
|
|
// Parse and verify metadata
|
|
const parsedMetadata = JSON.parse(feedback.metadata || '{}')
|
|
expect(parsedMetadata.aiProvider).toBe('ollama')
|
|
expect(parsedMetadata.confidence).toBe(87)
|
|
expect(parsedMetadata.additional).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('MemoryEchoInsight Data Migration', () => {
|
|
test('should create and retrieve MemoryEchoInsight entries', async () => {
|
|
const note1 = await prisma.note.create({
|
|
data: {
|
|
title: 'Echo Note 1',
|
|
content: 'Content about programming',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const note2 = await prisma.note.create({
|
|
data: {
|
|
title: 'Echo Note 2',
|
|
content: 'Content about coding',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const insight = await prisma.memoryEchoInsight.create({
|
|
data: {
|
|
userId: 'test-user-id',
|
|
note1Id: note1.id,
|
|
note2Id: note2.id,
|
|
similarityScore: 0.85,
|
|
insight: 'These notes are similar because they both discuss programming concepts',
|
|
insightDate: new Date()
|
|
}
|
|
})
|
|
|
|
expect(insight.note1Id).toBe(note1.id)
|
|
expect(insight.note2Id).toBe(note2.id)
|
|
expect(insight.similarityScore).toBe(0.85)
|
|
expect(insight.insight).toBeDefined()
|
|
})
|
|
|
|
test('should handle insight feedback and dismissal', async () => {
|
|
const note1 = await prisma.note.create({
|
|
data: {
|
|
title: 'Echo Feedback Note 1',
|
|
content: 'Content A',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const note2 = await prisma.note.create({
|
|
data: {
|
|
title: 'Echo Feedback Note 2',
|
|
content: 'Content B',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const insight = await prisma.memoryEchoInsight.create({
|
|
data: {
|
|
userId: 'test-user-id',
|
|
note1Id: note1.id,
|
|
note2Id: note2.id,
|
|
similarityScore: 0.75,
|
|
insight: 'Test insight',
|
|
feedback: 'useful',
|
|
dismissed: false
|
|
}
|
|
})
|
|
|
|
expect(insight.feedback).toBe('useful')
|
|
expect(insight.dismissed).toBe(false)
|
|
|
|
// Update insight to mark as dismissed
|
|
const updatedInsight = await prisma.memoryEchoInsight.update({
|
|
where: { id: insight.id },
|
|
data: { dismissed: true }
|
|
})
|
|
|
|
expect(updatedInsight.dismissed).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('UserAISettings Data Migration', () => {
|
|
test('should create and retrieve UserAISettings', async () => {
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: 'ai-settings@test.com',
|
|
name: 'AI Settings User'
|
|
}
|
|
})
|
|
|
|
const settings = await prisma.userAISettings.create({
|
|
data: {
|
|
userId: user.id,
|
|
titleSuggestions: true,
|
|
semanticSearch: false,
|
|
memoryEcho: true,
|
|
memoryEchoFrequency: 'weekly',
|
|
aiProvider: 'ollama',
|
|
preferredLanguage: 'fr'
|
|
}
|
|
})
|
|
|
|
expect(settings.userId).toBe(user.id)
|
|
expect(settings.titleSuggestions).toBe(true)
|
|
expect(settings.semanticSearch).toBe(false)
|
|
expect(settings.memoryEchoFrequency).toBe('weekly')
|
|
expect(settings.aiProvider).toBe('ollama')
|
|
expect(settings.preferredLanguage).toBe('fr')
|
|
})
|
|
|
|
test('should handle default values correctly', async () => {
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: 'default-ai-settings@test.com',
|
|
name: 'Default AI User'
|
|
}
|
|
})
|
|
|
|
const settings = await prisma.userAISettings.create({
|
|
data: {
|
|
userId: user.id
|
|
// All other fields should use defaults
|
|
}
|
|
})
|
|
|
|
expect(settings.titleSuggestions).toBe(true)
|
|
expect(settings.semanticSearch).toBe(true)
|
|
expect(settings.memoryEcho).toBe(true)
|
|
expect(settings.memoryEchoFrequency).toBe('daily')
|
|
expect(settings.aiProvider).toBe('auto')
|
|
expect(settings.preferredLanguage).toBe('auto')
|
|
})
|
|
})
|
|
|
|
describe('Data Integrity', () => {
|
|
test('should verify no data loss after migration', async () => {
|
|
// Create initial data
|
|
const initialNotes = await createSampleNotes(prisma, 50)
|
|
const initialCount = initialNotes.length
|
|
|
|
// Simulate migration by querying data
|
|
const notesAfterMigration = await prisma.note.findMany()
|
|
const finalCount = notesAfterMigration.length
|
|
|
|
// Verify no data loss
|
|
expect(finalCount).toBe(initialCount)
|
|
|
|
// Verify each note's data is intact
|
|
for (const note of notesAfterMigration) {
|
|
expect(note.title).toBeDefined()
|
|
expect(note.content).toBeDefined()
|
|
}
|
|
})
|
|
|
|
test('should verify no data corruption after migration', async () => {
|
|
// Create notes with complex data
|
|
await prisma.note.create({
|
|
data: {
|
|
title: 'Complex Data Note',
|
|
content: 'This is a note with **markdown** formatting',
|
|
checkItems: JSON.stringify([{ text: 'Task 1', done: false }, { text: 'Task 2', done: true }]),
|
|
images: JSON.stringify([{ url: 'image1.jpg', caption: 'Caption 1' }]),
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const note = await prisma.note.findFirst({
|
|
where: { title: 'Complex Data Note' }
|
|
})
|
|
|
|
// Verify complex data is preserved
|
|
expect(note?.content).toContain('**markdown**')
|
|
|
|
if (note?.checkItems) {
|
|
const checkItems = JSON.parse(note.checkItems)
|
|
expect(checkItems.length).toBe(2)
|
|
}
|
|
|
|
if (note?.images) {
|
|
const images = JSON.parse(note.images)
|
|
expect(images.length).toBe(1)
|
|
}
|
|
})
|
|
|
|
test('should maintain foreign key relationships', async () => {
|
|
// Create a user
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: 'fk-test@test.com',
|
|
name: 'FK Test User'
|
|
}
|
|
})
|
|
|
|
// Create a notebook for the user
|
|
const notebook = await prisma.notebook.create({
|
|
data: {
|
|
name: 'FK Test Notebook',
|
|
order: 0,
|
|
userId: user.id
|
|
}
|
|
})
|
|
|
|
// Create notes in the notebook
|
|
await prisma.note.createMany({
|
|
data: [
|
|
{
|
|
title: 'FK Note 1',
|
|
content: 'Content 1',
|
|
userId: user.id,
|
|
notebookId: notebook.id
|
|
},
|
|
{
|
|
title: 'FK Note 2',
|
|
content: 'Content 2',
|
|
userId: user.id,
|
|
notebookId: notebook.id
|
|
}
|
|
]
|
|
})
|
|
|
|
// Verify relationships are maintained
|
|
const retrievedNotebook = await prisma.notebook.findUnique({
|
|
where: { id: notebook.id },
|
|
include: { notes: true }
|
|
})
|
|
|
|
expect(retrievedNotebook?.notes.length).toBe(2)
|
|
expect(retrievedNotebook?.userId).toBe(user.id)
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
test('should handle empty strings in text fields', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: '',
|
|
content: 'Content with empty title',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
expect(note.title).toBe('')
|
|
expect(note.content).toBe('Content with empty title')
|
|
})
|
|
|
|
test('should handle very long text content', async () => {
|
|
const longContent = 'A'.repeat(10000)
|
|
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Long Content Note',
|
|
content: longContent,
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
expect(note.content).toHaveLength(10000)
|
|
})
|
|
|
|
test('should handle special characters in text fields', async () => {
|
|
const specialChars = 'Note with émojis 🎉 and spëcial çharacters & spåcial ñumbers 123'
|
|
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: specialChars,
|
|
content: specialChars,
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
expect(note.title).toBe(specialChars)
|
|
expect(note.content).toBe(specialChars)
|
|
})
|
|
|
|
test('should handle null userId in some tables (optional relationships)', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'No User Note',
|
|
content: 'Note without userId',
|
|
userId: null
|
|
}
|
|
})
|
|
|
|
expect(note.userId).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Migration Performance', () => {
|
|
test('should complete migration within acceptable time for 100 notes', async () => {
|
|
// Clean up
|
|
await prisma.note.deleteMany({})
|
|
|
|
// Create 100 notes and measure time
|
|
const { result, duration } = await measureExecutionTime(async () => {
|
|
await createSampleNotes(prisma, 100)
|
|
})
|
|
|
|
// Migration should complete quickly (< 5 seconds for 100 notes)
|
|
expect(duration).toBeLessThan(5000)
|
|
expect(result.length).toBe(100)
|
|
})
|
|
})
|
|
|
|
describe('Batch Operations', () => {
|
|
test('should handle batch insert of notes', async () => {
|
|
const notesData = Array.from({ length: 20 }, (_, i) => ({
|
|
title: `Batch Note ${i + 1}`,
|
|
content: `Batch content ${i + 1}`,
|
|
userId: 'test-user-id',
|
|
order: i
|
|
}))
|
|
|
|
await prisma.note.createMany({
|
|
data: notesData
|
|
})
|
|
|
|
const count = await prisma.note.count()
|
|
expect(count).toBe(20)
|
|
})
|
|
|
|
test('should handle batch insert of feedback', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Batch Feedback Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const feedbackData = Array.from({ length: 10 }, (_, i) => ({
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: 'thumbs_up',
|
|
feature: 'title_suggestion',
|
|
originalContent: `Feedback ${i + 1}`
|
|
}))
|
|
|
|
await prisma.aiFeedback.createMany({
|
|
data: feedbackData
|
|
})
|
|
|
|
const count = await prisma.aiFeedback.count()
|
|
expect(count).toBe(10)
|
|
})
|
|
})
|
|
})
|