From 7d47b3b587c4a9be9f4b886bad815cbcace082a8 Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 20 Jun 2026 13:34:11 +0800 Subject: [PATCH] ci: add M-AI-01 backward compatibility verification workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Checks out M-AI-01 commit 789d6ec against M-AI-02 migrated DB - Tests: createJob, findById, pending→processing, processing→completed, processing→failed with errorMessage, AiAnalysisResult FK, AiUsageLog insert - Verifies physical columns (errorMessage, completedAt, status, jobType) preserved - Any failure blocks CI Co-Authored-By: Claude --- .gitea/workflows/backward-compat.yml | 243 +++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 .gitea/workflows/backward-compat.yml diff --git a/.gitea/workflows/backward-compat.yml b/.gitea/workflows/backward-compat.yml new file mode 100644 index 0000000..b0fda6f --- /dev/null +++ b/.gitea/workflows/backward-compat.yml @@ -0,0 +1,243 @@ +name: M-AI-02 Backward Compatibility + +on: + push: + branches: [main] + +jobs: + backward-compat: + runs-on: prod + steps: + - name: Checkout M-AI-02 (latest) + run: | + if [ -d /tmp/api-server-compat ]; then + cd /tmp/api-server-compat && git fetch origin && git reset --hard origin/main + else + git clone http://10.2.0.7:3000/wangdl/api-server.git /tmp/api-server-compat + fi + + - name: Ensure infrastructure + run: | + docker start mysql redis 2>/dev/null || true + sleep 2 + + - name: Apply M-AI-02 migrations + run: | + cd /tmp/api-server-compat + npm ci + DATABASE_URL="${{ secrets.DATABASE_URL }}" npx prisma migrate deploy + timeout-minutes: 5 + + - name: Checkout M-AI-01 baseline + run: | + mkdir -p /tmp/api-server-old + cd /tmp/api-server-old + if [ -d .git ]; then + git fetch origin && git checkout 789d6ec --force + else + git clone http://10.2.0.7:3000/wangdl/api-server.git . + git checkout 789d6ec + fi + + - name: Install M-AI-01 dependencies + run: | + cd /tmp/api-server-old + npm ci + timeout-minutes: 5 + + - name: Generate M-AI-01 Prisma client + run: | + cd /tmp/api-server-old + npx prisma generate + + - name: Run backward compatibility script + run: | + cd /tmp/api-server-old + cat > /tmp/compat-test.ts << 'TESTEOF' + import { PrismaClient } from '@prisma/client'; + + const DB_URL = process.env.DATABASE_URL || ''; + + // Use direct MySQL for old-model access + const prisma = new PrismaClient({ datasources: { db: { url: DB_URL } } }); + + async function main() { + const TEST_USER = 'backward-compat-test-' + Date.now(); + let errors = 0; + + // 1. Create test user + await prisma.$executeRawUnsafe( + "INSERT IGNORE INTO User (id, email, createdAt, updatedAt) VALUES (?, ?, NOW(), NOW())", + TEST_USER, TEST_USER + '@test.com' + ); + console.log('1. User created: ' + TEST_USER); + + // 2. createJob via old Prisma delegate (prisma.aiAnalysisJob.create) + let job: any; + try { + job = await (prisma as any).aiAnalysisJob.create({ + data: { + userId: TEST_USER, + jobType: 'feynman-evaluation', + status: 'pending', + sessionId: null, + answerId: null, + queuedAt: new Date(), + }, + }); + console.log('2. createJob: id=' + job.id + ' status=' + job.status + ' ✅'); + } catch (e: any) { + console.log('2. createJob FAILED: ' + e.message + ' ❌'); + errors++; + await prisma.$disconnect(); + process.exit(1); + } + + // 3. findById / findUnique + try { + const found = await (prisma as any).aiAnalysisJob.findUnique({ + where: { id: job.id }, + include: { results: true }, + }); + if (!found) throw new Error('not found'); + console.log('3. findById: found id=' + found.id + ' status=' + found.status + ' ✅'); + } catch (e: any) { + console.log('3. findById FAILED: ' + e.message + ' ❌'); + errors++; + } + + // 4. pending → processing (updateJobStatus) + try { + const updated = await (prisma as any).aiAnalysisJob.update({ + where: { id: job.id }, + data: { status: 'processing', startedAt: new Date() }, + }); + console.log('4. pending→processing: status=' + updated.status + ' ✅'); + } catch (e: any) { + console.log('4. pending→processing FAILED: ' + e.message + ' ❌'); + errors++; + } + + // 5. processing → completed + try { + const updated = await (prisma as any).aiAnalysisJob.update({ + where: { id: job.id }, + data: { status: 'completed', completedAt: new Date() }, + }); + console.log('5. processing→completed: status=' + updated.status + ' ✅'); + } catch (e: any) { + console.log('5. processing→completed FAILED: ' + e.message + ' ❌'); + errors++; + } + + // 6. Create second job for failed test + let job2: any; + try { + job2 = await (prisma as any).aiAnalysisJob.create({ + data: { + userId: TEST_USER, + jobType: 'active-recall', + status: 'pending', + queuedAt: new Date(), + }, + }); + console.log('6. Second job created: id=' + job2.id + ' ✅'); + } catch (e: any) { + console.log('6. Second job FAILED: ' + e.message + ' ❌'); + errors++; + } + + // 7. processing → failed with errorMessage + if (job2) { + try { + const updated = await (prisma as any).aiAnalysisJob.update({ + where: { id: job2.id }, + data: { status: 'processing', startedAt: new Date() }, + }); + const failed = await (prisma as any).aiAnalysisJob.update({ + where: { id: job2.id }, + data: { status: 'failed', errorMessage: 'test error', completedAt: new Date() }, + }); + console.log('7. processing→failed: status=' + failed.status + ' errorMessage=' + failed.errorMessage + ' ✅'); + } catch (e: any) { + console.log('7. processing→failed FAILED: ' + e.message + ' ❌'); + errors++; + } + } + + // 8. Verify physical columns preserved + const cols = await prisma.$queryRawUnsafe( + "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'AiAnalysisJob' AND COLUMN_NAME IN ('errorMessage','completedAt','status','jobType') ORDER BY ORDINAL_POSITION" + ) as any[]; + const colNames = cols.map(c => c.COLUMN_NAME); + const required = ['jobType','status','errorMessage','completedAt']; + const missing = required.filter(r => !colNames.includes(r)); + if (missing.length === 0) { + console.log('8. Physical columns preserved: ' + colNames.join(', ') + ' ✅'); + } else { + console.log('8. MISSING columns: ' + missing.join(', ') + ' ❌'); + errors++; + } + + // 9. AiAnalysisResult FK + try { + await (prisma as any).aiAnalysisResult.create({ + data: { + userId: TEST_USER, + jobId: job.id, + summary: 'test summary', + masteryScore: 80, + }, + }); + const results = await (prisma as any).aiAnalysisResult.findMany({ + where: { jobId: job.id }, + }); + console.log('9. AiAnalysisResult FK: ' + results.length + ' results for job ✅'); + } catch (e: any) { + console.log('9. AiAnalysisResult FK FAILED: ' + e.message + ' ❌'); + errors++; + } + + // 10. AiUsageLog insert + try { + await (prisma as any).aiUsageLog.create({ + data: { + userId: TEST_USER, + feature: 'compat-test', + provider: 'deepseek', + model: 'deepseek-chat', + tier: 'primary', + promptKey: 'test', + promptVersion: 'v1', + }, + }); + console.log('10. AiUsageLog insert ✅'); + } catch (e: any) { + console.log('10. AiUsageLog insert FAILED: ' + e.message + ' ❌'); + errors++; + } + + // Cleanup + await prisma.$executeRawUnsafe("DELETE FROM AiAnalysisResult WHERE userId = ?", TEST_USER); + await prisma.$executeRawUnsafe("DELETE FROM AiJobArtifact WHERE jobId IN (SELECT id FROM AiAnalysisJob WHERE userId = ?)", TEST_USER); + await (prisma as any).aiUsageLog.deleteMany({ where: { userId: TEST_USER } }); + await (prisma as any).aiAnalysisJob.deleteMany({ where: { userId: TEST_USER } }); + await prisma.$executeRawUnsafe("DELETE FROM User WHERE id = ?", TEST_USER); + console.log('Cleanup done.'); + + await prisma.$disconnect(); + + if (errors > 0) { + console.log('\n' + errors + ' test(s) FAILED ❌'); + process.exit(1); + } + console.log('\nAll backward compatibility tests PASSED ✅'); + } + + main().catch(e => { console.error(e); process.exit(1); }); + TESTEOF + + DATABASE_URL="${{ secrets.DATABASE_URL }}" npx tsx /tmp/compat-test.ts + timeout-minutes: 5 + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }}