91 lines
5.6 KiB
JavaScript
91 lines
5.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
require('dotenv').config({ quiet: true });
|
|
const { PrismaClient } = require('@prisma/client');
|
|
const url = new URL(process.env.DATABASE_URL);
|
|
url.hostname = '192.168.0.20';
|
|
url.port = '15437';
|
|
url.pathname = '/lms';
|
|
url.searchParams.set('connect_timeout', '5');
|
|
const db = new PrismaClient({ datasources: { db: { url: url.toString() } } });
|
|
const apply = process.argv.includes('--apply');
|
|
const releasePath = process.env.CAMERA_RELEASE_MANIFEST || '/home/chyoo/다운로드/yahboom-lms-modules/exports/jetson-camera/release.json';
|
|
const ids = ['4eb01197-7605-4c3b-9a92-f9a3942050bd', '8e296f81-8445-455c-b1a2-47f2ce9099a4', '26d46589-31a5-4d94-b0e0-385a12c41721'];
|
|
const folderName = '[lms-nav2-camera-test]nav2-camera-test_v1.0.0_ws';
|
|
const image = 'future-nav2-camera-test:humble-jetson_orin';
|
|
const templateRepositoryUrl = 'https://gitea.autonomystudio.net/templates/lms-nav2-camera-test.git';
|
|
const defaults = ['video_device:=/dev/video0', 'width:=640', 'height:=480', 'fps:=30', 'publish_fps:=10', 'pixel_format:=auto'];
|
|
const snapshot = client => client.course.findMany({ where: { id: { in: ids } }, orderBy: { id: 'asc' }, include: {
|
|
weeks: { orderBy: { week: 'asc' }, include: { modules: { orderBy: { sortOrder: 'asc' } } } },
|
|
environmentProfiles: { orderBy: { id: 'asc' } },
|
|
} });
|
|
|
|
async function main() {
|
|
const release = JSON.parse(fs.readFileSync(releasePath, 'utf8'));
|
|
assert.equal(release.runtimeImages.length, 1, 'Runtime image upload must finish first');
|
|
assert.equal(release.prebuiltArtifacts.length, 1);
|
|
assert.equal(release.runtimeImages[0].image, image);
|
|
assert.equal(release.prebuiltArtifacts[0].architecture, 'arm64');
|
|
for (const item of [...release.runtimeImages, ...release.prebuiltArtifacts]) {
|
|
assert.match(item.checksumSha256, /^[a-f0-9]{64}$/);
|
|
const check = await fetch(item.url, { method: 'HEAD', signal: AbortSignal.timeout(15000) });
|
|
assert(check.ok, `Artifact unavailable: ${check.status}`);
|
|
}
|
|
const before = await snapshot(db);
|
|
assert.equal(before.length, 3, 'Expected the base curriculum and two existing courses');
|
|
assert(before.some(c => c.slug === 'futuredrive-autonomy-basic'));
|
|
for (const c of before) {
|
|
assert.notEqual(c.title, 'NAV2 알아보기');
|
|
const week = c.weeks.find(w => w.week === 3);
|
|
assert(week && week.modules.some(m => m.name === 'NAV2-MKMini'), `${c.title}: MKMini week 3 missing`);
|
|
const profiles = c.environmentProfiles.filter(p => p.week === 3);
|
|
assert.equal(profiles.filter(p => p.config.target === 'remote-jetson').length, 1);
|
|
assert(profiles.filter(p => p.config.target === 'local').every(p => p.config.moduleSet?.includeUnlisted === false),
|
|
'Local profile would implicitly include the ARM64-only camera');
|
|
}
|
|
if (apply) {
|
|
fs.mkdirSync('backups', { recursive: true });
|
|
const backup = path.resolve('backups', `week3-before-camera-${Date.now()}.json`);
|
|
fs.writeFileSync(backup, JSON.stringify(before, null, 2), { flag: 'wx', mode: 0o600 });
|
|
console.log('Backup:', backup);
|
|
}
|
|
const summary = [];
|
|
try {
|
|
await db.$transaction(async tx => {
|
|
assert.deepEqual(await snapshot(tx), before, 'Curriculum changed during preparation');
|
|
for (const c of before) {
|
|
const week = c.weeks.find(w => w.week === 3);
|
|
const existing = week.modules.find(m => m.folderName === folderName);
|
|
const data = { name: 'NAV2-Camera', category: 'Camera', folderName, templateRepositoryUrl,
|
|
sourceType: 'GIT', dockerImage: image, defaultParameters: defaults, prebuiltArtifacts: release.prebuiltArtifacts };
|
|
const module = existing
|
|
? await tx.courseWeekModule.update({ where: { id: existing.id }, data })
|
|
: await tx.courseWeekModule.create({ data: { ...data, weekId: week.id,
|
|
sortOrder: Math.max(0, ...week.modules.map(m => m.sortOrder)) + 1 } });
|
|
const profile = c.environmentProfiles.find(p => p.week === 3 && p.config.target === 'remote-jetson');
|
|
const config = structuredClone(profile.config);
|
|
assert(Array.isArray(config.moduleSet?.modules));
|
|
config.moduleSet.modules = [...config.moduleSet.modules.filter(m => m.id !== module.id),
|
|
{ id: module.id, dockerImage: image, defaultParameters: defaults }];
|
|
config.runtimeImages = [...(config.runtimeImages || []).filter(a => a.image !== image), ...release.runtimeImages];
|
|
await tx.courseEnvironmentProfile.update({ where: { id: profile.id }, data: { config } });
|
|
summary.push({ course: c.title, week: 3, module: module.name, id: module.id, target: config.target });
|
|
}
|
|
const after = await snapshot(tx);
|
|
for (const old of before) {
|
|
const current = after.find(c => c.id === old.id);
|
|
assert.deepEqual(current.weeks.filter(w => w.week !== 3), old.weeks.filter(w => w.week !== 3));
|
|
assert.deepEqual(current.environmentProfiles.filter(p => p.week !== 3 || p.config.target !== 'remote-jetson'),
|
|
old.environmentProfiles.filter(p => p.week !== 3 || p.config.target !== 'remote-jetson'));
|
|
for (const module of old.weeks.find(w => w.week === 3).modules.filter(m => m.folderName !== folderName)) {
|
|
assert.deepEqual(current.weeks.find(w => w.week === 3).modules.find(m => m.id === module.id), module);
|
|
}
|
|
}
|
|
if (!apply) throw new Error('DRY_RUN_ROLLBACK');
|
|
}, { isolationLevel: 'Serializable', timeout: 30000 });
|
|
} catch (e) { if (e.message !== 'DRY_RUN_ROLLBACK') throw e; }
|
|
console.log(JSON.stringify({ applied: apply, courses: summary }, null, 2));
|
|
}
|
|
main().catch(e => { console.error(e.message); process.exitCode = 1; }).finally(() => db.$disconnect());
|