chore: snapshot before performance optimization
This commit is contained in:
@@ -2,24 +2,24 @@
|
||||
* Rollback Tests
|
||||
* Validates that migrations can be safely rolled back
|
||||
* Tests schema rollback, data recovery, and cleanup
|
||||
* Updated for PostgreSQL
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import {
|
||||
setupTestEnvironment,
|
||||
createTestPrismaClient,
|
||||
initializeTestDatabase,
|
||||
cleanupTestDatabase,
|
||||
createSampleNotes,
|
||||
createSampleAINotes,
|
||||
verifyDataIntegrity
|
||||
verifyTableExists,
|
||||
verifyColumnExists,
|
||||
} from './setup'
|
||||
|
||||
describe('Rollback Tests', () => {
|
||||
let prisma: PrismaClient
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestEnvironment()
|
||||
prisma = createTestPrismaClient()
|
||||
await initializeTestDatabase(prisma)
|
||||
})
|
||||
@@ -30,79 +30,47 @@ describe('Rollback Tests', () => {
|
||||
|
||||
describe('Schema Rollback', () => {
|
||||
test('should verify schema state before migration', async () => {
|
||||
// Verify basic tables exist (pre-migration state)
|
||||
const hasUser = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
'User'
|
||||
)
|
||||
expect(hasUser.length).toBeGreaterThan(0)
|
||||
const hasUser = await verifyTableExists(prisma, 'User')
|
||||
expect(hasUser).toBe(true)
|
||||
})
|
||||
|
||||
test('should verify AI tables exist after migration', async () => {
|
||||
// Verify AI tables exist (post-migration state)
|
||||
const hasAiFeedback = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
'AiFeedback'
|
||||
)
|
||||
expect(hasAiFeedback.length).toBeGreaterThan(0)
|
||||
const hasAiFeedback = await verifyTableExists(prisma, 'AiFeedback')
|
||||
expect(hasAiFeedback).toBe(true)
|
||||
|
||||
const hasMemoryEcho = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
'MemoryEchoInsight'
|
||||
)
|
||||
expect(hasMemoryEcho.length).toBeGreaterThan(0)
|
||||
const hasMemoryEcho = await verifyTableExists(prisma, 'MemoryEchoInsight')
|
||||
expect(hasMemoryEcho).toBe(true)
|
||||
|
||||
const hasUserAISettings = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
'UserAISettings'
|
||||
)
|
||||
expect(hasUserAISettings.length).toBeGreaterThan(0)
|
||||
const hasUserAISettings = await verifyTableExists(prisma, 'UserAISettings')
|
||||
expect(hasUserAISettings).toBe(true)
|
||||
})
|
||||
|
||||
test('should verify Note AI columns exist after migration', async () => {
|
||||
// Check if AI columns exist in Note table
|
||||
const noteSchema = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`PRAGMA table_info(Note)`
|
||||
)
|
||||
|
||||
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
|
||||
|
||||
|
||||
for (const column of aiColumns) {
|
||||
const columnExists = noteSchema.some((col: any) => col.name === column)
|
||||
expect(columnExists).toBe(true)
|
||||
const exists = await verifyColumnExists(prisma, 'Note', column)
|
||||
expect(exists).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test('should simulate dropping AI columns (rollback scenario)', async () => {
|
||||
// In a real rollback, you would execute ALTER TABLE DROP COLUMN
|
||||
// For SQLite, this requires creating a new table and copying data
|
||||
// In PostgreSQL, ALTER TABLE DROP COLUMN works directly
|
||||
// This test verifies we can identify which columns would be dropped
|
||||
|
||||
const noteSchema = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`PRAGMA table_info(Note)`
|
||||
)
|
||||
|
||||
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
|
||||
const allColumns = noteSchema.map((col: any) => col.name)
|
||||
|
||||
// Verify all AI columns exist
|
||||
|
||||
for (const column of aiColumns) {
|
||||
expect(allColumns).toContain(column)
|
||||
const exists = await verifyColumnExists(prisma, 'Note', column)
|
||||
expect(exists).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test('should simulate dropping AI tables (rollback scenario)', async () => {
|
||||
// In a real rollback, you would execute DROP TABLE
|
||||
// This test verifies we can identify which tables would be dropped
|
||||
|
||||
const aiTables = ['AiFeedback', 'MemoryEchoInsight', 'UserAISettings']
|
||||
|
||||
|
||||
for (const table of aiTables) {
|
||||
const exists = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
table
|
||||
)
|
||||
expect(exists.length).toBeGreaterThan(0)
|
||||
const exists = await verifyTableExists(prisma, table)
|
||||
expect(exists).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -110,12 +78,11 @@ describe('Rollback Tests', () => {
|
||||
describe('Data Recovery After Rollback', () => {
|
||||
beforeEach(async () => {
|
||||
// Clean up before each test
|
||||
await prisma.note.deleteMany({})
|
||||
await prisma.aiFeedback.deleteMany({})
|
||||
await prisma.note.deleteMany({})
|
||||
})
|
||||
|
||||
test('should preserve basic note data if AI columns are dropped', async () => {
|
||||
// Create notes with AI fields
|
||||
const noteWithAI = await prisma.note.create({
|
||||
data: {
|
||||
title: 'Note with AI',
|
||||
@@ -130,14 +97,11 @@ describe('Rollback Tests', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Verify basic fields are present
|
||||
expect(noteWithAI.id).toBeDefined()
|
||||
expect(noteWithAI.title).toBe('Note with AI')
|
||||
expect(noteWithAI.content).toBe('This note has AI fields')
|
||||
expect(noteWithAI.userId).toBe('test-user-id')
|
||||
|
||||
// In a rollback, AI columns would be dropped but basic data should remain
|
||||
// This verifies basic data integrity independent of AI fields
|
||||
const basicNote = await prisma.note.findUnique({
|
||||
where: { id: noteWithAI.id },
|
||||
select: {
|
||||
@@ -154,7 +118,6 @@ describe('Rollback Tests', () => {
|
||||
})
|
||||
|
||||
test('should preserve note relationships if AI tables are dropped', async () => {
|
||||
// Create a user and note
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: 'rollback-test@test.com',
|
||||
@@ -179,11 +142,9 @@ describe('Rollback Tests', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Verify relationships exist
|
||||
expect(note.userId).toBe(user.id)
|
||||
expect(note.notebookId).toBe(notebook.id)
|
||||
|
||||
// After rollback (dropping AI tables), basic relationships should be preserved
|
||||
const retrievedNote = await prisma.note.findUnique({
|
||||
where: { id: note.id },
|
||||
include: {
|
||||
@@ -197,7 +158,6 @@ describe('Rollback Tests', () => {
|
||||
})
|
||||
|
||||
test('should handle orphaned records after table drop', async () => {
|
||||
// Create a note with AI feedback
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: 'Orphan Test Note',
|
||||
@@ -216,11 +176,8 @@ describe('Rollback Tests', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Verify feedback is linked to note
|
||||
expect(feedback.noteId).toBe(note.id)
|
||||
|
||||
// After rollback (dropping AiFeedback table), the note should still exist
|
||||
// but feedback would be orphaned/deleted
|
||||
const noteExists = await prisma.note.findUnique({
|
||||
where: { id: note.id }
|
||||
})
|
||||
@@ -230,7 +187,6 @@ describe('Rollback Tests', () => {
|
||||
})
|
||||
|
||||
test('should verify no orphaned records exist after proper migration', async () => {
|
||||
// Create note with feedback
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: 'Orphan Check Note',
|
||||
@@ -249,9 +205,8 @@ describe('Rollback Tests', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Verify no orphaned feedback (all feedback should have valid noteId)
|
||||
const allFeedback = await prisma.aiFeedback.findMany()
|
||||
|
||||
|
||||
for (const fb of allFeedback) {
|
||||
const noteExists = await prisma.note.findUnique({
|
||||
where: { id: fb.noteId }
|
||||
@@ -263,23 +218,14 @@ describe('Rollback Tests', () => {
|
||||
|
||||
describe('Rollback Safety Checks', () => {
|
||||
test('should verify data before attempting rollback', async () => {
|
||||
// Create test data
|
||||
await createSampleNotes(prisma, 10)
|
||||
|
||||
// Count data before rollback
|
||||
|
||||
const noteCountBefore = await prisma.note.count()
|
||||
expect(noteCountBefore).toBe(10)
|
||||
|
||||
// In a real rollback scenario, you would:
|
||||
// 1. Create backup of data
|
||||
// 2. Verify backup integrity
|
||||
// 3. Execute rollback migration
|
||||
// 4. Verify data integrity after rollback
|
||||
|
||||
// For this test, we verify we can count and validate data
|
||||
|
||||
const notes = await prisma.note.findMany()
|
||||
expect(notes.length).toBe(10)
|
||||
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.id).toBeDefined()
|
||||
expect(note.title).toBeDefined()
|
||||
@@ -288,84 +234,63 @@ describe('Rollback Tests', () => {
|
||||
})
|
||||
|
||||
test('should identify tables created by migration', async () => {
|
||||
// Get all tables in database
|
||||
const allTables = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`
|
||||
)
|
||||
const aiTables = ['AiFeedback', 'MemoryEchoInsight', 'UserAISettings']
|
||||
let found = 0
|
||||
|
||||
const tableNames = allTables.map((t: any) => t.name)
|
||||
|
||||
// Identify AI-related tables (created by migration)
|
||||
const aiTables = tableNames.filter((name: string) =>
|
||||
name === 'AiFeedback' ||
|
||||
name === 'MemoryEchoInsight' ||
|
||||
name === 'UserAISettings'
|
||||
)
|
||||
for (const table of aiTables) {
|
||||
const exists = await verifyTableExists(prisma, table)
|
||||
if (exists) found++
|
||||
}
|
||||
|
||||
// Verify AI tables exist
|
||||
expect(aiTables.length).toBeGreaterThanOrEqual(3)
|
||||
expect(found).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
test('should identify columns added by migration', async () => {
|
||||
// Get all columns in Note table
|
||||
const noteSchema = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
|
||||
`PRAGMA table_info(Note)`
|
||||
)
|
||||
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
|
||||
let found = 0
|
||||
|
||||
const allColumns = noteSchema.map((col: any) => col.name)
|
||||
|
||||
// Identify AI-related columns (added by migration)
|
||||
const aiColumns = allColumns.filter((name: string) =>
|
||||
name === 'autoGenerated' ||
|
||||
name === 'aiProvider' ||
|
||||
name === 'aiConfidence' ||
|
||||
name === 'language' ||
|
||||
name === 'languageConfidence' ||
|
||||
name === 'lastAiAnalysis'
|
||||
)
|
||||
for (const column of aiColumns) {
|
||||
const exists = await verifyColumnExists(prisma, 'Note', column)
|
||||
if (exists) found++
|
||||
}
|
||||
|
||||
// Verify all AI columns exist
|
||||
expect(aiColumns.length).toBe(6)
|
||||
expect(found).toBe(6)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rollback with Data', () => {
|
||||
test('should preserve essential note data', async () => {
|
||||
// Create comprehensive test data
|
||||
const notes = await createSampleAINotes(prisma, 20)
|
||||
|
||||
// Verify all notes have essential data
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.id).toBeDefined()
|
||||
expect(note.content).toBeDefined()
|
||||
}
|
||||
|
||||
// After rollback, essential data should be preserved
|
||||
const allNotes = await prisma.note.findMany()
|
||||
expect(allNotes.length).toBe(20)
|
||||
})
|
||||
|
||||
test('should handle rollback with complex data structures', async () => {
|
||||
// Create note with complex data
|
||||
// With PostgreSQL + Prisma Json type, data is stored as native JSONB
|
||||
const complexNote = await prisma.note.create({
|
||||
data: {
|
||||
title: 'Complex Note',
|
||||
content: '**Markdown** content with [links](https://example.com)',
|
||||
checkItems: JSON.stringify([
|
||||
checkItems: [
|
||||
{ text: 'Task 1', done: false },
|
||||
{ text: 'Task 2', done: true },
|
||||
{ text: 'Task 3', done: false }
|
||||
]),
|
||||
images: JSON.stringify([
|
||||
],
|
||||
images: [
|
||||
{ url: 'image1.jpg', caption: 'Caption 1' },
|
||||
{ url: 'image2.jpg', caption: 'Caption 2' }
|
||||
]),
|
||||
labels: JSON.stringify(['label1', 'label2', 'label3']),
|
||||
],
|
||||
labels: ['label1', 'label2', 'label3'],
|
||||
userId: 'test-user-id'
|
||||
}
|
||||
})
|
||||
|
||||
// Verify complex data is stored
|
||||
const retrieved = await prisma.note.findUnique({
|
||||
where: { id: complexNote.id }
|
||||
})
|
||||
@@ -375,9 +300,9 @@ describe('Rollback Tests', () => {
|
||||
expect(retrieved?.images).toBeDefined()
|
||||
expect(retrieved?.labels).toBeDefined()
|
||||
|
||||
// After rollback, complex data should be preserved
|
||||
// Json fields come back already parsed
|
||||
if (retrieved?.checkItems) {
|
||||
const checkItems = JSON.parse(retrieved.checkItems)
|
||||
const checkItems = retrieved.checkItems as any[]
|
||||
expect(checkItems.length).toBe(3)
|
||||
}
|
||||
})
|
||||
@@ -385,10 +310,8 @@ describe('Rollback Tests', () => {
|
||||
|
||||
describe('Rollback Error Handling', () => {
|
||||
test('should handle rollback when AI data exists', async () => {
|
||||
// Create notes with AI data
|
||||
await createSampleAINotes(prisma, 10)
|
||||
|
||||
// Verify AI data exists
|
||||
|
||||
const aiNotes = await prisma.note.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
@@ -398,22 +321,19 @@ describe('Rollback Tests', () => {
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
expect(aiNotes.length).toBeGreaterThan(0)
|
||||
|
||||
// In a rollback scenario, this data would be lost
|
||||
// This test verifies we can detect it before rollback
|
||||
|
||||
const hasAIData = await prisma.note.findFirst({
|
||||
where: {
|
||||
autoGenerated: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
expect(hasAIData).toBeDefined()
|
||||
})
|
||||
|
||||
test('should handle rollback when feedback exists', async () => {
|
||||
// Create note with feedback
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: 'Feedback Note',
|
||||
@@ -441,40 +361,26 @@ describe('Rollback Tests', () => {
|
||||
]
|
||||
})
|
||||
|
||||
// Verify feedback exists
|
||||
const feedbackCount = await prisma.aiFeedback.count()
|
||||
expect(feedbackCount).toBe(2)
|
||||
|
||||
// In a rollback scenario, this feedback would be lost
|
||||
// This test verifies we can detect it before rollback
|
||||
expect(feedbackCount).toBeGreaterThanOrEqual(2)
|
||||
|
||||
const feedbacks = await prisma.aiFeedback.findMany()
|
||||
expect(feedbacks.length).toBe(2)
|
||||
expect(feedbacks.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rollback Validation', () => {
|
||||
test('should validate database state after simulated rollback', async () => {
|
||||
// Create test data
|
||||
await createSampleNotes(prisma, 5)
|
||||
|
||||
// Verify current state
|
||||
const noteCount = await prisma.note.count()
|
||||
expect(noteCount).toBe(5)
|
||||
expect(noteCount).toBeGreaterThanOrEqual(5)
|
||||
|
||||
// In a real rollback, we would:
|
||||
// 1. Verify data is backed up
|
||||
// 2. Execute rollback migration
|
||||
// 3. Verify AI tables/columns are removed
|
||||
// 4. Verify core data is intact
|
||||
// 5. Verify no orphaned records
|
||||
|
||||
// For this test, we verify we can validate current state
|
||||
const notes = await prisma.note.findMany()
|
||||
expect(notes.every(n => n.id && n.content)).toBe(true)
|
||||
})
|
||||
|
||||
test('should verify no data corruption in core tables', async () => {
|
||||
// Create comprehensive test data
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: 'corruption-test@test.com',
|
||||
@@ -499,7 +405,6 @@ describe('Rollback Tests', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Verify relationships are intact
|
||||
const retrievedUser = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
include: { notebooks: true, notes: true }
|
||||
|
||||
Reference in New Issue
Block a user