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
+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);
});