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:
@@ -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); });
|
||||
Reference in New Issue
Block a user