Files
rootnode-academy/platform/test/foundations-helper.js
T
ced4568andClaude Sonnet 5 59edceea1f test(phase6e): acceptance suite, real-Lab rehearsal, and regression fixes for the lock fix
Adds platform/test/e2e-phase6e.js (23 checks): full ROOT 02/03/04 hierarchy/content/assessment-
pool verification, the complete ROOT 01->02->03->04->05 chain proven on one student (each
course's completion unlocking exactly the next), the direct-API lock bypass proven blocked on
both a Phase 6E course pair and a pre-existing unrelated pair, existing ROOT 06+/Capstone/
is_required behavior confirmed unaffected, self-paced no-instructor-deferral scan,
instructor-notes exclusion, RBAC, persistence, isolation, version stability, content validation.
23/23 pass against both a warm and a freshly-reset stack.

Adds platform/test/foundations-helper.js — a shared completeRootFoundations01Through04/
completeThroughRoot06/completeThroughRoot10 helper, extracted once multiple suites needed the
identical real-completion sequence as test setup, rather than duplicating it four times.

Adds infra/scripts/drills/{run,test}-phase6e-foundations-rehearsal — the real Platform API/
Gateway/Provisioner/disposable-Lab/nested-Docker/Git/k3s dress rehearsal: one real Lab task per
new course (nginx inspection, a real merge conflict, a real container-networking fix) plus the
full chain to a genuine ROOT 05 unlock, with k3s reconfirmed healthy and untouched at the end.
13/13 pass against a real, freshly-built stack.

Fixes the regression fallout from the (correct) course-routes.js lock fix: several earlier
suites' own historical shortcuts had accumulated small ordering/scenario gaps the missing
enforcement had been silently covering for — none were platform bugs. e2e-phase6a/6b/6c/6d.js
now inject the real ROOT 01-04 (and where needed ROOT 05/06/10) completion sequence via
foundations-helper.js before touching ROOT 05+ content; e2e-phase6b.js's bypass-student flow now
completes deployment-rollback/job-task/cronjob-task/container-logging-mcq explicitly (real
scenarios added in later phases that its original ALL_STAGES list never included); e2e-phase6a/
6d.js's own ROOT 01/05 completion tests now complete each module fully (through its lab and
reflection) before moving to the next, since those courses' own modules carry real intra-course
prerequisites. Two now-obsolete Phase-6D-era assertions in e2e-phase6d.js (ROOT 02-04 stay
PLANNED / stay vacuously unlocked) are updated to assert the opposite, with a comment explaining
Phase 6E intentionally changed that fact. All four suites re-verified green fresh.

Also fixes a real, reproducible dash-vs-bash bug caught while testing the new ROOT 01
rehearsal's ordering fix: `&>file` misparses under this image's actual /bin/sh (dash), breaking
a later `$!`-based kill — the rehearsal script now writes multi-line completion loops with
explicit status assertions, matching the discipline the other suites already use.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:48 -05:00

86 lines
10 KiB
JavaScript

