ci: add M-AI-01 backward compatibility verification workflow
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
a5b210f314
commit
7d47b3b587
243
.gitea/workflows/backward-compat.yml
Normal file
243
.gitea/workflows/backward-compat.yml
Normal file
@ -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 }}
|
||||
Loading…
x
Reference in New Issue
Block a user