#!/usr/bin/env node // Phase 6F Step 24 — the broadest curriculum acceptance test yet: proves one real student can // progress ROOT 01 -> 02 -> 03 -> 04 -> 05+ -> Security -> Troubleshooting -> Real Ops -> // Capstone-eligible, entirely through the REAL student API (never an admin shortcut, never a // direct database write to fake completion), with every real server-side prerequisite lock // actually enforced along the way — not bypassed. Real disposable Lab + real nested Docker used // for one genuine checkpoint (the ROOT 04 container-networking diagnose-and-fix, matching the // standard Phase 6D/6E rehearsals already established); the rest of ROOT 01-04's real-Lab // mechanics were already proven end-to-end in those rehearsals and in this phase's own // multi-student pilot rehearsal, so this script does not repeat them — it proves the ADDITIONAL // distance (05 through Capstone-eligibility) that no prior phase's rehearsal covered. const assert = require('assert'); const { execSync } = require('child_process'); const { makeSession } = require('../../../platform/test/session-helper'); const { completeRootFoundations01Through04 } = require('../../../platform/test/foundations-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 courseById(courses, id) { return courses.find((c) => c.course_id === id); } 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 act(session, id) { const r = await session.request('POST', `/api/student/activities/${id}/complete`); assert.equal(r.status, 200, `completing ${id}: ${JSON.stringify(r.body)}`); } async function scen(session, sid) { const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); assert.equal(r.status, 200, `completing scenario ${sid}`); } async function main() { console.log('Phase 6F ROOT_ V1 end-to-end acceptance rehearsal — ROOT 01 through Capstone eligibility\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 { body: create } = await owner.request('POST', '/api/admin/students', { display_name: 'E2E V1 Journey', login_identifier: `p6f-journey-${RUN_ID}` }); const session = makeSession(GW); let enrollmentId; await step('student activates and is enrolled in ROOT_', async () => { const act1 = await session.request('POST', '/api/student/activate', { token: create.activation_token, password: 'JourneyPass123!' }); assert.equal(act1.status, 200); const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' }); enrollmentId = enroll.enrollment_id; }); await step('ROOT 01-04 (Linux/Git/Docker foundations) complete through the real student API, unlocking ROOT 05', async () => { await completeRootFoundations01Through04(session); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-04-containers-and-docker').complete, true); assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false); }); // ── One real Lab checkpoint: the ROOT 04 diagnose-and-fix, re-proven live on THIS student ── await step('practical boundary check: a real nested-Docker container-networking fix, run live on this student\'s own Lab', async () => { const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`); assert.equal(launch.status, 200); const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1]; const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/docker-container-networking/setup`); assert.equal(setup.status, 200); const envRow = (await owner.request('GET', `/api/admin/students/${create.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(); // Confirm the REAL pre-fix failure before fixing it. let failedBefore = false; try { execSync(`docker exec ${containerName} curl -sf --max-time 3 http://localhost:8099`, { stdio: 'pipe' }); } catch (e) { failedBefore = true; } assert.equal(failedBefore, true, 'expected a REAL connection failure before the fix (container started without -p)'); execSync(`docker exec ${containerName} docker rm -f net-lab-web`, { stdio: 'pipe' }); execSync(`docker exec ${containerName} docker run -d --name net-lab-web -p 8099:80 nginx:alpine`, { stdio: 'pipe' }); const afterFix = execSync(`docker exec ${containerName} curl -sf --max-time 5 http://localhost:8099`, { stdio: 'pipe' }).toString(); assert.ok(afterFix.includes('nginx') || afterFix.length > 0, 'expected a real successful response after the real fix'); const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/docker-container-networking/validate`); assert.equal(validate.status, 200); assert.equal(validate.body.passed, true); await scen(session, 'docker-container-networking'); }); await step('direct API cannot skip ahead: ROOT 09/10/12/Capstone all remain locked until ROOT 05/06 are genuinely complete', async () => { const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); for (const id of ['root-09-kubernetes-security', 'root-10-kubernetes-troubleshooting-observability', 'root-12-real-ops', 'root-capstone']) { assert.equal(courseById(body.courses, id).locked, true, `${id} should still be locked`); } const bypass = await session.request('POST', '/api/student/activities/root-09-m01-a01-lesson-security-model/complete'); assert.equal(bypass.status, 403); assert.equal(bypass.body.error, 'locked'); }); await step('ROOT 05 (Kubernetes Foundations) completes and unlocks ROOT 06', async () => { await act(session, 'root-05-m00-a01-lesson-mental-model'); await act(session, 'root-05-m00-a02-demonstration-desired-state'); await act(session, 'root-05-m00-a03-knowledge-check'); await act(session, 'root-05-m00-a04-guided-exercise-namespaces'); await scen(session, 'namespaces-basics'); await act(session, 'root-05-m00-a06-reflection'); const pods = ['pod-basics-mcq', 'kubectl-essentials-mcq', 'labels-selectors-mcq', 'init-containers-mcq', 'namespaces-basics', 'multi-container-pod-basics', 'pod-labeling-basics', 'jsonpath-basics', 'dry-run-manifest-basics', 'delete-by-label-basics', 'annotate-resource-basics', 'init-container-task-basics', 'restart-policy-mcq', 'kubectl-output-format-mcq', 'kubectl-explain-basics', 'kubectl-patch-basics', 'field-selector-basics', 'sort-by-basics', 'kubectl-diff-basics', 'static-pods-mcq']; for (const sid of pods) await scen(session, sid); await act(session, 'root-05-m01-a00-lesson-kubectl-fluency'); await act(session, 'root-05-m01-a21-reflection'); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').complete, true); assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').locked, false); }); await step('ROOT 06 (Application Workloads, incl. the practice bank) completes and unlocks ROOT 07/09/10', async () => { for (const sid of ['deploy-nginx', 'scale-deployment', 'rolling-update-task', 'liveness-probe-task', 'jobs-cronjobs-mcq', 'daemonset-mcq', 'edit-deployment-basics', 'deployment-strategy-mcq', 'deployment-rollback']) await scen(session, sid); await act(session, 'root-06-m02-a01-lesson-deployments'); await act(session, 'root-06-m02-a02-demonstration-rollout'); await act(session, 'root-06-m02-a03-knowledge-check'); await act(session, 'root-06-m02-a04-guided-exercise-rollout-strategy'); await act(session, 'root-06-m02-a06-reflection'); await act(session, 'root-06-m01-a00-lab-bank-intro'); for (const sid of ['job-task', 'cronjob-task']) await scen(session, sid); await act(session, 'root-06-m03-a01-lesson-jobs-cronjobs-daemonsets'); await act(session, 'root-06-m03-a01b-demonstration-job-lifecycle'); await act(session, 'root-06-m03-a02-knowledge-check'); await act(session, 'root-06-m03-a02b-guided-exercise-choosing-primitive'); await act(session, 'root-06-m03-a05-reflection'); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').complete, true); assert.equal(courseById(body.courses, 'root-07-kubernetes-networking-config').locked, false); assert.equal(courseById(body.courses, 'root-09-kubernetes-security').locked, false); assert.equal(courseById(body.courses, 'root-10-kubernetes-troubleshooting-observability').locked, false); }); await step('ROOT 09 (Security) completes via a real formal assessment', async () => { for (const suffix of ['a01-lesson-security-model', 'a02-demonstration-securitycontext', 'a03-knowledge-check', 'a04-guided-exercise-hardening-plan', 'a09-reflection']) await act(session, `root-09-m01-${suffix}`); for (const sid of ['cks-non-root-enforce', 'cks-drop-capabilities', 'cks-readonly-filesystem', 'cks-pod-security-context']) await scen(session, sid); for (const suffix of ['a01-lesson-serviceaccounts-rbac', 'a01b-demonstration-rbac-denial', 'a02-knowledge-check', 'a02b-guided-exercise-least-privilege-role', 'a09-reflection']) await act(session, `root-09-m02-${suffix}`); for (const sid of ['serviceaccount-pod', 'cks-automount-token', 'rbac-role', 'cks-rbac-clusterrole', 'cks-rbac-least-privilege']) await scen(session, sid); for (const suffix of ['a01-lesson-services', 'a02-demonstration-zero-endpoints', 'a03-knowledge-check', 'a04-guided-exercise-service-type', 'a06-reflection']) await act(session, `root-07-m03-${suffix}`); await scen(session, 'expose-service-basics'); for (const suffix of ['a01-lesson-ingress-networkpolicy', 'a01b-demonstration-ingress-routing', 'a02-knowledge-check', 'a02b-guided-exercise-network-isolation', 'a05-reflection']) await act(session, `root-07-m04-${suffix}`); await scen(session, 'ingress-task'); await scen(session, 'network-policy'); for (const suffix of ['a01-lesson-network-supplychain', 'a01b-demonstration-default-deny', 'a02-knowledge-check', 'a02b-guided-exercise-egress-plan', 'a09-reflection']) await act(session, `root-09-m03-${suffix}`); for (const sid of ['cks-network-policy', 'cks-egress-namespace', 'cks-tls-ingress', 'cks-image-pull-secret']) await scen(session, sid); await act(session, 'root-09-m04-a01-lesson-cluster-hardening'); for (const sid of ['cks-psa-namespace', 'cks-immutable-secret']) await scen(session, sid); const r = await session.request('POST', '/api/assessments/rootnode-security-assessment/start'); assert.equal(r.status, 201); for (const s of r.body.scenarios) await scen(session, s.scenario_id); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-09-kubernetes-security').complete, true); }); await step('ROOT 10 (Troubleshooting, incl. the practice bank) completes using the RootNode framework', async () => { await act(session, 'root-10-m01-a01-lesson-health-check-concepts'); await act(session, 'root-10-m01-a02-demonstration-probes-in-action'); await act(session, 'root-10-m01-a03-knowledge-check'); await act(session, 'root-10-m01-a04-guided-exercise-readiness-probe'); await scen(session, 'rootnode-probe-port-mismatch'); await act(session, 'root-10-m01-a06-reflection-real-ops-takeaway'); await act(session, 'root-10-m03-a01-lesson-framework'); await act(session, 'root-10-m03-a02-demonstration-crashloop'); await act(session, 'root-10-m03-a03-knowledge-check'); await act(session, 'root-10-m03-a03b-guided-exercise-endpoint-mismatch'); await scen(session, 'crashloop-fix'); await scen(session, 'endpoint-fix-task'); await act(session, 'root-10-m03-a06-reflection'); await act(session, 'root-10-m02-a00-lab-bank-intro'); for (const sid of ['extract-logs-basics', 'kubectl-cp-basics', 'exec-command-basics', 'node-cordon-basics', 'pod-node-assignment-mcq', 'container-logging-mcq']) await scen(session, sid); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-10-kubernetes-troubleshooting-observability').complete, true); assert.equal(courseById(body.courses, 'root-12-real-ops').locked, false); }); await step('ROOT 12 (Real Ops) completes, real incident scenarios, deliberately un-guided', async () => { await act(session, 'root-12-m01-a00-lesson-incident-response'); for (const sid of ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-enforce-limits']) await scen(session, sid); await act(session, 'root-12-m01-a99-reflection-post-incident'); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-12-real-ops').complete, true); }); await step('root-capstone is now genuinely unlocked — both real prerequisites (ROOT 09 AND ROOT 12) satisfied', async () => { const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(courseById(body.courses, 'root-capstone').locked, false); }); await step('the Capstone Readiness orientation module is completable', async () => { for (const suffix of ['a01-lesson-what-it-measures', 'a02-lesson-time-management', 'a03-knowledge-check', 'a04-reflection-readiness']) await act(session, `root-capstone-m02-${suffix}`); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); assert.equal(moduleById(body.courses, 'root-capstone-m02-capstone-readiness').complete, true); }); await step('the pre-existing legacy six-stage gate (a SEPARATE, older mechanism) also needs its own "networking"/"configuration" stage scenarios — never required by this journey\'s course-layer path, since root-07-m01/m02 aren\'t prerequisites of root-09-m03', async () => { // "networking" stage needs services-mcq (root-07-m01's scenario; expose-service-basics was // already done above via root-07-m03). "configuration" stage needs all 5 of root-07-m02's // scenarios. Neither module is a course-content-model prerequisite of anything this journey // already needed to touch — proving these two, genuinely separate gates aren't the same // thing is exactly this step's point. for (const sid of ['services-mcq', 'configmap-volume', 'resource-limits-task', 'env-vars-configmap', 'secrets-basics', 'pod-env-vars-inline']) await scen(session, sid); }); await step('the real Capstone assessment itself is now startable (the pre-existing six-stage gate is also satisfied by this journey)', async () => { const r = await session.request('POST', '/api/assessments/rootnode-capstone/start'); assert.equal(r.status, 201, `expected the Capstone to be genuinely startable: ${JSON.stringify(r.body)}`); }); console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed ? 1 : 0); } main().catch((e) => { console.error(e); process.exit(1); });