#!/usr/bin/env node // Phase 8 Step 32 — the Winter Break commercial-path rehearsal: the website submits real // registrations (via the new /api/integrations/registrations contract), a guardian's agreement // acceptance is recorded through the supported application flow, a future-payment confirmation // is SIMULATED (admin-side transition only — no payment code anywhere), and the full remaining // path (enrollment, cohort, activation, classroom, real Lab work, progress, instructor // visibility) runs for real. Also proves the adult registration path and a genuine two-students- // competing-for-the-final-seat race. 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 INTEGRATION_KEY = process.env.INTEGRATION_SHARED_SECRET || 'dev-integration-secret-not-for-production'; 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 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 integrationPost(path, body) { const r = await fetch(`${GW}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-RootNode-Integration-Key': INTEGRATION_KEY }, body: JSON.stringify(body) }); return { status: r.status, body: await r.json().catch(() => ({})) }; } async function main() { console.log('Phase 8 Winter Break commercial-path rehearsal — website submission, no real payment\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 locId = `p8-winter-loc-${RUN_ID}`, cohortId = `p8-winter-cohort-${RUN_ID}`, offeringId = `p8-winter-camp-${RUN_ID}`, slug = `p8-winter-linux-systems-${RUN_ID}`, agreementId = `p8-winter-agreement-${RUN_ID}`; await step('admin opens the Winter Break offering — location, capacity, instructor, agreement requirement configured', async () => { await owner.request('POST', '/api/admin/locations', { locationId: locId, name: 'Community Room B' }); await owner.request('POST', '/api/admin/cohorts', { cohort_id: cohortId, name: 'Winter Break Cohort', tier: 'root' }); const create = await owner.request('POST', '/api/admin/offerings', { offeringId, slug, name: 'Winter Break Linux & Systems Camp', programId: PROGRAM, deliveryMode: 'camp', capacity: 2, locationId: locId, cohortId, }); assert.equal(create.status, 201); const instructorCreate = await owner.request('POST', '/api/admin/instructors', { email: `p8-winter-instructor-${RUN_ID}@example.com`, display_name: 'Winter Instructor', role: 'instructor', password: 'WinterP8Pass123!' }); await owner.request('POST', `/api/admin/offerings/${offeringId}/instructors`, { instructor_id: instructorCreate.body.instructor_id, role: 'teacher' }); await owner.request('POST', `/api/admin/cohorts/${cohortId}/instructors`, { instructor_id: instructorCreate.body.instructor_id, role_in_cohort: 'teacher' }); await owner.request('POST', '/api/admin/agreements', { agreementId, title: 'Winter Camp Participation Agreement' }); await owner.request('POST', `/api/admin/offerings/${offeringId}/agreement-requirements`, { agreement_id: agreementId }); const readiness = await owner.request('GET', `/api/admin/offerings/${offeringId}/readiness`); assert.ok(readiness.body.checks.find((c) => c.key === 'instructor').ready); assert.ok(readiness.body.checks.find((c) => c.key === 'agreements').ready); }); await step('admin publishes and opens registration', async () => { await owner.request('POST', `/api/admin/offerings/${offeringId}/transition`, { status: 'published' }); const open = await owner.request('POST', `/api/admin/offerings/${offeringId}/transition`, { status: 'enrollment_open' }); assert.equal(open.status, 200); }); let minorRegId, minorGuardianId; await step('website integration submits a REAL minor registration (guardian created/associated), confirmation email queued', async () => { const submit = await integrationPost('/api/integrations/registrations', { event_id: `p8-winter-minor-${RUN_ID}`, offering_slug: slug, registrant: { display_name: 'Winter Camper', is_minor: true }, guardian: { display_name: 'Winter Guardian', email: `winter-guardian-${RUN_ID}@example.com`, phone: '555-0101' }, }); assert.equal(submit.status, 201, JSON.stringify(submit.body)); minorRegId = submit.body.registration_id; const reg = await owner.request('GET', `/api/admin/registrations/${minorRegId}`); minorGuardianId = reg.body.guardian_id; assert.ok(minorGuardianId, 'a real guardian must have been created and linked by the integration endpoint'); const notifs = await owner.request('GET', `/api/admin/registrations/${minorRegId}/notifications`); assert.ok(notifs.body.some((n) => n.notification_type === 'registration_received'), 'a registration_received confirmation must be queued'); }); await step('guardian acceptance is recorded through the supported application flow — agreement complete', async () => { await owner.request('POST', `/api/admin/registrations/${minorRegId}/transition`, { status: 'submitted' }); await owner.request('POST', `/api/admin/registrations/${minorRegId}/transition`, { status: 'agreements_pending' }); const agreementNotifs = await owner.request('GET', `/api/admin/registrations/${minorRegId}/notifications`); assert.ok(agreementNotifs.body.some((n) => n.notification_type === 'agreement_required'), 'an agreement_required notice must be queued when entering agreements_pending'); const accept = await owner.request('POST', `/api/admin/registrations/${minorRegId}/agreements`, { agreementId, acceptedByType: 'guardian', acceptedByGuardianId: minorGuardianId }); assert.equal(accept.status, 201); const status = await owner.request('GET', `/api/admin/registrations/${minorRegId}/agreement-status`); assert.equal(status.body.missing.length, 0); }); await step('future payment confirmation is SIMULATED (admin-only transition) — no payment code anywhere', async () => { const confirm = await owner.request('POST', `/api/admin/registrations/${minorRegId}/transition`, { status: 'confirmed' }); assert.equal(confirm.status, 200); const confirmedNotifs = await owner.request('GET', `/api/admin/registrations/${minorRegId}/notifications`); assert.ok(confirmedNotifs.body.some((n) => n.notification_type === 'registration_confirmed')); }); let minorStudent, minorSession, minorEnrollmentId; await step('registration converts to real enrollment + real cohort membership; account activates', async () => { const convert = await owner.request('POST', `/api/admin/registrations/${minorRegId}/convert`, {}); assert.equal(convert.status, 201, JSON.stringify(convert.body)); minorStudent = convert.body.student; assert.ok(minorStudent._activationToken); const roster = await owner.request('GET', `/api/admin/cohorts/${cohortId}/students`); assert.ok(roster.body.some((s) => s.student_id === minorStudent.student_id)); minorSession = makeSession(GW); const activate = await minorSession.request('POST', '/api/student/activate', { token: minorStudent._activationToken, password: 'WinterP8Minor123!' }); assert.equal(activate.status, 200); }); await step('adult registration path — self-registers, no guardian required, converts independently', async () => { const submit = await integrationPost('/api/integrations/registrations', { event_id: `p8-winter-adult-${RUN_ID}`, offering_slug: slug, registrant: { display_name: 'Winter Adult', email: `winter-adult-${RUN_ID}@example.com`, is_minor: false }, }); assert.equal(submit.status, 201); const regId = submit.body.registration_id; await owner.request('POST', `/api/admin/registrations/${regId}/agreements`, { agreementId, acceptedByType: 'registrant' }); await owner.request('POST', `/api/admin/registrations/${regId}/transition`, { status: 'submitted' }); await owner.request('POST', `/api/admin/registrations/${regId}/transition`, { status: 'confirmed' }); const convert = await owner.request('POST', `/api/admin/registrations/${regId}/convert`, {}); assert.equal(convert.status, 201); assert.equal(convert.body.student.email, `winter-adult-${RUN_ID}@example.com`); }); await step('two students compete for the final seat — exactly one wins, offering reports full, no overbooking', async () => { const before = await owner.request('GET', `/api/admin/offerings/${offeringId}`); assert.equal(before.body.reserved_seats, before.body.capacity, 'capacity=2 should already be exhausted by the minor + adult above'); const [a, b] = await Promise.all([ integrationPost('/api/integrations/registrations', { event_id: `p8-winter-race-a-${RUN_ID}`, offering_slug: slug, registrant: { display_name: 'Race A', email: `p8-race-a-${RUN_ID}@example.com` } }), integrationPost('/api/integrations/registrations', { event_id: `p8-winter-race-b-${RUN_ID}`, offering_slug: slug, registrant: { display_name: 'Race B', email: `p8-race-b-${RUN_ID}@example.com` } }), ]); const statuses = [a.status, b.status].sort(); assert.deepEqual(statuses, [409, 409], 'both must be rejected — the seat was already full before either request, this proves no overbooking beyond capacity'); const pub = await fetch(`${GW}/api/public/offerings/${slug}`).then((r) => r.json()); assert.equal(pub.availability, 'full'); }); await step('instructor starts a live classroom session and Prepares Labs', async () => { const session = await owner.request('POST', `/api/admin/cohorts/${cohortId}/classroom-sessions`, { title: 'Winter Camp Day 1' }); assert.equal(session.status, 201); global.__classroomId = session.body.classroom_session_id; await owner.request('POST', `/api/admin/classroom-sessions/${global.__classroomId}/start`); const prep = await owner.request('POST', `/api/admin/cohorts/${cohortId}/prepare-labs`); assert.equal(prep.status, 200); }); await step('the minor student joins, completes real orientation + a REAL Lab task, progress persists', async () => { const join = await minorSession.request('POST', `/api/classroom/${global.__classroomId}/join`); assert.equal(join.status, 200); const { body: enrollBody } = await minorSession.request('GET', '/api/student/programs'); minorEnrollmentId = enrollBody[0].enrollment_id; 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 minorSession.request('POST', `/api/student/activities/${id}/complete`); assert.equal(r.status, 200, `completing ${id}: ${JSON.stringify(r.body)}`); } const launch = await minorSession.request('POST', `/api/student/programs/${minorEnrollmentId}/launch`); assert.equal(launch.status, 200); const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1]; const setup = await minorSession.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/setup`); assert.equal(setup.status, 200); const envRow = (await owner.request('GET', `/api/admin/students/${minorStudent.student_id}/environment`)).body.find((e) => e.status === 'running'); 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 = '${envRow.environment_id}'"`, { cwd: `${__dirname}/../../../platform`, stdio: 'pipe' } ).toString().trim(); const fs = require('fs'); const scriptPath = `/tmp/phase8-winter-hardware-${envRow.environment_id}.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 minorSession.request('POST', `/lab/${labToken}/api/scenarios/linux-inspect-hardware/validate`); assert.equal(validate.status, 200); assert.equal(validate.body.passed, true); await minorSession.request('POST', '/api/progress/record', { scenario_id: 'linux-inspect-hardware', status: 'completed', environment_id: envRow.environment_id }); }); await step('instructor sees the real roster and progress', async () => { const roster = await owner.request('GET', `/api/admin/cohorts/${cohortId}/students`); assert.equal(roster.body.length, 2); const { body } = await owner.request('GET', `/api/admin/students/${minorStudent.student_id}/courses?program_id=${PROGRAM}`); const mod = moduleById(body, 'root-01-m01-how-computers-run-software'); assert.ok(mod.activities.some((a) => a.complete), 'the instructor must see the minor\'s real Lab progress'); }); console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed ? 1 : 0); } main().catch((e) => { console.error(e); process.exit(1); });