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]>
This commit is contained in:
2026-08-26 17:08:48 -05:00
co-authored by Claude Sonnet 5
parent a09b7aaa53
commit 59edceea1f
9 changed files with 922 additions and 27 deletions
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Orchestrates the Phase 6E §24 ROOT Foundations Completion dress rehearsal: brings up the real
# dev stack, bootstraps an owner, runs test-phase6e-foundations-rehearsal.js (real disposable
# Lab, real nested Docker, real Git, real k3s left untouched), tears down.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../../../platform"
export PROVISIONER_SHARED_SECRET="${PROVISIONER_SHARED_SECRET:-rehearsal-shared-secret-not-for-production}"
export INTEGRATION_SHARED_SECRET="${INTEGRATION_SHARED_SECRET:-dev-integration-secret-not-for-production}"
export LAB_IMAGE_REF="${LAB_IMAGE_REF:-rootnode-academy:test}"
docker compose -f docker-compose.dev.yml up -d --build
cleanup() {
echo "==> Tearing down"
docker ps -aq --filter "name=rootnode-lab-" | xargs -r docker rm -f >/dev/null 2>&1 || true
docker network ls --filter "name=rootnode-lab-net-" --format "{{.Name}}" | xargs -r -n1 docker network rm >/dev/null 2>&1 || true
docker compose -f docker-compose.dev.yml down >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "==> Waiting for the stack"
for _ in $(seq 1 60); do
curl -sf http://localhost:4300/healthz >/dev/null 2>&1 && curl -sf http://localhost:4200/healthz >/dev/null 2>&1 && break
sleep 2
done
echo "==> Bootstrapping the rehearsal owner"
( cd api && PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
node bootstrap-owner.js [email protected] "P6E Rehearsal Owner" 'RehearsalPass!2026' )
echo "==> Running the rehearsal"
OWNER_EMAIL=[email protected] OWNER_PASSWORD='RehearsalPass!2026' \
node ../infra/scripts/drills/test-phase6e-foundations-rehearsal.js
@@ -150,16 +150,20 @@ async function main() {
await step('completing the remaining ROOT 01 activities finishes the course and unlocks ROOT 02', async () => {
await session.request('POST', '/api/student/activities/root-01-m01-a04-reflection/complete');
// ROOT 01's own modules carry real intra-course prerequisites (m02 requires m01 complete,
// etc., found and enforced this phase — see docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md) —
// each module must complete fully, including its lab and reflection, before the next
// module's activities will accept a manual-marker completion.
const labs = { 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']) {
for (const suffix of ['a01-lesson', 'a02-demonstration', 'a03-knowledge-check', 'a04-guided-exercise']) {
await session.request('POST', `/api/student/activities/root-01-${m}-${suffix}/complete`);
const r = await session.request('POST', `/api/student/activities/root-01-${m}-${suffix}/complete`);
assert.equal(r.status, 200, `completing root-01-${m}-${suffix} should 200: ${JSON.stringify(r.body)}`);
}
await session.request('POST', '/api/progress/record', { scenario_id: labs[m], status: 'completed', environment_id: envId });
const r = await session.request('POST', `/api/student/activities/root-01-${m}-a06-reflection/complete`);
assert.equal(r.status, 200, `completing root-01-${m}-a06-reflection should 200: ${JSON.stringify(r.body)}`);
}
for (const [sid, activity] of [['linux-shell-basics', 'root-01-m02-a06-reflection'], ['linux-filesystem-navigation', 'root-01-m03-a06-reflection'], ['linux-files-and-text', 'root-01-m04-a06-reflection'], ['linux-process-management', 'root-01-m05-a06-reflection']]) {
await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed', environment_id: envId });
await session.request('POST', `/api/student/activities/${activity}/complete`);
}
await session.request('POST', '/api/student/activities/root-01-m06-a06-reflection/complete');
const start = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
assert.equal(start.status, 201);
@@ -0,0 +1,275 @@
#!/usr/bin/env node
// Phase 6E §24 — the ROOT Foundations Completion dress rehearsal. One continuous realistic
// student narrative through ROOT 02, ROOT 03, and ROOT 04, using the REAL Platform API, REAL
// Gateway, REAL Provisioner, REAL disposable Lab, REAL shell, REAL Git, and REAL nested Docker
// (built and tested in Phase 6D) end to end — no mocking of the important path. Exercises one
// genuinely representative real Lab task per course (service inspection, a real merge conflict,
// and a real container networking fix) rather than re-running every single lab for real — the
// non-real-Lab e2e-phase6e.js suite already proves full completion logic exhaustively; this
// script's job is proving the underlying real mechanisms work end to end, matching the
// established convention from test-phase6c/6d-*-rehearsal.js.
//
// Run via run-phase6e-foundations-rehearsal.sh, which brings up platform/docker-compose.dev.yml
// first.
const assert = require('assert');
const fs = require('fs');
const { execSync } = require('child_process');
const { makeSession } = require('../../../platform/test/session-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
if (!OWNER_EMAIL || !OWNER_PASSWORD) { console.error('Set OWNER_EMAIL and OWNER_PASSWORD'); process.exit(1); }
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 moduleFrom(tree, id) { for (const c of tree) { const m = c.modules.find((x) => x.module_id === id); if (m) return m; } return null; }
function courseFrom(tree, id) { return tree.find((c) => c.course_id === id); }
async function completeActivity(session, id) {
const r = await session.request('POST', `/api/student/activities/${id}/complete`);
assert.equal(r.status, 200, `completing ${id} should 200: ${JSON.stringify(r.body)}`);
}
async function completeScenario(session, sid, envId) {
const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed', environment_id: envId });
assert.equal(r.status, 200, `completing scenario ${sid} should 200`);
}
function runInContainer(containerName, script) {
const scriptPath = `/tmp/phase6e-rehearsal-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`;
fs.writeFileSync(scriptPath, `#!/bin/bash\nset -e\n${script}\n`);
const out = execSync(`docker cp ${scriptPath} ${containerName}:/tmp/rehearsal-step.sh && docker exec ${containerName} bash /tmp/rehearsal-step.sh`, { encoding: 'utf8' });
fs.unlinkSync(scriptPath);
return out;
}
async function main() {
console.log('Phase 6E ROOT Foundations Completion dress rehearsal — real Lab, real Docker, real Git\n');
const owner = makeSession(GW);
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
const { body: created } = await owner.request('POST', '/api/admin/students', { display_name: 'Rehearsal 6E Student', login_identifier: `p6e-foundations-${RUN_ID}` });
const session = makeSession(GW);
let enrollmentId, envId, containerName;
await step('student activates credentials, logs in, enrolls', async () => {
const act = await session.request('POST', '/api/student/activate', { token: created.activation_token, password: 'RehearsalPass123!' });
assert.equal(act.status, 200);
const enroll = await owner.request('POST', '/api/admin/enrollments', { student_id: created.student_id, program_id: PROGRAM, status: 'active' });
assert.equal(enroll.status, 201);
enrollmentId = enroll.body.enrollment_id;
});
await step('student launches a REAL disposable Lab', async () => {
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
assert.equal(launch.status, 200);
assert.equal(launch.body.status, 'running');
const envRow = (await owner.request('GET', `/api/admin/students/${created.student_id}/environment`)).body.find((e) => e.status === 'running');
envId = envRow.environment_id;
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 = '${envId}'"`,
{ cwd: `${__dirname}/../../../platform`, stdio: 'pipe' }
).toString().trim();
});
// ── ROOT 01 (already READY since Phase 6D) — complete quickly via real scenario endpoint ──
await step('ROOT 01 completes via the real Lab scenario-validation API', async () => {
for (const suffix of ['a01-lesson', 'a02-guided-exercise']) await completeActivity(session, `root-01-m01-${suffix}`);
await completeScenario(session, 'linux-inspect-hardware', envId);
await completeActivity(session, 'root-01-m01-a04-reflection');
const labs = { 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']) {
for (const suffix of ['a01-lesson', 'a02-demonstration', 'a03-knowledge-check', 'a04-guided-exercise']) await completeActivity(session, `root-01-${m}-${suffix}`);
await completeScenario(session, labs[m], envId);
await completeActivity(session, `root-01-${m}-a06-reflection`);
}
const start = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
for (const s of start.body.scenarios) await completeScenario(session, s.scenario_id, envId);
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-01-computing-linux-foundations').complete, true);
assert.equal(courseFrom(body.courses, 'root-02-linux-administration-networking').locked, false);
});
// ── ROOT 02 — one REAL Lab task: inspect the real nginx service, real ss/curl commands ────
await step('student solves the real linux-service-inspection Lab using real shell commands', async () => {
await completeActivity(session, 'root-02-m01-a01-lesson');
await completeActivity(session, 'root-02-m01-a02-demonstration');
await completeActivity(session, 'root-02-m01-a03-knowledge-check');
await completeActivity(session, 'root-02-m01-a04-guided-exercise');
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-service-inspection/setup`);
assert.equal(setup.status, 200);
runInContainer(containerName, `
mkdir -p /srv/service-lab
printf "LISTENING_PORT=80\\nCONFIG_PATH=/etc/nginx/nginx.conf\\nLOG_DIR=/var/log/nginx\\n" > /srv/service-lab/report.txt`);
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/linux-service-inspection/validate`);
assert.equal(validate.status, 200);
assert.equal(validate.body.passed, true, `expected real validation to pass: ${JSON.stringify(validate.body)}`);
await completeScenario(session, 'linux-service-inspection', envId);
await completeScenario(session, 'linux-service-lifecycle', envId);
await completeActivity(session, 'root-02-m01-a07-reflection');
});
await step('completing the rest of ROOT 02 (shortcut) finishes the course and unlocks ROOT 03', async () => {
await completeActivity(session, 'root-02-m02-a01-lesson'); await completeActivity(session, 'root-02-m02-a02-demonstration');
await completeActivity(session, 'root-02-m02-a03-knowledge-check'); await completeActivity(session, 'root-02-m02-a04-guided-exercise');
await completeScenario(session, 'linux-storage-inspection', envId); await completeActivity(session, 'root-02-m02-a06-reflection');
await completeActivity(session, 'root-02-m03-a01-lesson'); await completeActivity(session, 'root-02-m03-a02-demonstration');
await completeActivity(session, 'root-02-m03-a03-knowledge-check'); await completeActivity(session, 'root-02-m03-a04-guided-exercise');
await completeActivity(session, 'root-02-m03-a05-reflection');
await completeActivity(session, 'root-02-m04-a01-lesson'); await completeActivity(session, 'root-02-m04-a02-demonstration');
await completeActivity(session, 'root-02-m04-a03-knowledge-check'); await completeActivity(session, 'root-02-m04-a04-guided-exercise');
await completeScenario(session, 'linux-network-inspection', envId); await completeActivity(session, 'root-02-m04-a06-reflection');
await completeActivity(session, 'root-02-m05-a01-lesson'); await completeActivity(session, 'root-02-m05-a02-demonstration');
await completeActivity(session, 'root-02-m05-a03-knowledge-check'); await completeActivity(session, 'root-02-m05-a04-guided-exercise');
await completeScenario(session, 'linux-dns-http-diagnosis', envId); await completeActivity(session, 'root-02-m05-a06-reflection');
await completeActivity(session, 'root-02-m06-a01-lesson'); await completeActivity(session, 'root-02-m06-a02-demonstration');
await completeActivity(session, 'root-02-m06-a03-knowledge-check'); await completeActivity(session, 'root-02-m06-a04-guided-exercise');
await completeScenario(session, 'linux-troubleshooting-framework', envId); await completeActivity(session, 'root-02-m06-a06-reflection');
const start = await session.request('POST', '/api/assessments/rootnode-linux-networking-assessment/start');
for (const s of start.body.scenarios) await completeScenario(session, s.scenario_id, envId);
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-02-linux-administration-networking').complete, true);
assert.equal(courseFrom(body.courses, 'root-03-git-collaborative-development').locked, false);
});
// ── ROOT 03 — one REAL Lab task: a real merge conflict, resolved with real git commands ───
await step('student resolves a REAL merge conflict using real git commands', async () => {
await completeActivity(session, 'root-03-m01-a01-lesson'); await completeActivity(session, 'root-03-m01-a02-knowledge-check'); await completeActivity(session, 'root-03-m01-a03-reflection');
await completeActivity(session, 'root-03-m02-a01-lesson'); await completeActivity(session, 'root-03-m02-a02-demonstration');
await completeActivity(session, 'root-03-m02-a03-knowledge-check'); await completeActivity(session, 'root-03-m02-a04-guided-exercise'); await completeActivity(session, 'root-03-m02-a05-reflection');
await completeActivity(session, 'root-03-m03-a01-lesson'); await completeActivity(session, 'root-03-m03-a02-demonstration');
await completeActivity(session, 'root-03-m03-a03-knowledge-check'); await completeActivity(session, 'root-03-m03-a04-guided-exercise');
await completeScenario(session, 'git-everyday-basics', envId); await completeActivity(session, 'root-03-m03-a06-reflection');
await completeActivity(session, 'root-03-m04-a01-lesson'); await completeActivity(session, 'root-03-m04-a02-demonstration');
await completeActivity(session, 'root-03-m04-a03-knowledge-check'); await completeActivity(session, 'root-03-m04-a04-guided-exercise');
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
const labToken = launch.body.launch_path.match(/\/lab\/([^/]+)\//)[1];
const setup = await session.request('POST', `/lab/${labToken}/api/scenarios/git-branching-and-merge/setup`);
assert.equal(setup.status, 200);
// Confirm the REAL conflict exists before resolving it.
const conflictCheck = runInContainer(containerName, `grep -c '<<<<<<<' /srv/git-lab-branch/shared.txt || true`);
assert.ok(conflictCheck.trim() !== '0', 'expected a real, unresolved merge conflict to exist before the fix');
runInContainer(containerName, `
cd /srv/git-lab-branch
echo "resolved version" > shared.txt
git add shared.txt
git commit -q -m "Merge feature, resolve conflict"`);
const validate = await session.request('POST', `/lab/${labToken}/api/scenarios/git-branching-and-merge/validate`);
assert.equal(validate.status, 200);
assert.equal(validate.body.passed, true, `expected real validation to pass: ${JSON.stringify(validate.body)}`);
await completeScenario(session, 'git-branching-and-merge', envId);
await completeActivity(session, 'root-03-m04-a06-reflection');
});
await step('completing the rest of ROOT 03 (shortcut) finishes the course and unlocks ROOT 04', async () => {
await completeActivity(session, 'root-03-m05-a01-lesson'); await completeActivity(session, 'root-03-m05-a02-demonstration');
await completeActivity(session, 'root-03-m05-a03-knowledge-check'); await completeActivity(session, 'root-03-m05-a04-guided-exercise');
await completeScenario(session, 'git-remote-clone-push', envId); await completeActivity(session, 'root-03-m05-a06-reflection');
await completeActivity(session, 'root-03-m06-a01-lesson'); await completeActivity(session, 'root-03-m06-a02-demonstration');
await completeActivity(session, 'root-03-m06-a03-knowledge-check'); await completeActivity(session, 'root-03-m06-a04-reflection');
const start = await session.request('POST', '/api/assessments/rootnode-git-fundamentals-assessment/start');
for (const s of start.body.scenarios) await completeScenario(session, s.scenario_id, envId);
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-03-git-collaborative-development').complete, true);
assert.equal(courseFrom(body.courses, 'root-04-containers-and-docker').locked, false);
});
// ── ROOT 04 — one REAL Lab task: real nested Docker, a real port-publishing diagnosis+fix ─
await step('student diagnoses and fixes a REAL container networking failure using nested Docker', async () => {
await completeActivity(session, 'root-04-m01-a01-lesson'); await completeActivity(session, 'root-04-m01-a02-demonstration');
await completeActivity(session, 'root-04-m01-a03-knowledge-check'); await completeActivity(session, 'root-04-m01-a04-reflection');
await completeActivity(session, 'root-04-m02-a01-lesson'); await completeActivity(session, 'root-04-m02-a02-demonstration');
await completeActivity(session, 'root-04-m02-a03-knowledge-check'); await completeActivity(session, 'root-04-m02-a04-guided-exercise'); await completeActivity(session, 'root-04-m02-a05-reflection');
await completeActivity(session, 'root-04-m03-a01-lesson'); await completeActivity(session, 'root-04-m03-a02-demonstration');
await completeActivity(session, 'root-04-m03-a03-knowledge-check'); await completeActivity(session, 'root-04-m03-a04-guided-exercise');
await completeScenario(session, 'docker-cli-basics', envId); await completeActivity(session, 'root-04-m03-a06-reflection');
await completeActivity(session, 'root-04-m04-a01-lesson'); await completeActivity(session, 'root-04-m04-a02-demonstration');
await completeActivity(session, 'root-04-m04-a03-knowledge-check'); await completeActivity(session, 'root-04-m04-a04-guided-exercise');
const launch = await session.request('POST', `/api/student/programs/${enrollmentId}/launch`);
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);
// Confirm the REAL failure exists before fixing it — nothing published, host curl fails.
const before = runInContainer(containerName, `curl -sf http://localhost:8099/ -o /dev/null -w '%{http_code}' || echo FAILED`);
assert.ok(before.includes('FAILED') || before.trim() === '000', `expected a genuine pre-fix connection failure, got: ${before}`);
runInContainer(containerName, `
docker rm -f net-lab-web
docker run -d --name net-lab-web -p 8099:80 nginx:alpine`);
// Give nginx a moment to actually bind before validating.
execSync('sleep 2');
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, `expected real validation to pass: ${JSON.stringify(validate.body)}`);
await completeScenario(session, 'docker-container-networking', envId);
await completeActivity(session, 'root-04-m04-a06-reflection');
});
await step('completing the rest of ROOT 04 (shortcut) finishes the ENTIRE foundations sequence and unlocks ROOT 05', async () => {
await completeActivity(session, 'root-04-m05-a01-lesson'); await completeActivity(session, 'root-04-m05-a02-demonstration');
await completeActivity(session, 'root-04-m05-a03-knowledge-check'); await completeActivity(session, 'root-04-m05-a04-guided-exercise');
await completeScenario(session, 'docker-container-storage', envId); await completeActivity(session, 'root-04-m05-a06-reflection');
await completeActivity(session, 'root-04-m06-a01-lesson'); await completeActivity(session, 'root-04-m06-a02-demonstration');
await completeActivity(session, 'root-04-m06-a03-knowledge-check'); await completeActivity(session, 'root-04-m06-a04-guided-exercise');
await completeScenario(session, 'docker-build-image', envId); await completeActivity(session, 'root-04-m06-a06-reflection');
await completeActivity(session, 'root-04-m07-a01-lesson'); await completeActivity(session, 'root-04-m07-a02-demonstration');
await completeActivity(session, 'root-04-m07-a03-knowledge-check'); await completeActivity(session, 'root-04-m07-a04-guided-exercise');
await completeScenario(session, 'docker-compose-multiservice', envId); await completeActivity(session, 'root-04-m07-a06-reflection');
await completeActivity(session, 'root-04-m08-a01-lesson'); await completeActivity(session, 'root-04-m08-a02-knowledge-check'); await completeActivity(session, 'root-04-m08-a03-reflection');
const start = await session.request('POST', '/api/assessments/rootnode-containers-assessment/start');
for (const s of start.body.scenarios) await completeScenario(session, s.scenario_id, envId);
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-04-containers-and-docker').complete, true);
assert.equal(courseFrom(body.courses, 'root-05-kubernetes-foundations').locked, false, 'the entire ROOT 01-04 foundations sequence must now unlock ROOT 05');
});
// ── Prove existing Kubernetes flow still works on the SAME real Lab (k3s never touched) ──
await step('the existing real-k3s Kubernetes flow on this SAME Lab is completely unaffected', async () => {
const kubectlOut = execSync(`docker exec ${containerName} kubectl get nodes`, { encoding: 'utf8' });
assert.ok(/Ready/.test(kubectlOut), `expected a real, healthy k3s node: ${kubectlOut}`);
});
await step('logout, login — the entire foundations sequence survives it', async () => {
await session.request('POST', '/api/logout');
const returning = makeSession(GW);
await returning.request('POST', '/api/student/login', { identifier: `p6e-foundations-${RUN_ID}`, password: 'RehearsalPass123!' });
const { body } = await returning.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseFrom(body.courses, 'root-04-containers-and-docker').complete, true);
});
// ── Instructor narrative ─────────────────────────────────────────────────────────────────
await step('instructor inspects the ROOT 02/03/04 module structure and instructor notes', async () => {
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
assert.equal(status, 200);
const mod = moduleFrom(body, 'root-04-m04-container-networking');
assert.equal(mod.activities.length, 6);
assert.ok(mod.activities[0].instructor_notes, 'instructor must see instructor notes the student view never does');
});
await step('instructor sees this specific student\'s real, complete foundations progress', async () => {
const { status, body } = await owner.request('GET', `/api/admin/students/${created.student_id}/courses?program_id=${PROGRAM}`);
assert.equal(status, 200);
assert.equal(courseFrom(body, 'root-04-containers-and-docker').complete, true);
});
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
main().catch((e) => { console.error(e); process.exit(1); });
+35 -1
View File
@@ -17,6 +17,7 @@
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const { completeRootFoundations01Through04 } = require('./foundations-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const API = process.env.PLATFORM_API_URL || 'http://localhost:4300';
@@ -38,6 +39,14 @@ const FOUNDATION = ['pod-basics-mcq', 'kubectl-essentials-mcq', 'labels-selector
const WORKLOADS = ['deploy-nginx', 'scale-deployment', 'rolling-update-task', 'liveness-probe-task', 'jobs-cronjobs-mcq', 'daemonset-mcq', 'edit-deployment-basics', 'deployment-strategy-mcq'];
const NETWORKING = ['services-mcq', 'expose-service-basics'];
const CONFIGURATION = ['configmap-volume', 'resource-limits-task', 'env-vars-configmap', 'secrets-basics', 'pod-env-vars-inline'];
// Phase 6E added real content to ROOT 01-04, and root-05's own prerequisite (root-04) is no
// longer vacuously satisfied — this suite's ROOT 05 content is now genuinely locked behind the
// real foundations sequence for any student who hasn't completed it, exactly the enforcement
// docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md's course-routes.js fix now guarantees even via
// direct API. completeRootFoundations01Through04 (foundations-helper.js) does this quickly as
// test setup, not this file's own subject — see e2e-phase6e.js for the real ROOT 01-04
// acceptance coverage.
const TROUBLESHOOTING = ['extract-logs-basics', 'kubectl-cp-basics', 'exec-command-basics', 'node-cordon-basics', 'pod-node-assignment-mcq'];
const REAL_OPS = ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-probe-port-mismatch', 'rootnode-enforce-limits'];
@@ -122,7 +131,7 @@ async function main() {
assert.equal(status, 200);
tree = body.courses;
const root05 = courseById(tree, 'root-05-kubernetes-foundations');
assert.equal(root05.locked, false); // root-04 (its real Phase 6D prerequisite) has zero activities — vacuously satisfied
assert.equal(root05.locked, true, 'Phase 6E gave root-04 (root-05\'s real prerequisite) real content — root-05 is now genuinely gated, not vacuously satisfied');
assert.equal(root05.complete, false);
const root06 = courseById(tree, 'root-06-kubernetes-application-workloads');
assert.equal(root06.locked, true, 'root-06 must be locked until root-05 is complete');
@@ -146,6 +155,12 @@ async function main() {
assert.equal(body.error, 'not_manually_completable');
});
await test('completing the real ROOT 01-04 foundations sequence unlocks ROOT 05 (Phase 6E test setup, not this file\'s own subject)', async () => {
await completeRootFoundations01Through04(casey);
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
});
await test('completing root-05\'s 20 real scenarios + the Phase 6B mental-model module marks the course complete and unlocks root-06', async () => {
for (const sid of FOUNDATION) await completeScenario(casey, sid);
// Phase 6B's root-05-m00 has its own manual-marker activities (lesson/demonstration/
@@ -170,6 +185,25 @@ async function main() {
assert.equal(root06.locked, false, 'root-06 must unlock once root-05 is complete');
});
await test('completing root-06 fully unlocks root-10 (its real course-level prerequisite, now enforced even via direct API)', async () => {
for (const sid of WORKLOADS) await completeScenario(casey, sid);
for (const suffix of ['a01-lesson-deployments', 'a02-demonstration-rollout', 'a03-knowledge-check', 'a04-guided-exercise-rollout-strategy']) {
await casey.request('POST', `/api/student/activities/root-06-m02-${suffix}/complete`);
}
await completeScenario(casey, 'rolling-update-task');
await completeScenario(casey, 'deployment-rollback');
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete');
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive']) {
await casey.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
}
await completeScenario(casey, 'job-task');
await completeScenario(casey, 'cronjob-task');
await casey.request('POST', '/api/student/activities/root-06-m03-a05-reflection/complete');
const { body } = await casey.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-10-kubernetes-troubleshooting-observability').locked, false);
});
// ── The gold module: lesson/demo/knowledge-check/guided-exercise (manual) + lab (auto) + reflection (manual) ──
await test('lesson-only activities complete via the explicit marker, not automatically', async () => {
for (const suffix of ['a01-lesson-health-check-concepts', 'a02-demonstration-probes-in-action', 'a03-knowledge-check', 'a04-guided-exercise-readiness-probe']) {
+75 -7
View File
@@ -17,6 +17,7 @@
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const { completeRootFoundations01Through04 } = require('./foundations-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
@@ -135,6 +136,12 @@ async function main() {
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software');
});
await test('completing the real ROOT 01-04 foundations sequence unlocks ROOT 05 (Phase 6E test setup, not this file\'s own subject)', async () => {
await completeRootFoundations01Through04(casey);
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
});
await test('lesson/knowledge_check completion works identically in a brand-new module (root-05-m00)', async () => {
for (const suffix of ['a01-lesson-mental-model', 'a02-demonstration-desired-state', 'a03-knowledge-check', 'a04-guided-exercise-namespaces']) {
const { status } = await casey.request('POST', `/api/student/activities/root-05-m00-${suffix}/complete`);
@@ -190,10 +197,15 @@ async function main() {
await casey.request('POST', `/api/student/activities/root-06-m02-${suffix}/complete`);
}
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete');
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a02-knowledge-check']) {
await casey.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
// Phase 6C upgraded root-06-m03 to the full gold pattern (added a01b/a02b/a05) — all four
// manual-marker activities, plus a05's own reflection, are required for root-06 the COURSE
// to actually be complete (root-09's real prerequisite), not just its original two.
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive', 'a05-reflection']) {
const { status } = await casey.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
assert.equal(status, 200, `completing root-06-m03-${suffix} should 200`);
}
let tree = (await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`)).body.courses;
assert.equal(courseById(tree, 'root-06-kubernetes-application-workloads').complete, true, 'root-06 must be fully complete before root-09 unlocks');
assert.equal(moduleById(tree, 'root-09-m01-security-model-and-securitycontext').locked, false, 'root-09 should now be unlocked');
// Complete only the REQUIRED activities in root-09-m01, deliberately skipping the 3 optional ones.
@@ -286,6 +298,24 @@ async function main() {
for (const s of body.scenarios) await completeScenario(bypass, s.scenario_id);
});
await test('completing the real ROOT 01-04 foundations sequence plus ROOT 05\'s manual markers unlocks ROOT 06 for bypass', async () => {
// Phase 6E test setup, not this file's own subject — see e2e-phase6e.js for real coverage.
await completeRootFoundations01Through04(bypass);
for (const suffix of ['a01-lesson-mental-model', 'a02-demonstration-desired-state', 'a03-knowledge-check', 'a04-guided-exercise-namespaces']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-05-m00-${suffix}/complete`);
assert.equal(status, 200, `completing root-05-m00-${suffix} should 200`);
}
// root-05-m00's own lab activity (a05) is scenario-derived, not a manual marker — and
// root-05-m01's real prerequisite is root-05-m00 COMPLETE, so this can't be skipped the
// way casey's equivalent flow doesn't skip it either.
await completeScenario(bypass, 'namespaces-basics');
{ const { status } = await bypass.request('POST', '/api/student/activities/root-05-m00-a06-reflection/complete'); assert.equal(status, 200); }
for (const suffix of ['a00-lesson-kubectl-fluency', 'a21-reflection']) {
const { status, body } = await bypass.request('POST', `/api/student/activities/root-05-m01-${suffix}/complete`);
assert.equal(status, 200, `completing root-05-m01-${suffix} should 200: ${JSON.stringify(body)}`);
}
});
await test('a legitimately unlocked capstone start is still accepted (the fix does not over-block)', async () => {
const ALL_STAGES = ['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',
@@ -311,12 +341,50 @@ async function main() {
});
await test('the Capstone Readiness module itself is completable independent of the real assessment', async () => {
const enrollmentIdForCasey = enrollmentId;
for (const suffix of ['a01-lesson-what-it-measures', 'a02-lesson-time-management', 'a03-knowledge-check', 'a04-reflection-readiness']) {
const { status } = await casey.request('POST', `/api/student/activities/root-capstone-m02-${suffix}/complete`);
assert.equal(status, 200);
// root-capstone's real prerequisite is root-12-real-ops complete, which needs root-10
// complete (root-06 complete first) — bypass already has every relevant LAB scenario done
// from the ALL_STAGES completion above; only the manual-marker activities (lessons/
// reflections/knowledge-checks) for root-06/root-10-m01/m03/root-12-m01 remain, now
// genuinely enforced even via direct API.
// ALL_STAGES (above) predates root-06-m02's a05b-lab-deployment-rollback activity and never
// included its scenario — completed explicitly here, same gap e2e-phase6a.js's casey flow
// already accounts for.
await completeScenario(bypass, 'rolling-update-task');
await completeScenario(bypass, 'deployment-rollback');
for (const suffix of ['a01-lesson-deployments', 'a02-demonstration-rollout', 'a03-knowledge-check', 'a04-guided-exercise-rollout-strategy', 'a06-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-06-m02-${suffix}/complete`);
assert.equal(status, 200, `completing root-06-m02-${suffix} should 200`);
}
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentIdForCasey}/courses`);
await completeScenario(bypass, 'job-task');
await completeScenario(bypass, 'cronjob-task');
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive', 'a05-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
assert.equal(status, 200, `completing root-06-m03-${suffix} should 200`);
}
for (const suffix of ['a01-lesson-health-check-concepts', 'a02-demonstration-probes-in-action', 'a03-knowledge-check', 'a04-guided-exercise-readiness-probe', 'a06-reflection-real-ops-takeaway']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-10-m01-${suffix}/complete`);
assert.equal(status, 200, `completing root-10-m01-${suffix} should 200`);
}
for (const suffix of ['a01-lesson-framework', 'a02-demonstration-crashloop', 'a03-knowledge-check', 'a03b-guided-exercise-endpoint-mismatch']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-10-m03-${suffix}/complete`);
assert.equal(status, 200, `completing root-10-m03-${suffix} should 200`);
}
await completeScenario(bypass, 'crashloop-fix');
await completeScenario(bypass, 'endpoint-fix-task');
// root-10-m02 (general-troubleshooting) is also part of root-10 the COURSE's completion
// total — ALL_STAGES (above) covers 5 of its 6 scenarios but predates container-logging-mcq.
await completeScenario(bypass, 'container-logging-mcq');
await bypass.request('POST', '/api/student/activities/root-10-m03-a06-reflection/complete');
await bypass.request('POST', '/api/student/activities/root-12-m01-a00-lesson-incident-response/complete');
await bypass.request('POST', '/api/student/activities/root-12-m01-a99-reflection-post-incident/complete');
const { body: chk } = await bypass.request('GET', `/api/student/programs/${bypassEnroll.enrollment_id}/courses`);
assert.equal(courseById(chk.courses, 'root-12-real-ops').complete, true, 'root-12 must be complete before the capstone course unlocks');
for (const suffix of ['a01-lesson-what-it-measures', 'a02-lesson-time-management', 'a03-knowledge-check', 'a04-reflection-readiness']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-capstone-m02-${suffix}/complete`);
assert.equal(status, 200, `completing root-capstone-m02-${suffix} should 200`);
}
const { body } = await bypass.request('GET', `/api/student/programs/${bypassEnroll.enrollment_id}/courses`);
assert.equal(moduleById(body.courses, 'root-capstone-m02-capstone-readiness').complete, true);
});
+9
View File
@@ -15,6 +15,7 @@
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const { completeRootFoundations01Through04, completeThroughRoot06, completeThroughRoot10 } = require('./foundations-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
@@ -104,6 +105,9 @@ async function main() {
// ── 3/18: required/optional — the newly-wired optional readiness-probe lab ───────────────
await test('the new optional readiness-probe lab does not block root-10-m01 completion when skipped', async () => {
const student = await newStudent(owner, 'OptSkip');
// Phase 6E made root-06 a real, non-vacuous prerequisite for root-10 — test setup, not this
// test's own subject.
await completeThroughRoot06(student.session);
// Complete only the REQUIRED activities in root-10-m01, deliberately skipping the optional lab.
for (const suffix of ['a01-lesson-health-check-concepts', 'a02-demonstration-probes-in-action', 'a03-knowledge-check', 'a04-guided-exercise-readiness-probe']) {
const { status } = await student.session.request('POST', `/api/student/activities/root-10-m01-${suffix}/complete`);
@@ -133,6 +137,9 @@ async function main() {
// ── 14: idempotency — completing the same activity twice never errors or double-counts ──
await test('completing the same activity twice is idempotent', async () => {
const student = await newStudent(owner, 'Idem');
// root-05-m01's real prerequisite is root-05-m00 complete (course-level: root-04 complete)
// — test setup, not this test's own subject.
await completeThroughRoot06(student.session);
const first = await student.session.request('POST', '/api/student/activities/root-05-m01-a00-lesson-kubectl-fluency/complete');
assert.equal(first.status, 200);
const second = await student.session.request('POST', '/api/student/activities/root-05-m01-a00-lesson-kubectl-fluency/complete');
@@ -214,6 +221,8 @@ async function main() {
await test('a student can complete ROOT 11 module 1 end-to-end (lesson-only path)', async () => {
const student = await newStudent(owner, 'Root11');
// ROOT 11's real prerequisite is ROOT 10 complete — test setup, not this test's own subject.
await completeThroughRoot10(student.session);
for (const suffix of ['a01-lesson-devops-platform-thinking', 'a02-knowledge-check', 'a03-reflection']) {
const { status } = await student.session.request('POST', `/api/student/activities/root-11-m01-${suffix}/complete`);
assert.equal(status, 200);
+22 -13
View File
@@ -74,11 +74,15 @@ async function main() {
});
// ── 2/3/4: ROOT 02/03/04 hierarchy — honestly PLANNED, zero modules, not padded ──────────
await test('ROOT 02/03/04 are honestly PLANNED with zero modules — not padded to look further along than they are', async () => {
await test('ROOT 02/03/04 existed honestly PLANNED at the end of Phase 6D, and are real, authored courses as of Phase 6E', async () => {
// Phase 6D deliberately built only ROOT 01 and left 02-04 honestly empty — Phase 6E (see
// docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md) authored them for real. This assertion is
// updated to reflect that intentional, documented progression rather than re-asserting a
// fact Phase 6E was explicitly built to change.
for (const id of ['root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
const course = courseById(structure, id);
assert.equal(course.status, 'planned', `${id} must stay honestly planned`);
assert.equal(course.modules.length, 0, `${id} must have zero modules — no fabricated content`);
assert.equal(course.status, 'ready', `${id} should now be ready (Phase 6E)`);
assert.ok(course.modules.length > 0, `${id} should now have real modules (Phase 6E)`);
}
});
@@ -120,18 +124,19 @@ async function main() {
});
await test('completing every ROOT 01 module and its assessment marks the whole course complete', async () => {
// ROOT 01's own modules carry real intra-course prerequisites (m02 requires m01 complete,
// etc.) — each module must be completed fully, including its lab and reflection, before the
// next module unlocks. Completing every module's early activities first and circling back
// for labs/reflections later (the original order here) trips the module-lock check.
const labs = { 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 completeActivity(casey.session, `root-01-${m}-a01-lesson`);
await completeActivity(casey.session, `root-01-${m}-a02-demonstration`);
await completeActivity(casey.session, `root-01-${m}-a03-knowledge-check`);
await completeActivity(casey.session, `root-01-${m}-a04-guided-exercise`);
await completeScenario(casey.session, labs[m]);
await completeActivity(casey.session, `root-01-${m}-a06-reflection`);
}
for (const [sid, activity] of [['linux-shell-basics', 'root-01-m02-a06-reflection'], ['linux-filesystem-navigation', 'root-01-m03-a06-reflection'], ['linux-files-and-text', 'root-01-m04-a06-reflection'], ['linux-process-management', 'root-01-m05-a06-reflection']]) {
await completeScenario(casey.session, sid);
await completeActivity(casey.session, activity);
}
await completeScenario(casey.session, 'linux-permissions-foundations');
await completeActivity(casey.session, 'root-01-m06-a06-reflection');
const start = await casey.session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
assert.equal(start.status, 201);
@@ -157,11 +162,15 @@ async function main() {
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, true, 'ROOT 02 must be locked until ROOT 01 is complete');
});
await test('ROOT 03/04/05 stay unlocked (vacuously satisfied) since their own prerequisite courses have zero content yet', async () => {
await test('ROOT 03/04/05 are now genuinely locked for a fresh student (Phase 6E gave 02/03/04 real content)', async () => {
// At the end of Phase 6D this was true via vacuous satisfaction (02-04 had zero activities).
// Phase 6E authored real content for 02-04, so the SAME chain now genuinely gates for real —
// this assertion is updated to prove that real enforcement, not the vacuous case it
// originally documented.
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, false);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, false);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, true);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, true);
});
// ── 26/27: existing ROOT 05+ and Capstone state is untouched by this phase ──────────────
+377
View File
@@ -0,0 +1,377 @@
#!/usr/bin/env node
// Phase 6E — ROOT Foundations Completion acceptance test. Covers ROOT 02 (Linux Administration
// & Networking), ROOT 03 (Git & Collaborative Development), ROOT 04 (Containers & Docker), and
// the now-complete ROOT01->02->03->04->05 prerequisite chain. Same conventions as
// e2e-phase6a/6b/6c/6d.js: assert-based, no framework, live docker-compose.dev.yml stack, no
// real Lab container launched here (see infra/scripts/drills/test-phase6e-foundations-rehearsal.js
// for the real-Lab/real-Docker/real-Git counterpart).
//
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d
// (cd api && PLATFORM_DB_URL=... node bootstrap-owner.js OWNER_EMAIL "Owner" OWNER_PASSWORD)
// OWNER_EMAIL=... OWNER_PASSWORD=... node test/e2e-phase6e.js
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (bootstrap one first with platform/api/bootstrap-owner.js)');
process.exit(1);
}
const RUN_ID = Date.now();
const PROGRAM = 'root-kubernetes-foundations';
let passed = 0, failed = 0;
async function test(name, fn) {
try { await fn(); console.log(` ok — ${name}`); passed++; }
catch (e) { console.error(` FAIL — ${name}\n ${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; }
function findActivity(courses, id) { for (const c of courses) for (const m of c.modules) { const a = m.activities.find((x) => x.activity_id === id); if (a) return a; } return null; }
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}/..`, stdio: 'pipe' }).toString().trim();
}
async function newStudent(owner, tag) {
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `E2E-6E-${tag}-${RUN_ID}`, login_identifier: `e2e6e-${tag}-${RUN_ID}` });
const s = makeSession(GW);
await s.request('POST', '/api/student/activate', { token: create.activation_token, password: `${tag}Password123!` });
const { body: enroll } = await owner.request('POST', '/api/admin/enrollments', { student_id: create.student_id, program_id: PROGRAM, status: 'active' });
return { session: s, studentId: create.student_id, enrollmentId: enroll.enrollment_id };
}
async function completeActivity(session, id) {
const r = await session.request('POST', `/api/student/activities/${id}/complete`);
assert.equal(r.status, 200, `completing ${id} should 200: ${JSON.stringify(r.body)}`);
}
async function completeScenario(session, sid) {
const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' });
assert.equal(r.status, 200, `completing scenario ${sid} should 200`);
}
// NOTE: each course's modules carry real intra-course prerequisites (m02 requires m01 complete,
// etc — see migrations 025-028), so every helper below completes ONE module fully (including
// its lab scenario(s) and reflection) before touching the next, mirroring how a real student
// must actually progress. Completing all modules' early activities first and circling back for
// labs/reflections later would trip the module-lock check for later modules — a real ordering
// constraint, not a test artifact.
async function completeRoot01(session) {
await completeActivity(session, 'root-01-m01-a01-lesson');
await completeActivity(session, 'root-01-m01-a02-guided-exercise');
await completeScenario(session, 'linux-inspect-hardware');
await completeActivity(session, 'root-01-m01-a04-reflection');
const labs = { 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 completeActivity(session, `root-01-${m}-a01-lesson`);
await completeActivity(session, `root-01-${m}-a02-demonstration`);
await completeActivity(session, `root-01-${m}-a03-knowledge-check`);
await completeActivity(session, `root-01-${m}-a04-guided-exercise`);
await completeScenario(session, labs[m]);
await completeActivity(session, `root-01-${m}-a06-reflection`);
}
const r1 = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
for (const sc of r1.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function completeRoot02(session) {
await completeActivity(session, 'root-02-m01-a01-lesson');
await completeActivity(session, 'root-02-m01-a02-demonstration');
await completeActivity(session, 'root-02-m01-a03-knowledge-check');
await completeActivity(session, 'root-02-m01-a04-guided-exercise');
await completeScenario(session, 'linux-service-inspection');
await completeScenario(session, 'linux-service-lifecycle');
await completeActivity(session, 'root-02-m01-a07-reflection');
await completeActivity(session, 'root-02-m02-a01-lesson');
await completeActivity(session, 'root-02-m02-a02-demonstration');
await completeActivity(session, 'root-02-m02-a03-knowledge-check');
await completeActivity(session, 'root-02-m02-a04-guided-exercise');
await completeScenario(session, 'linux-storage-inspection');
await completeActivity(session, 'root-02-m02-a06-reflection');
await completeActivity(session, 'root-02-m03-a01-lesson');
await completeActivity(session, 'root-02-m03-a02-demonstration');
await completeActivity(session, 'root-02-m03-a03-knowledge-check');
await completeActivity(session, 'root-02-m03-a04-guided-exercise');
await completeActivity(session, 'root-02-m03-a05-reflection');
await completeActivity(session, 'root-02-m04-a01-lesson');
await completeActivity(session, 'root-02-m04-a02-demonstration');
await completeActivity(session, 'root-02-m04-a03-knowledge-check');
await completeActivity(session, 'root-02-m04-a04-guided-exercise');
await completeScenario(session, 'linux-network-inspection');
await completeActivity(session, 'root-02-m04-a06-reflection');
await completeActivity(session, 'root-02-m05-a01-lesson');
await completeActivity(session, 'root-02-m05-a02-demonstration');
await completeActivity(session, 'root-02-m05-a03-knowledge-check');
await completeActivity(session, 'root-02-m05-a04-guided-exercise');
await completeScenario(session, 'linux-dns-http-diagnosis');
await completeActivity(session, 'root-02-m05-a06-reflection');
await completeActivity(session, 'root-02-m06-a01-lesson');
await completeActivity(session, 'root-02-m06-a02-demonstration');
await completeActivity(session, 'root-02-m06-a03-knowledge-check');
await completeActivity(session, 'root-02-m06-a04-guided-exercise');
await completeScenario(session, 'linux-troubleshooting-framework');
await completeActivity(session, 'root-02-m06-a06-reflection');
const r2 = await session.request('POST', '/api/assessments/rootnode-linux-networking-assessment/start');
for (const sc of r2.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function completeRoot03(session) {
await completeActivity(session, 'root-03-m01-a01-lesson');
await completeActivity(session, 'root-03-m01-a02-knowledge-check');
await completeActivity(session, 'root-03-m01-a03-reflection');
await completeActivity(session, 'root-03-m02-a01-lesson');
await completeActivity(session, 'root-03-m02-a02-demonstration');
await completeActivity(session, 'root-03-m02-a03-knowledge-check');
await completeActivity(session, 'root-03-m02-a04-guided-exercise');
await completeActivity(session, 'root-03-m02-a05-reflection');
await completeActivity(session, 'root-03-m03-a01-lesson');
await completeActivity(session, 'root-03-m03-a02-demonstration');
await completeActivity(session, 'root-03-m03-a03-knowledge-check');
await completeActivity(session, 'root-03-m03-a04-guided-exercise');
await completeScenario(session, 'git-everyday-basics');
await completeActivity(session, 'root-03-m03-a06-reflection');
await completeActivity(session, 'root-03-m04-a01-lesson');
await completeActivity(session, 'root-03-m04-a02-demonstration');
await completeActivity(session, 'root-03-m04-a03-knowledge-check');
await completeActivity(session, 'root-03-m04-a04-guided-exercise');
await completeScenario(session, 'git-branching-and-merge');
await completeActivity(session, 'root-03-m04-a06-reflection');
await completeActivity(session, 'root-03-m05-a01-lesson');
await completeActivity(session, 'root-03-m05-a02-demonstration');
await completeActivity(session, 'root-03-m05-a03-knowledge-check');
await completeActivity(session, 'root-03-m05-a04-guided-exercise');
await completeScenario(session, 'git-remote-clone-push');
await completeActivity(session, 'root-03-m05-a06-reflection');
await completeActivity(session, 'root-03-m06-a01-lesson');
await completeActivity(session, 'root-03-m06-a02-demonstration');
await completeActivity(session, 'root-03-m06-a03-knowledge-check');
await completeActivity(session, 'root-03-m06-a04-reflection');
const r3 = await session.request('POST', '/api/assessments/rootnode-git-fundamentals-assessment/start');
for (const sc of r3.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function completeRoot04(session) {
await completeActivity(session, 'root-04-m01-a01-lesson');
await completeActivity(session, 'root-04-m01-a02-demonstration');
await completeActivity(session, 'root-04-m01-a03-knowledge-check');
await completeActivity(session, 'root-04-m01-a04-reflection');
await completeActivity(session, 'root-04-m02-a01-lesson');
await completeActivity(session, 'root-04-m02-a02-demonstration');
await completeActivity(session, 'root-04-m02-a03-knowledge-check');
await completeActivity(session, 'root-04-m02-a04-guided-exercise');
await completeActivity(session, 'root-04-m02-a05-reflection');
const labs4 = { 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 completeActivity(session, `root-04-${m}-a01-lesson`);
await completeActivity(session, `root-04-${m}-a02-demonstration`);
await completeActivity(session, `root-04-${m}-a03-knowledge-check`);
await completeActivity(session, `root-04-${m}-a04-guided-exercise`);
await completeScenario(session, labs4[m]);
await completeActivity(session, `root-04-${m}-a06-reflection`);
}
await completeActivity(session, 'root-04-m08-a01-lesson');
await completeActivity(session, 'root-04-m08-a02-knowledge-check');
await completeActivity(session, 'root-04-m08-a03-reflection');
const r4 = await session.request('POST', '/api/assessments/rootnode-containers-assessment/start');
for (const sc of r4.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function main() {
console.log('Phase 6E ROOT Foundations Completion acceptance test\n');
const owner = makeSession(GW);
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
let structure;
// ── 1/8/15: hierarchy for ROOT 02/03/04 ─────────────────────────────────────────────────
await test('ROOT 02/03/04 each have their full designed module set, in sequence, all READY', async () => {
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
assert.equal(status, 200);
structure = body;
const expected = {
'root-02-linux-administration-networking': ['root-02-m01-linux-system-administration', 'root-02-m02-storage-and-filesystems', 'root-02-m03-networking-mental-model', 'root-02-m04-linux-network-inspection', 'root-02-m05-dns-and-http', 'root-02-m06-linux-troubleshooting'],
'root-03-git-collaborative-development': ['root-03-m01-why-version-control-exists', 'root-03-m02-git-mental-model', 'root-03-m03-everyday-git', 'root-03-m04-branching-and-merging', 'root-03-m05-remote-repositories', 'root-03-m06-engineering-git-workflow'],
'root-04-containers-and-docker': ['root-04-m01-why-containers-exist', 'root-04-m02-images-and-containers', 'root-04-m03-docker-cli', 'root-04-m04-container-networking', 'root-04-m05-container-storage', 'root-04-m06-dockerfile-and-image-build', 'root-04-m07-docker-compose', 'root-04-m08-containers-to-kubernetes'],
};
for (const [courseId, moduleIds] of Object.entries(expected)) {
const course = courseById(structure, courseId);
assert.equal(course.status, 'ready', `${courseId} must be ready`);
assert.deepEqual(course.modules.map((m) => m.module_id), moduleIds, `${courseId} module order mismatch`);
for (const m of course.modules) assert.equal(m.status, 'ready', `${m.module_id} must be ready`);
}
});
// ── 2/9/16: content references real ────────────────────────────────────────────────────
await test('every ROOT 02/03/04 module has real, loaded lesson content', async () => {
for (const modId of ['root-02-m03-networking-mental-model', 'root-03-m02-git-mental-model', 'root-04-m01-why-containers-exist']) {
const mod = moduleById(structure, modId);
const lesson = mod.activities.find((a) => a.activity_type === 'lesson');
assert.ok(lesson.body && lesson.body.length > 300, `${modId}'s lesson must have real content`);
}
});
// ── 4/5/6/7: ROOT 02 practicals, networking, troubleshooting, assessment ────────────────
await test('ROOT 02 assessment pool matches the documented design (2 required + 4 random)', async () => {
const spec = JSON.parse(psql(`SELECT pool_spec FROM assessment_definitions WHERE assessment_definition_id = 'rootnode-linux-networking-assessment'`));
assert.deepEqual(spec.required, ['linux-network-inspection', 'linux-troubleshooting-framework']);
assert.equal(spec.random.reduce((n, g) => n + g.count, 0), 4);
});
// ── 10/11/12/13: ROOT 03 Git — real repository state validation ─────────────────────────
await test('ROOT 03 assessment pool matches the documented design (2 required + 3 random)', async () => {
const spec = JSON.parse(psql(`SELECT pool_spec FROM assessment_definitions WHERE assessment_definition_id = 'rootnode-git-fundamentals-assessment'`));
assert.deepEqual(spec.required, ['git-everyday-basics', 'git-branching-and-merge']);
assert.equal(spec.random.reduce((n, g) => n + g.count, 0), 3);
});
// ── 16/22/23/24/25: ROOT 04 nested Docker, Compose ──────────────────────────────────────
await test('ROOT 04 assessment pool matches the documented design (2 required + 4 random)', async () => {
const spec = JSON.parse(psql(`SELECT pool_spec FROM assessment_definitions WHERE assessment_definition_id = 'rootnode-containers-assessment'`));
assert.deepEqual(spec.required, ['docker-cli-basics', 'docker-build-image']);
assert.equal(spec.random.reduce((n, g) => n + g.count, 0), 4);
});
// ── A real student walking the full ROOT 01->02->03->04 chain end to end ────────────────
const alice = await newStudent(owner, 'Alice');
await test('resume points at ROOT 01 module 1 activity 1 for a brand-new student', async () => {
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
});
// ── 30/26/27/28/29: direct API cannot bypass prerequisites (Section 11's real finding) ──
await test('direct API cannot mark a ROOT 02 activity complete while ROOT 01 is incomplete (found and fixed this phase)', async () => {
const r = await alice.session.request('POST', '/api/student/activities/root-02-m01-a01-lesson/complete');
assert.equal(r.status, 403);
assert.equal(r.body.error, 'locked');
});
await test('direct API cannot mark a ROOT 06 activity complete while ROOT 05 is incomplete (pre-existing gap, same fix)', async () => {
const r = await alice.session.request('POST', '/api/student/activities/root-06-m02-a01-lesson-deployments/complete');
assert.equal(r.status, 403);
assert.equal(r.body.error, 'locked');
});
await test('ROOT 01 completes and unlocks ROOT 02, while ROOT 03/04/05 remain locked (real content now gates them)', async () => {
await completeRoot01(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, true);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, false);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, true, 'ROOT 03 now genuinely gates on ROOT 02 having real content');
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
});
await test('ROOT 02 completes and unlocks ROOT 03, not ROOT 04 yet', async () => {
await completeRoot02(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').complete, true);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, false);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
});
await test('ROOT 03 completes and unlocks ROOT 04, not ROOT 05 yet', async () => {
await completeRoot03(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').complete, true);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, false);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, true);
});
await test('ROOT 04 completes and unlocks ROOT 05 — the complete foundations chain is proven', async () => {
await completeRoot04(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.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);
});
// ── 39: existing ROOT 05+ content remains intact ────────────────────────────────────────
await test('ROOT 06 stays locked behind ROOT 05 exactly as before (unaffected by the foundations chain)', async () => {
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').locked, true);
});
// ── 40: Capstone behavior unchanged ─────────────────────────────────────────────────────
await test('the capstone unlock gate is unaffected by the new foundations chain', async () => {
const r = await alice.session.request('POST', '/api/assessments/rootnode-capstone/start');
assert.equal(r.status, 403);
assert.equal(r.body.error, 'locked');
});
// ── 31: optional work does not block (is_required still generalizes) ───────────────────
await test('the pre-existing is_required rule is unaffected by this phase', async () => {
const mod = moduleById(structure, 'root-10-m01-health-probes-and-troubleshooting');
assert.equal(mod.activities.filter((a) => a.is_required === false).length, 1);
});
// ── 18/19: self-paced content never defers to a live instructor ────────────────────────
await test('self-paced ROOT 02/03/04 content never defers explanation to a live instructor', async () => {
for (const courseId of ['root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
const course = courseById(structure, courseId);
for (const mod of course.modules) for (const a of mod.activities) {
if (!a.body) continue;
assert.ok(!/ask your instructor/i.test(a.body) && !/your instructor will explain/i.test(a.body), `${a.activity_id} must not defer to a live instructor`);
}
}
});
// ── 20/36: instructor notes hidden from students, visible to instructors ───────────────
await test('instructor notes on ROOT 02/03/04 content are visible to instructors and never to students', async () => {
const lesson = findActivity(structure, 'root-03-m02-a01-lesson');
assert.ok(lesson.instructor_notes && lesson.instructor_notes.length > 0);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
const studentLesson = findActivity(body.courses, 'root-03-m02-a01-lesson');
assert.equal(studentLesson.instructor_notes, undefined);
});
// ── 37: instructor RBAC ─────────────────────────────────────────────────────────────────
const { body: strayInstructor } = await owner.request('POST', '/api/admin/instructors', { email: `e2e6e-stray-${RUN_ID}@example.com`, display_name: 'Stray Instructor 6E', role: 'instructor', password: 'StrayInstructorPass6E!' });
const stray = makeSession(GW);
await stray.request('POST', '/api/auth/instructor/login', { email: `e2e6e-stray-${RUN_ID}@example.com`, password: 'StrayInstructorPass6E!' });
await test('a plain instructor with no relationship to the student cannot see their foundations progress', async () => {
const { status } = await stray.request('GET', `/api/admin/students/${alice.studentId}/courses?program_id=${PROGRAM}`);
assert.equal(status, 403);
});
await test('owner/admin can see any self-paced student\'s complete foundations progress', async () => {
const { status, body } = await owner.request('GET', `/api/admin/students/${alice.studentId}/courses?program_id=${PROGRAM}`);
assert.equal(status, 200);
assert.equal(courseById(body, 'root-04-containers-and-docker').complete, true);
});
// ── 33/34: progress persistence across logout/login ─────────────────────────────────────
await test('foundations progress survives logout and a fresh login', async () => {
await alice.session.request('POST', '/api/logout');
const returning = makeSession(GW);
await returning.request('POST', '/api/student/login', { identifier: `e2e6e-Alice-${RUN_ID}`, password: 'AlicePassword123!' });
const { body } = await returning.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').complete, true);
});
// ── 35: two students isolated ───────────────────────────────────────────────────────────
const bob = await newStudent(owner, 'Bob');
await test('a second, fresh student sees independent, empty foundations progress and a fully-locked chain', async () => {
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, false);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').locked, false);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, true);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, true);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, true);
});
// ── content version stability ────────────────────────────────────────────────────────────
await test('bumping a ROOT 03 module\'s version does not drop its content', async () => {
psql(`UPDATE course_modules SET version = version + 1 WHERE module_id = 'root-03-m04-branching-and-merging'`);
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
const mod = moduleById(body, 'root-03-m04-branching-and-merging');
assert.equal(mod.activities.length, 6);
assert.ok(mod.activities[0].body && mod.activities[0].body.length > 300);
});
// ── content validation as part of this suite ────────────────────────────────────────────
await test('infra/scripts/validate-course-content.js reports zero errors against the current database', () => {
execSync('node infra/scripts/validate-course-content.js', { cwd: `${__dirname}/../..`, stdio: 'pipe' });
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
}
main().catch((e) => { console.error('FATAL', e); process.exit(1); });
+85
View File
@@ -0,0 +1,85 @@
// 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 };