api-server/.gitea/workflows/backward-compat.yml
wangdl 7d47b3b587
Some checks failed
M-AI-02 Backward Compatibility / backward-compat (push) Failing after 15s
Deploy API Server / build-and-deploy (push) Failing after 27s
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>
2026-06-20 13:34:11 +08:00

244 lines
9.2 KiB
YAML

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 }}