#!/usr/bin/env node // Phase 6F Step 23 — multi-student ROOT_ V1 pilot rehearsal. Three students in genuinely // different states inside one real instructor-led cohort/classroom session: Student A stays // on track, Student B hits a REAL validation failure and needs help before fixing it, Student C // has their real disposable Lab reaped (admin-destroyed) mid-task and must recreate it. Verifies // instructor visibility distinguishes all three, no cross-student progress leakage, and no // academic-state loss from the reap. Real Labs used for Student B and C's own tasks; Student A's // path uses the same legitimate direct-scenario-completion pattern e2e-phase6e.js established // for already-proven ROOT 01 content (no bypass of server-side prerequisite enforcement). // // Usage: bash infra/scripts/drills/run-phase6f-root-v1-pilot.sh const assert = require('assert'); const { execSync } = require('child_process'); const { makeSession } = require('../../../platform/test/session-helper'); const GW = process.env.GATEWAY_URL || 'http://localhost:4200'; const RUN_ID = Date.now(); const PROGRAM = 'root-kubernetes-foundations'; let passed = 0, failed = 0; async function step(name, fn) { try { await fn(); console.log(` ok — ${name}`); passed++; } catch (e) { console.error(` FAIL — ${name}\n ${e.stack || e.message}`); failed++; } } function psql(sql) { return execSync(`docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "${sql}"`, { cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }).toString().trim(); } function moduleById(courses, id) { for (const c of courses) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; } async function main() { console.log('Phase 6F ROOT_ V1 multi-student pilot rehearsal — real cohort, real Labs\n'); const owner = makeSession(GW); await step('owner logs in', async () => { const r = await owner.request('POST', '/api/auth/instructor/login', { email: process.env.OWNER_EMAIL, password: process.env.OWNER_PASSWORD }); assert.equal(r.status, 200); }); const cohortId = `p6f-pilot-${RUN_ID}`; await step('instructor creates a cohort and a live classroom session', async () => { const r = await owner.request('POST', '/api/admin/cohorts', { cohort_id: cohortId, name: 'ROOT_ V1 Pilot Rehearsal', tier: 'root' }); assert.equal(r.status, 201); }); const students = {}; for (const tag of ['A', 'B', 'C']) { await step(`create and enroll Student ${tag}`, async () => { const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `Student${tag}`, cohort_id: cohortId, login_identifier: `p6f-${tag.toLowerCase()}-${RUN_ID}` }); const session = makeSession(GW); const act = await session.request('POST', '/api/student/activate', { token: create.activation_token, password: `Student${tag}Pass123!` }); assert.equal(act.status, 200); const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' }); students[tag] = { session, studentId: create.student_id, enrollmentId: enroll.enrollment_id }; }); } let classroomId; await step('instructor starts a live classroom session for the cohort', async () => { const r = await owner.request('POST', `/api/admin/cohorts/${cohortId}/classroom-sessions`, { title: 'ROOT_ V1 Pilot Session' }); assert.equal(r.status, 201); classroomId = r.body.classroom_session_id; await owner.request('POST', `/api/admin/classroom-sessions/${classroomId}/start`); }); for (const tag of ['A', 'B', 'C']) { await step(`Student ${tag} joins the live classroom session`, async () => { const r = await students[tag].session.request('POST', `/api/classroom/${classroomId}/join`); assert.equal(r.status, 200); }); } // ── Student A: on track — real orientation + module 1 progression ────────────────────── await step('Student A completes the orientation module and starts ROOT 01 module 1 (on track)', async () => { for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check', 'root-01-m01-a01-lesson', 'root-01-m01-a02-guided-exercise']) { const r = await students.A.session.request('POST', `/api/student/activities/${id}/complete`); assert.equal(r.status, 200, id); } }); // ── Student B: needs help — a REAL Lab, a REAL failed validation, then a real fix ─────── let bEnvId, bLabToken; await step('Student B launches a REAL disposable Lab', async () => { for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) await students.B.session.request('POST', `/api/student/activities/${id}/complete`); const launch = await students.B.session.request('POST', `/api/student/programs/${students.B.enrollmentId}/launch`); assert.equal(launch.status, 200); bLabToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1]; const envRow = (await owner.request('GET', `/api/admin/students/${students.B.studentId}/environment`)).body.find((e) => e.status === 'running'); bEnvId = envRow.environment_id; }); await step('Student B attempts the linux-inspect-hardware Lab and genuinely FAILS validation first (nothing done yet)', async () => { const setup = await students.B.session.request('POST', `/lab/${bLabToken}/api/scenarios/linux-inspect-hardware/setup`); assert.equal(setup.status, 200); const validate = await students.B.session.request('POST', `/lab/${bLabToken}/api/scenarios/linux-inspect-hardware/validate`); assert.equal(validate.status, 200); assert.equal(validate.body.passed, false, 'expected a REAL failure — the report file was never written'); }); await step('Student B sends a real Need Help request, categorized lab_problem', async () => { const r = await students.B.session.request('POST', `/api/classroom/${classroomId}/help-request`, { category: 'lab_problem', message: 'My hardware report Lab keeps failing validation.' }); assert.equal(r.status, 201); }); await step('the instructor sees Student B\'s help request immediately', async () => { const r = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}/help-requests`); assert.equal(r.status, 200); const mine = r.body.find((h) => h.student_id === students.B.studentId); assert.ok(mine, 'instructor must see the real help request'); assert.equal(mine.category, 'lab_problem'); assert.equal(mine.status, 'open'); }); await step('Student B actually fixes it — real shell commands in the real Lab container — and validation now genuinely passes', async () => { const containerName = execSync( `docker compose -f docker-compose.dev.yml exec -T postgres psql -U postgres -d rootnode_platform -t -c "SELECT container_name FROM environments WHERE environment_id = '${bEnvId}'"`, { cwd: `${__dirname}/../../../platform`, stdio: 'pipe' } ).toString().trim(); const fs = require('fs'); const scriptPath = `/tmp/phase6f-hardware-report-${bEnvId}.sh`; fs.writeFileSync(scriptPath, ['#!/bin/bash', 'set -e', 'mkdir -p /srv/hardware-lab', 'CPUS=$(nproc)', "MEM_MB=$(free -m | awk '/Mem:/ {print $2}')", 'DISK_GB=$(df -BG / | awk \'NR==2{gsub("G","",$2); print $2}\')', 'printf "CPUS=%s\\nMEM_MB=%s\\nDISK_GB=%s\\n" "$CPUS" "$MEM_MB" "$DISK_GB" > /srv/hardware-lab/report.txt'].join('\n')); execSync(`docker cp ${scriptPath} ${containerName}:/tmp/hardware-report.sh && docker exec ${containerName} bash /tmp/hardware-report.sh`, { stdio: 'pipe' }); fs.unlinkSync(scriptPath); const validate = await students.B.session.request('POST', `/lab/${bLabToken}/api/scenarios/linux-inspect-hardware/validate`); assert.equal(validate.status, 200); assert.equal(validate.body.passed, true, 'expected the real fix to genuinely pass now'); }); await step('the instructor acknowledges and resolves Student B\'s help request', async () => { const list = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}/help-requests`); const mine = list.body.find((h) => h.student_id === students.B.studentId); const ack = await owner.request('POST', `/api/admin/help-requests/${mine.help_request_id}/acknowledge`); assert.equal(ack.status, 200); const resolve = await owner.request('POST', `/api/admin/help-requests/${mine.help_request_id}/resolve`); assert.equal(resolve.status, 200); }); // ── Student C: Lab reaped mid-task, must recreate — academic state must survive ───────── let cEnvId; await step('Student C launches a REAL disposable Lab and starts a real Lab task', async () => { for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) await students.C.session.request('POST', `/api/student/activities/${id}/complete`); const launch = await students.C.session.request('POST', `/api/student/programs/${students.C.enrollmentId}/launch`); assert.equal(launch.status, 200); const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1]; const setup = await students.C.session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`); assert.equal(setup.status, 200); const envRow = (await owner.request('GET', `/api/admin/students/${students.C.studentId}/environment`)).body.find((e) => e.status === 'running'); cEnvId = envRow.environment_id; }); await step('the instructor reaps Student C\'s Lab mid-task (real admin destroy, simulating an idle-reaper event)', async () => { const r = await owner.request('DELETE', `/api/admin/environments/${cEnvId}`); assert.equal(r.status, 200); }); await step('Student C\'s academic progress survived the reap (orientation still complete)', async () => { const { body } = await students.C.session.request('GET', `/api/student/programs/${students.C.enrollmentId}/courses`); assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true, 'orientation completion must survive a Lab reap'); }); await step('Student C recreates a fresh Lab and can pick the task back up', async () => { const launch = await students.C.session.request('POST', `/api/student/programs/${students.C.enrollmentId}/launch`); assert.equal(launch.status, 200); assert.equal(launch.body.status, 'running'); const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1]; const setup = await students.C.session.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`); assert.equal(setup.status, 200, 'the recreated Lab must accept the same task setup again'); }); // ── Instructor sees three genuinely different states ──────────────────────────────────── await step('the instructor sees three genuinely different student states in the classroom view', async () => { const { body } = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}`); const names = body.students.map((s) => s.display_name); assert.ok(names.includes('StudentA') && names.includes('StudentB') && names.includes('StudentC')); const helpHistory = await owner.request('GET', `/api/admin/classroom-sessions/${classroomId}/help-requests`); const bRequest = helpHistory.body.find((h) => h.student_id === students.B.studentId); assert.equal(bRequest.status, 'resolved', 'Student B\'s help request should show resolved, distinguishing them from A and C'); }); await step('cross-student isolation: Student A is genuinely ahead of B and C, and none of B/C\'s events touched A', async () => { const [{ body: aBody }, { body: bBody }, { body: cBody }] = await Promise.all([ students.A.session.request('GET', `/api/student/programs/${students.A.enrollmentId}/courses`), students.B.session.request('GET', `/api/student/programs/${students.B.enrollmentId}/courses`), students.C.session.request('GET', `/api/student/programs/${students.C.enrollmentId}/courses`), ]); assert.equal(moduleById(aBody.courses, 'root-01-m00-welcome-to-root').complete, true, 'Student A completed orientation'); const aLesson = moduleById(aBody.courses, 'root-01-m01-how-computers-run-software').activities.find((act) => act.activity_id === 'root-01-m01-a01-lesson'); assert.equal(aLesson.complete, true, 'Student A explicitly marked the module 1 lesson complete'); assert.equal(moduleById(bBody.courses, 'root-01-m00-welcome-to-root').complete, true, 'Student B also completed orientation, independently'); const bLesson = moduleById(bBody.courses, 'root-01-m01-how-computers-run-software').activities.find((act) => act.activity_id === 'root-01-m01-a01-lesson'); assert.equal(bLesson.complete, false, 'Student B never marked the lesson complete — only ran the real Lab task, isolated from A\'s manual-marker progress'); assert.equal(moduleById(cBody.courses, 'root-01-m00-welcome-to-root').complete, true, 'Student C also completed orientation, independently, surviving their own Lab reap'); }); console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed ? 1 : 0); } main().catch((e) => { console.error(e); process.exit(1); });