- test-phase6f-root-v1-pilot.js (22/22): a real cohort + live classroom session with three students in genuinely different states — on track, a real Lab validation failure/help- request/fix cycle, and a real admin Lab-reap/recreate with academic-state persistence proven, not assumed. Instructor visibility distinguishes all three. - test-phase6f-root-v1-end-to-end.js (14/14): one student, entirely through the real student API, ROOT 01 through Capstone-eligible, including a real nested-Docker container-networking fix and an explicit proof that the real rootnode-capstone assessment is genuinely startable at the end — not just that the course tree says complete. Found and documented (not fixed) a real, pre-existing gap: the legacy six-stage Capstone unlock_rule requires two scenario sets (root-07-m01/m02) that are not course-layer prerequisites of anything on the path to root-capstone. Co-Authored-By: Claude Sonnet 5 <[email protected]>
189 lines
16 KiB
JavaScript
189 lines
16 KiB
JavaScript
#!/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); });
|