// Shared test helper — completes the real ROOT 01-04 foundations sequence for a given session.
// Phase 6E gave ROOT 01-04 real content, so ROOT 05's own prerequisite (root-04) is no longer
// vacuously satisfied: any e2e suite that drives a student into ROOT 05+ content now needs this
// as real test setup first (not this file's own subject — see e2e-phase6e.js for the actual
// ROOT 01-04 acceptance coverage). Extracted here once other suites needed the identical
// sequence, rather than duplicating it in e2e-phase6a/6b/6c/6d.js.
async function completeRootFoundations01Through04(session) {
async function act(id) {
const r = await session.request('POST', `/api/student/activities/${id}/complete`);
if (r.status !== 200) throw new Error(`completing ${id} failed: ${r.status} ${JSON.stringify(r.body)}`);
}
async function scen(sid) {
const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' });
if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`);
}
await act('root-01-m01-a01-lesson'); await act('root-01-m01-a02-guided-exercise'); await scen('linux-inspect-hardware'); await act('root-01-m01-a04-reflection');
const l1 = { m02: 'linux-shell-basics', m03: 'linux-filesystem-navigation', m04: 'linux-files-and-text', m05: 'linux-process-management', m06: 'linux-permissions-foundations' };
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) { await act(`root-01-${m}-a01-lesson`); await act(`root-01-${m}-a02-demonstration`); await act(`root-01-${m}-a03-knowledge-check`); await act(`root-01-${m}-a04-guided-exercise`); await scen(l1[m]); await act(`root-01-${m}-a06-reflection`); }
let r = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start'); for (const s of r.body.scenarios) await scen(s.scenario_id);
await act('root-02-m01-a01-lesson'); await act('root-02-m01-a02-demonstration'); await act('root-02-m01-a03-knowledge-check'); await act('root-02-m01-a04-guided-exercise');
await scen('linux-service-inspection'); await scen('linux-service-lifecycle'); await act('root-02-m01-a07-reflection');
await act('root-02-m02-a01-lesson'); await act('root-02-m02-a02-demonstration'); await act('root-02-m02-a03-knowledge-check'); await act('root-02-m02-a04-guided-exercise'); await scen('linux-storage-inspection'); await act('root-02-m02-a06-reflection');
await act('root-02-m03-a01-lesson'); await act('root-02-m03-a02-demonstration'); await act('root-02-m03-a03-knowledge-check'); await act('root-02-m03-a04-guided-exercise'); await act('root-02-m03-a05-reflection');
await act('root-02-m04-a01-lesson'); await act('root-02-m04-a02-demonstration'); await act('root-02-m04-a03-knowledge-check'); await act('root-02-m04-a04-guided-exercise'); await scen('linux-network-inspection'); await act('root-02-m04-a06-reflection');
await act('root-02-m05-a01-lesson'); await act('root-02-m05-a02-demonstration'); await act('root-02-m05-a03-knowledge-check'); await act('root-02-m05-a04-guided-exercise'); await scen('linux-dns-http-diagnosis'); await act('root-02-m05-a06-reflection');
await act('root-02-m06-a01-lesson'); await act('root-02-m06-a02-demonstration'); await act('root-02-m06-a03-knowledge-check'); await act('root-02-m06-a04-guided-exercise'); await scen('linux-troubleshooting-framework'); await act('root-02-m06-a06-reflection');
r = await session.request('POST', '/api/assessments/rootnode-linux-networking-assessment/start'); for (const s of r.body.scenarios) await scen(s.scenario_id);
await act('root-03-m01-a01-lesson'); await act('root-03-m01-a02-knowledge-check'); await act('root-03-m01-a03-reflection');
await act('root-03-m02-a01-lesson'); await act('root-03-m02-a02-demonstration'); await act('root-03-m02-a03-knowledge-check'); await act('root-03-m02-a04-guided-exercise'); await act('root-03-m02-a05-reflection');
await act('root-03-m03-a01-lesson'); await act('root-03-m03-a02-demonstration'); await act('root-03-m03-a03-knowledge-check'); await act('root-03-m03-a04-guided-exercise'); await scen('git-everyday-basics'); await act('root-03-m03-a06-reflection');
await act('root-03-m04-a01-lesson'); await act('root-03-m04-a02-demonstration'); await act('root-03-m04-a03-knowledge-check'); await act('root-03-m04-a04-guided-exercise'); await scen('git-branching-and-merge'); await act('root-03-m04-a06-reflection');
await act('root-03-m05-a01-lesson'); await act('root-03-m05-a02-demonstration'); await act('root-03-m05-a03-knowledge-check'); await act('root-03-m05-a04-guided-exercise'); await scen('git-remote-clone-push'); await act('root-03-m05-a06-reflection');
await act('root-03-m06-a01-lesson'); await act('root-03-m06-a02-demonstration'); await act('root-03-m06-a03-knowledge-check'); await act('root-03-m06-a04-reflection');
r = await session.request('POST', '/api/assessments/rootnode-git-fundamentals-assessment/start'); for (const s of r.body.scenarios) await scen(s.scenario_id);
await act('root-04-m01-a01-lesson'); await act('root-04-m01-a02-demonstration'); await act('root-04-m01-a03-knowledge-check'); await act('root-04-m01-a04-reflection');
await act('root-04-m02-a01-lesson'); await act('root-04-m02-a02-demonstration'); await act('root-04-m02-a03-knowledge-check'); await act('root-04-m02-a04-guided-exercise'); await act('root-04-m02-a05-reflection');
const l4 = { m03: 'docker-cli-basics', m04: 'docker-container-networking', m05: 'docker-container-storage', m06: 'docker-build-image', m07: 'docker-compose-multiservice' };
for (const m of ['m03', 'm04', 'm05', 'm06', 'm07']) { await act(`root-04-${m}-a01-lesson`); await act(`root-04-${m}-a02-demonstration`); await act(`root-04-${m}-a03-knowledge-check`); await act(`root-04-${m}-a04-guided-exercise`); await scen(l4[m]); await act(`root-04-${m}-a06-reflection`); }
await act('root-04-m08-a01-lesson'); await act('root-04-m08-a02-knowledge-check'); await act('root-04-m08-a03-reflection');
r = await session.request('POST', '/api/assessments/rootnode-containers-assessment/start'); for (const s of r.body.scenarios) await scen(s.scenario_id);
}
// Completes ROOT 01-04, then ROOT 05 (m00 + m01), then ROOT 06 (m01/m02/m03) fully — the
// complete real prerequisite chain any suite needs before touching ROOT 09/10/11 content
// (their course-level prerequisite is root-06 complete, or a course chained behind it).
async function completeThroughRoot06(session) {
await completeRootFoundations01Through04(session);
async function act(id) { const r = await session.request('POST', `/api/student/activities/${id}/complete`); if (r.status !== 200) throw new Error(`completing ${id} failed: ${r.status} ${JSON.stringify(r.body)}`); }
async function scen(sid) { const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`); }
await act('root-05-m00-a01-lesson-mental-model'); await act('root-05-m00-a02-demonstration-desired-state'); await act('root-05-m00-a03-knowledge-check'); await act('root-05-m00-a04-guided-exercise-namespaces');
await scen('namespaces-basics'); await act('root-05-m00-a06-reflection');
const root05m01Scenarios = ['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 root05m01Scenarios) await scen(sid);
await act('root-05-m01-a00-lesson-kubectl-fluency'); await act('root-05-m01-a21-reflection');
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(sid);
await act('root-06-m02-a01-lesson-deployments'); await act('root-06-m02-a02-demonstration-rollout'); await act('root-06-m02-a03-knowledge-check'); await act('root-06-m02-a04-guided-exercise-rollout-strategy'); await act('root-06-m02-a06-reflection');
for (const sid of ['job-task', 'cronjob-task']) await scen(sid);
await act('root-06-m03-a01-lesson-jobs-cronjobs-daemonsets'); await act('root-06-m03-a01b-demonstration-job-lifecycle'); await act('root-06-m03-a02-knowledge-check'); await act('root-06-m03-a02b-guided-exercise-choosing-primitive'); await act('root-06-m03-a05-reflection');
}
// Completes through ROOT 06, then ROOT 10 (m01/m02/m03) fully — the real prerequisite ROOT 11
// (and anything else chained behind ROOT 10) needs.
async function completeThroughRoot10(session) {
await completeThroughRoot06(session);
async function act(id) { const r = await session.request('POST', `/api/student/activities/${id}/complete`); if (r.status !== 200) throw new Error(`completing ${id} failed: ${r.status} ${JSON.stringify(r.body)}`); }
async function scen(sid) { const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`); }
await act('root-10-m01-a01-lesson-health-check-concepts'); await act('root-10-m01-a02-demonstration-probes-in-action'); await act('root-10-m01-a03-knowledge-check'); await act('root-10-m01-a04-guided-exercise-readiness-probe');
await scen('rootnode-probe-port-mismatch'); await act('root-10-m01-a06-reflection-real-ops-takeaway');
await act('root-10-m03-a01-lesson-framework'); await act('root-10-m03-a02-demonstration-crashloop'); await act('root-10-m03-a03-knowledge-check'); await act('root-10-m03-a03b-guided-exercise-endpoint-mismatch');
await scen('crashloop-fix'); await scen('endpoint-fix-task'); await act('root-10-m03-a06-reflection');
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(sid);
}
module.exports = { completeRootFoundations01Through04, completeThroughRoot06, completeThroughRoot10 };