Files
rootnode-academy/infra/backup/seed-realistic-data.js
T
ced4568 f5110d3c1b test(dr): destructive Postgres restore drill
infra/backup/test-destructive-restore-drill.sh - realistic academy data
(owner, admin instructor, cohort, 3 students, progress, attempts, an
assessment definition, achievements) seeded through the REAL Platform API,
encrypted-backed-up, the source Postgres container genuinely destroyed
(docker rm, not just left running alongside a second one), a fresh
Postgres created from nothing, restored, and the real Platform API booted
again against the restored database and used to log in as the restored
admin instructor over HTTP. Never touches the developer's real database.
Confirmed passing.
2026-08-24 20:15:45 -05:00

95 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
// Seeds realistic academy data for the Phase 4.1 §20 destructive restore drill — an owner, an
// admin instructor, a cohort, 3 students, progress, attempts, an assessment definition +
// session, and an achievement. Uses the REAL Platform API over HTTP for everything that
// doesn't need a live Provisioner/k3s Lab (owner login, instructor/cohort/student/assessment
// creation — genuinely exercises the real RBAC/creation code paths), and direct SQL inserts
// only for progress/attempts/achievements (those need real scenario activity in the full app;
// re-running the whole learning engine here would just re-prove what e2e-phase3b.js already
// proves — this drill is about DATA DURABILITY, not re-testing scenario validation).
//
// Usage: PLATFORM_API_URL=http://localhost:PORT PLATFORM_DB_URL=postgres://... node seed-realistic-data.js
const { Pool } = require('../../platform/api/node_modules/pg');
const API = process.env.PLATFORM_API_URL || 'http://localhost:4390';
const DB_URL = process.env.PLATFORM_DB_URL;
if (!DB_URL) { console.error('Set PLATFORM_DB_URL'); process.exit(1); }
function makeSession() {
const jar = new Map();
function cookieHeader() { return [...jar].map(([k, v]) => `${k}=${v}`).join('; '); }
function absorb(res) {
const cookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [];
for (const c of cookies) {
const [pair] = c.split(';');
const eq = pair.indexOf('=');
if (eq === -1) continue;
jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
}
}
return {
async req(method, path, body) {
const headers = { 'Content-Type': 'application/json' };
if (cookieHeader()) headers.Cookie = cookieHeader();
if (!['GET', 'HEAD'].includes(method)) headers['X-CSRF-Token'] = jar.get('rn_csrf_i') || '';
const res = await fetch(`${API}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
absorb(res);
const text = await res.text();
let json; try { json = JSON.parse(text); } catch { json = text; }
if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(json)}`);
return json;
},
};
}
async function main() {
const owner = makeSession();
await owner.req('POST', '/api/auth/instructor/login', { email: '[email protected]', password: 'DrillOwnerPass!2026' });
console.log(' owner login ok');
const admin = await owner.req('POST', '/api/admin/instructors', {
email: '[email protected]', display_name: 'Drill Admin', password: 'DrillAdminPass!2026', role: 'admin',
});
console.log(' admin instructor created:', admin.instructor_id);
const cohort = await owner.req('POST', '/api/admin/cohorts', { cohort_id: 'drill-cohort-01', name: 'Restore Drill Cohort' });
console.log(' cohort created');
const students = [];
for (const name of ['Drill Alice', 'Drill Bob', 'Drill Carlos']) {
const s = await owner.req('POST', '/api/admin/students', { display_name: name, cohort_id: 'drill-cohort-01' });
students.push(s.student_id);
}
console.log(' 3 students rostered and enrolled:', students.join(', '));
const assessment = await owner.req('POST', '/api/admin/assessments', {
id: 'drill-exam-01', type: 'exam', title: 'Restore Drill Exam', duration_minutes: 30, hint_policy: 'disabled',
pool_spec: { required: ['pod-basics-mcq'], random: [] },
});
console.log(' assessment definition created:', assessment.assessment_definition_id || 'drill-exam-01');
// Direct SQL for progress/attempts/achievements — see file header for why.
const pool = new Pool({ connectionString: DB_URL });
for (const studentId of students) {
// environment_id is a real FK to the Provisioner's environments table (nullable) — left
// NULL here rather than faking a row that doesn't exist, same as how a genuinely old
// attempt whose environment has long since been cleaned up would look in real data.
await pool.query(
`INSERT INTO scenario_attempts (student_id, scenario_id, status, result, started_at, completed_at, validation_attempt_count, validation_pass_count)
VALUES ($1, 'pod-basics-mcq', 'completed', 'passed', now() - interval '1 hour', now(), 1, 1)`,
[studentId]
);
await pool.query(
`INSERT INTO achievements (student_id, achievement_type, stage_id, achieved_at) VALUES ($1, 'stage_completed', 'foundation', now())`,
[studentId]
);
}
await pool.end();
console.log(' progress/attempts/achievements seeded for all 3 students');
console.log('\nSeed complete.');
}
main().catch((e) => { console.error('SEED FAILED:', e.message); process.exit(1); });