fix: unify theme system - fix theme switching persistence
- 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
This commit is contained in:
631
keep-notes/tests/migration/data-migration.test.ts
Normal file
631
keep-notes/tests/migration/data-migration.test.ts
Normal file
@@ -0,0 +1,631 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user