All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
421 lines
12 KiB
TypeScript
421 lines
12 KiB
TypeScript
/**
|
|
* 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 {
|
|
createTestPrismaClient,
|
|
initializeTestDatabase,
|
|
cleanupTestDatabase,
|
|
ensureTestUser,
|
|
createSampleNotes,
|
|
createSampleAINotes,
|
|
verifyTableExists,
|
|
verifyColumnExists,
|
|
} from './setup'
|
|
|
|
describe('Rollback Tests', () => {
|
|
let prisma: PrismaClient
|
|
|
|
beforeAll(async () => {
|
|
prisma = createTestPrismaClient()
|
|
await initializeTestDatabase(prisma)
|
|
// Create fixture users required by FK constraints on Note
|
|
await ensureTestUser(prisma, 'test-user-id')
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await cleanupTestDatabase(prisma)
|
|
})
|
|
|
|
describe('Schema Rollback', () => {
|
|
test('should verify schema state before migration', async () => {
|
|
const hasUser = await verifyTableExists(prisma, 'User')
|
|
expect(hasUser).toBe(true)
|
|
})
|
|
|
|
test('should verify AI tables exist after migration', async () => {
|
|
const hasAiFeedback = await verifyTableExists(prisma, 'AiFeedback')
|
|
expect(hasAiFeedback).toBe(true)
|
|
|
|
const hasMemoryEcho = await verifyTableExists(prisma, 'MemoryEchoInsight')
|
|
expect(hasMemoryEcho).toBe(true)
|
|
|
|
const hasUserAISettings = await verifyTableExists(prisma, 'UserAISettings')
|
|
expect(hasUserAISettings).toBe(true)
|
|
})
|
|
|
|
test('should verify Note AI columns exist after migration', async () => {
|
|
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
|
|
|
|
for (const column of aiColumns) {
|
|
const exists = await verifyColumnExists(prisma, 'Note', column)
|
|
expect(exists).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('should simulate dropping AI columns (rollback scenario)', async () => {
|
|
// In PostgreSQL, ALTER TABLE DROP COLUMN works directly
|
|
// This test verifies we can identify which columns would be dropped
|
|
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
|
|
|
|
for (const column of aiColumns) {
|
|
const exists = await verifyColumnExists(prisma, 'Note', column)
|
|
expect(exists).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('should simulate dropping AI tables (rollback scenario)', async () => {
|
|
const aiTables = ['AiFeedback', 'MemoryEchoInsight', 'UserAISettings']
|
|
|
|
for (const table of aiTables) {
|
|
const exists = await verifyTableExists(prisma, table)
|
|
expect(exists).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Data Recovery After Rollback', () => {
|
|
beforeEach(async () => {
|
|
// Clean up before each test
|
|
await prisma.aiFeedback.deleteMany({})
|
|
await prisma.note.deleteMany({})
|
|
})
|
|
|
|
test('should preserve basic note data if AI columns are dropped', async () => {
|
|
const noteWithAI = await prisma.note.create({
|
|
data: {
|
|
title: 'Note with AI',
|
|
content: 'This note has AI fields',
|
|
userId: 'test-user-id',
|
|
autoGenerated: true,
|
|
aiProvider: 'openai',
|
|
aiConfidence: 95,
|
|
language: 'en',
|
|
languageConfidence: 0.98,
|
|
lastAiAnalysis: new Date()
|
|
}
|
|
})
|
|
|
|
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')
|
|
|
|
const basicNote = await prisma.note.findUnique({
|
|
where: { id: noteWithAI.id },
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
content: true,
|
|
userId: true
|
|
}
|
|
})
|
|
|
|
expect(basicNote?.id).toBe(noteWithAI.id)
|
|
expect(basicNote?.title).toBe(noteWithAI.title)
|
|
expect(basicNote?.content).toBe(noteWithAI.content)
|
|
})
|
|
|
|
test('should preserve note relationships if AI tables are dropped', async () => {
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: 'rollback-test@test.com',
|
|
name: 'Rollback User'
|
|
}
|
|
})
|
|
|
|
const notebook = await prisma.notebook.create({
|
|
data: {
|
|
name: 'Rollback Notebook',
|
|
order: 0,
|
|
userId: user.id
|
|
}
|
|
})
|
|
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Rollback Test Note',
|
|
content: 'Test content',
|
|
userId: user.id,
|
|
notebookId: notebook.id
|
|
}
|
|
})
|
|
|
|
expect(note.userId).toBe(user.id)
|
|
expect(note.notebookId).toBe(notebook.id)
|
|
|
|
const retrievedNote = await prisma.note.findUnique({
|
|
where: { id: note.id },
|
|
include: {
|
|
notebook: true,
|
|
user: true
|
|
}
|
|
})
|
|
|
|
expect(retrievedNote?.userId).toBe(user.id)
|
|
expect(retrievedNote?.notebookId).toBe(notebook.id)
|
|
})
|
|
|
|
test('should handle orphaned records after table drop', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Orphan 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: 'Test feedback'
|
|
}
|
|
})
|
|
|
|
expect(feedback.noteId).toBe(note.id)
|
|
|
|
const noteExists = await prisma.note.findUnique({
|
|
where: { id: note.id }
|
|
})
|
|
|
|
expect(noteExists).toBeDefined()
|
|
expect(noteExists?.id).toBe(note.id)
|
|
})
|
|
|
|
test('should verify no orphaned records exist after proper migration', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Orphan Check Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
await prisma.aiFeedback.create({
|
|
data: {
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: 'thumbs_up',
|
|
feature: 'title_suggestion',
|
|
originalContent: 'Test feedback'
|
|
}
|
|
})
|
|
|
|
const allFeedback = await prisma.aiFeedback.findMany()
|
|
|
|
for (const fb of allFeedback) {
|
|
const noteExists = await prisma.note.findUnique({
|
|
where: { id: fb.noteId }
|
|
})
|
|
expect(noteExists).toBeDefined()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Rollback Safety Checks', () => {
|
|
beforeEach(async () => {
|
|
await prisma.note.deleteMany({})
|
|
})
|
|
|
|
test('should verify data before attempting rollback', async () => {
|
|
await createSampleNotes(prisma, 10)
|
|
|
|
const noteCountBefore = await prisma.note.count()
|
|
expect(noteCountBefore).toBe(10)
|
|
|
|
const notes = await prisma.note.findMany()
|
|
expect(notes.length).toBe(10)
|
|
|
|
for (const note of notes) {
|
|
expect(note.id).toBeDefined()
|
|
expect(note.title).toBeDefined()
|
|
expect(note.content).toBeDefined()
|
|
}
|
|
})
|
|
|
|
test('should identify tables created by migration', async () => {
|
|
const aiTables = ['AiFeedback', 'MemoryEchoInsight', 'UserAISettings']
|
|
let found = 0
|
|
|
|
for (const table of aiTables) {
|
|
const exists = await verifyTableExists(prisma, table)
|
|
if (exists) found++
|
|
}
|
|
|
|
expect(found).toBeGreaterThanOrEqual(3)
|
|
})
|
|
|
|
test('should identify columns added by migration', async () => {
|
|
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
|
|
let found = 0
|
|
|
|
for (const column of aiColumns) {
|
|
const exists = await verifyColumnExists(prisma, 'Note', column)
|
|
if (exists) found++
|
|
}
|
|
|
|
expect(found).toBe(6)
|
|
})
|
|
})
|
|
|
|
describe('Rollback with Data', () => {
|
|
beforeEach(async () => {
|
|
await prisma.aiFeedback.deleteMany({})
|
|
await prisma.note.deleteMany({})
|
|
})
|
|
|
|
test('should preserve essential note data', async () => {
|
|
const notes = await createSampleAINotes(prisma, 20)
|
|
|
|
for (const note of notes) {
|
|
expect(note.id).toBeDefined()
|
|
expect(note.content).toBeDefined()
|
|
}
|
|
|
|
const allNotes = await prisma.note.findMany()
|
|
expect(allNotes.length).toBe(20)
|
|
})
|
|
|
|
test('should handle rollback with complex data structures', async () => {
|
|
const complexNote = await prisma.note.create({
|
|
data: {
|
|
title: 'Complex Note',
|
|
content: '**Markdown** content with [links](https://example.com)',
|
|
checkItems: JSON.stringify([
|
|
{ text: 'Task 1', done: false },
|
|
{ text: 'Task 2', done: true },
|
|
{ text: 'Task 3', done: false }
|
|
]),
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
const retrieved = await prisma.note.findUnique({
|
|
where: { id: complexNote.id }
|
|
})
|
|
|
|
expect(retrieved?.content).toContain('**Markdown**')
|
|
expect(retrieved?.checkItems).toBeDefined()
|
|
|
|
if (retrieved?.checkItems) {
|
|
const checkItems = JSON.parse(retrieved.checkItems as string) as any[]
|
|
expect(checkItems.length).toBe(3)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Rollback Error Handling', () => {
|
|
test('should handle rollback when AI data exists', async () => {
|
|
await createSampleAINotes(prisma, 10)
|
|
|
|
const aiNotes = await prisma.note.findMany({
|
|
where: {
|
|
OR: [
|
|
{ autoGenerated: true },
|
|
{ aiProvider: { not: null } },
|
|
{ language: { not: null } }
|
|
]
|
|
}
|
|
})
|
|
|
|
expect(aiNotes.length).toBeGreaterThan(0)
|
|
|
|
const hasAIData = await prisma.note.findFirst({
|
|
where: {
|
|
autoGenerated: true
|
|
}
|
|
})
|
|
|
|
expect(hasAIData).toBeDefined()
|
|
})
|
|
|
|
test('should handle rollback when feedback exists', async () => {
|
|
const note = await prisma.note.create({
|
|
data: {
|
|
title: 'Feedback Note',
|
|
content: 'Test content',
|
|
userId: 'test-user-id'
|
|
}
|
|
})
|
|
|
|
await prisma.aiFeedback.createMany({
|
|
data: [
|
|
{
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: 'thumbs_up',
|
|
feature: 'title_suggestion',
|
|
originalContent: 'Feedback 1'
|
|
},
|
|
{
|
|
noteId: note.id,
|
|
userId: 'test-user-id',
|
|
feedbackType: 'thumbs_down',
|
|
feature: 'semantic_search',
|
|
originalContent: 'Feedback 2'
|
|
}
|
|
]
|
|
})
|
|
|
|
const feedbackCount = await prisma.aiFeedback.count()
|
|
expect(feedbackCount).toBeGreaterThanOrEqual(2)
|
|
|
|
const feedbacks = await prisma.aiFeedback.findMany()
|
|
expect(feedbacks.length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
})
|
|
|
|
describe('Rollback Validation', () => {
|
|
test('should validate database state after simulated rollback', async () => {
|
|
await createSampleNotes(prisma, 5)
|
|
|
|
const noteCount = await prisma.note.count()
|
|
expect(noteCount).toBeGreaterThanOrEqual(5)
|
|
|
|
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 () => {
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: 'corruption-test@test.com',
|
|
name: 'Corruption Test User'
|
|
}
|
|
})
|
|
|
|
const notebook = await prisma.notebook.create({
|
|
data: {
|
|
name: 'Corruption Test Notebook',
|
|
order: 0,
|
|
userId: user.id
|
|
}
|
|
})
|
|
|
|
await prisma.note.create({
|
|
data: {
|
|
title: 'Corruption Test Note',
|
|
content: 'Test content',
|
|
userId: user.id,
|
|
notebookId: notebook.id
|
|
}
|
|
})
|
|
|
|
const retrievedUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
include: { notebooks: true, notes: true }
|
|
})
|
|
|
|
expect(retrievedUser?.notebooks.length).toBe(1)
|
|
expect(retrievedUser?.notes.length).toBe(1)
|
|
})
|
|
})
|
|
})
|