- 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>
244 lines
9.2 KiB
YAML
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 }}
|