test(phase6f): add V1 pilot acceptance suite; fix regression from real curriculum changes

- New platform/test/e2e-phase6f.js (17 tests): V1 manifest/exclusion correctness, the full
  V1 prerequisite graph, ROOT 01-04 re-verification, the orientation module's unlock
  behavior, optional-activity handling, the strengthened Capstone gate in both directions,
  instructor-notes RBAC on locked (not just draft) content, isolation, and an explicit
  age-independence check.
- foundations-helper.js: completeThroughRoot06/completeThroughRoot10 updated for the new
  practice-bank intro-lesson activities; new completeThroughCapstoneEligible() helper
  (ROOT 07-m03/m04 + full ROOT 09 + ROOT 12) shared by e2e-phase6f.js and both new drills.
- e2e-phase6a/6b/6d/6e.js: resume-pointer assertions updated for the new orientation
  module; e2e-phase6a.js's and e2e-phase6b.js's own inline ROOT 06/07/09/10/12 completion
  sequences updated for the new intro-lesson activities and the new root-09 Capstone
  dependency; e2e-phase6b.js's stale ROOT 09 status assertion and its draft-activity test
  (previously targeting the now fully-excluded ROOT 08) corrected.
- test-phase6d/6e-foundations-rehearsal.js: same orientation-module fix applied to the
  real-Lab dress rehearsals.

All of 6A (23/23) / 6B (26/26) / 6C (17/17) / 6D (21/21) / 6E (23/23) / 6F (17/17) verified
green on independent fresh stacks. See docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 18:33:21 -05:00
co-authored by Claude Sonnet 5
parent f695b7c95d
commit 2864fe03dc
8 changed files with 397 additions and 22 deletions
@@ -59,9 +59,18 @@ async function main() {
}); });
await step('resume points at ROOT 01\'s very first activity', async () => { await step('resume points at ROOT 01\'s very first activity', async () => {
// Phase 6F added a required orientation module in front of root-01-m01 — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`); const { body } = await session.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations'); assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson'); assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
});
await step('student completes the ROOT_ V1 orientation module', async () => {
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) {
const { status } = await session.request('POST', `/api/student/activities/${id}/complete`);
assert.equal(status, 200);
}
}); });
await step('student reads ROOT 01 module 1\'s lesson and guided exercise', async () => { await step('student reads ROOT 01 module 1\'s lesson and guided exercise', async () => {
@@ -78,6 +78,9 @@ async function main() {
// ── ROOT 01 (already READY since Phase 6D) — complete quickly via real scenario endpoint ── // ── 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 () => { await step('ROOT 01 completes via the real Lab scenario-validation API', async () => {
// Phase 6F added a required orientation module gating root-01-m01 — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) await completeActivity(session, id);
for (const suffix of ['a01-lesson', 'a02-guided-exercise']) await completeActivity(session, `root-01-m01-${suffix}`); for (const suffix of ['a01-lesson', 'a02-guided-exercise']) await completeActivity(session, `root-01-m01-${suffix}`);
await completeScenario(session, 'linux-inspect-hardware', envId); await completeScenario(session, 'linux-inspect-hardware', envId);
await completeActivity(session, 'root-01-m01-a04-reflection'); await completeActivity(session, 'root-01-m01-a04-reflection');
+17 -4
View File
@@ -89,7 +89,10 @@ async function main() {
const plannedWithModules = structure.filter((c) => c.status === 'planned' && c.modules.length > 0); const plannedWithModules = structure.filter((c) => c.status === 'planned' && c.modules.length > 0);
assert.equal(plannedWithModules.length, 0, 'a PLANNED course must have zero modules, not placeholder content'); assert.equal(plannedWithModules.length, 0, 'a PLANNED course must have zero modules, not placeholder content');
const gold = courseById(structure, 'root-10-kubernetes-troubleshooting-observability'); const gold = courseById(structure, 'root-10-kubernetes-troubleshooting-observability');
assert.equal(gold.status, 'partial'); // real content (gold module) alongside un-authored modules // Phase 6F gave root-10-m02 (the bare practice bank) real framing (docs/
// PHASE6F_ROOT_V1_PILOT_READINESS.md), so all three modules are now genuinely authored and
// the course honestly earns 'ready' — no fabrication, just a real content-completeness fix.
assert.equal(gold.status, 'ready');
}); });
await test('the gold-standard module is fully READY with 7 ordered activities and real content', async () => { await test('the gold-standard module is fully READY with 7 ordered activities and real content', async () => {
@@ -143,10 +146,12 @@ async function main() {
// sequenced before ROOT 05 — it is now genuinely the program's front door, not ROOT 05's // sequenced before ROOT 05 — it is now genuinely the program's front door, not ROOT 05's
// own gold module (see docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's original intent — // own gold module (see docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's original intent —
// "resume lands on the first REAL content, not a bare-lab bank" — is still what's being // "resume lands on the first REAL content, not a bare-lab bank" — is still what's being
// proven, just against the current front door. // proven, just against the current front door. Phase 6F then added a required orientation
// module (root-01-m00-welcome-to-root) in front of that — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations'); assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software'); assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson'); assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
}); });
await test('a student cannot mark a lab activity complete manually — only real scenario completion counts', async () => { await test('a student cannot mark a lab activity complete manually — only real scenario completion counts', async () => {
@@ -193,6 +198,14 @@ async function main() {
await completeScenario(casey, 'rolling-update-task'); await completeScenario(casey, 'rolling-update-task');
await completeScenario(casey, 'deployment-rollback'); await completeScenario(casey, 'deployment-rollback');
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete'); await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete');
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
// and not part of WORKLOADS's scenario list. root-06-m01's own prerequisite is root-06-m02
// complete, so this must come after root-06-m02's reflection above, not before it — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
{
const { status } = await casey.request('POST', '/api/student/activities/root-06-m01-a00-lab-bank-intro/complete');
assert.equal(status, 200, 'completing root-06-m01-a00-lab-bank-intro should 200');
}
for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive']) { 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 casey.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
} }
+86 -10
View File
@@ -86,9 +86,13 @@ async function main() {
} }
}); });
await test('ROOT 08 and ROOT 09 moved from PLANNED to PARTIAL — real content exists now', async () => { await test('ROOT 08 stays PARTIAL, ROOT 09 reached READY — real content exists now', async () => {
// Phase 6F (Step 0's audit) found ROOT 09's course-level status had drifted stale — all 4
// of its modules were already individually 'ready' — and corrected it; ROOT 08 is
// unaffected and additionally excluded from V1 (visibility='draft') this same phase — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
assert.equal(courseById(structure, 'root-08-kubernetes-storage-scheduling').status, 'partial'); assert.equal(courseById(structure, 'root-08-kubernetes-storage-scheduling').status, 'partial');
assert.equal(courseById(structure, 'root-09-kubernetes-security').status, 'partial'); assert.equal(courseById(structure, 'root-09-kubernetes-security').status, 'ready');
}); });
await test('the gold-standard Health Probes module\'s required chain is unchanged (Phase 6C added one optional lab)', async () => { await test('the gold-standard Health Probes module\'s required chain is unchanged (Phase 6C added one optional lab)', async () => {
@@ -131,9 +135,11 @@ async function main() {
// Phase 6D authored ROOT 01 as real, non-PLANNED content sequenced before ROOT 05 — it is // Phase 6D authored ROOT 01 as real, non-PLANNED content sequenced before ROOT 05 — it is
// now genuinely the program's front door (docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's // now genuinely the program's front door (docs/PHASE6D_ROOT_FOUNDATIONS.md). This test's
// original intent — resume must never land on a bare-lab bank when real front-door content // original intent — resume must never land on a bare-lab bank when real front-door content
// exists — is unchanged, just against the current front door. // exists — is unchanged, just against the current front door. Phase 6F then added a
// required orientation module in front of root-01-m01 — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`); const { body } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software'); assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
}); });
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 test('completing the real ROOT 01-04 foundations sequence unlocks ROOT 05 (Phase 6E test setup, not this file\'s own subject)', async () => {
@@ -197,6 +203,13 @@ 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-${suffix}/complete`);
} }
await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete'); await casey.request('POST', '/api/student/activities/root-06-m02-a06-reflection/complete');
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
// and its own prerequisite is root-06-m02 complete, so this must come right after — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
{
const { status } = await casey.request('POST', '/api/student/activities/root-06-m01-a00-lab-bank-intro/complete');
assert.equal(status, 200, 'completing root-06-m01-a00-lab-bank-intro should 200');
}
// Phase 6C upgraded root-06-m03 to the full gold pattern (added a01b/a02b/a05) — all four // 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 // 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. // to actually be complete (root-09's real prerequisite), not just its original two.
@@ -242,16 +255,20 @@ async function main() {
}); });
await test('a draft activity in a new module is hidden from students but visible to instructors', async () => { await test('a draft activity in a new module is hidden from students but visible to instructors', async () => {
psql(`UPDATE module_activities SET visibility = 'draft' WHERE activity_id = 'root-08-m01-a01-lesson-storage-fundamentals'`); // Phase 6F excluded ROOT 08 from V1 entirely (visibility='draft' on the whole course) — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md — so it can no longer demonstrate per-activity
// draft filtering (the whole module is already invisible to students regardless). Using
// root-09-m01 instead, still a real Phase 6B module and still published in V1.
psql(`UPDATE module_activities SET visibility = 'draft' WHERE activity_id = 'root-09-m01-a01-lesson-security-model'`);
try { try {
const { body: studentView } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`); const { body: studentView } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
const mod = moduleById(studentView.courses, 'root-08-m01-storage-fundamentals'); const mod = moduleById(studentView.courses, 'root-09-m01-security-model-and-securitycontext');
assert.ok(!mod.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals')); assert.ok(!mod.activities.some((a) => a.activity_id === 'root-09-m01-a01-lesson-security-model'));
const { body: adminView } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`); const { body: adminView } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
const modAdmin = moduleById(adminView, 'root-08-m01-storage-fundamentals'); const modAdmin = moduleById(adminView, 'root-09-m01-security-model-and-securitycontext');
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals')); assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-09-m01-a01-lesson-security-model'));
} finally { } finally {
psql(`UPDATE module_activities SET visibility = 'published' WHERE activity_id = 'root-08-m01-a01-lesson-storage-fundamentals'`); psql(`UPDATE module_activities SET visibility = 'published' WHERE activity_id = 'root-09-m01-a01-lesson-security-model'`);
} }
}); });
@@ -355,6 +372,13 @@ async function main() {
const { status } = await bypass.request('POST', `/api/student/activities/root-06-m02-${suffix}/complete`); 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`); assert.equal(status, 200, `completing root-06-m02-${suffix} should 200`);
} }
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
// and its own prerequisite is root-06-m02 complete, so it comes right after — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
{
const { status } = await bypass.request('POST', '/api/student/activities/root-06-m01-a00-lab-bank-intro/complete');
assert.equal(status, 200, 'completing root-06-m01-a00-lab-bank-intro should 200');
}
await completeScenario(bypass, 'job-task'); await completeScenario(bypass, 'job-task');
await completeScenario(bypass, 'cronjob-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']) { for (const suffix of ['a01-lesson-jobs-cronjobs-daemonsets', 'a01b-demonstration-job-lifecycle', 'a02-knowledge-check', 'a02b-guided-exercise-choosing-primitive', 'a05-reflection']) {
@@ -375,11 +399,63 @@ async function main() {
// total — ALL_STAGES (above) covers 5 of its 6 scenarios but predates container-logging-mcq. // total — ALL_STAGES (above) covers 5 of its 6 scenarios but predates container-logging-mcq.
await completeScenario(bypass, '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-10-m03-a06-reflection/complete');
// Phase 6F gave root-10-m02 (a bare practice bank) a real intro-lesson activity — required,
// and its own prerequisite is root-10-m03 complete (just finished above). Without this,
// root-10 the COURSE never reaches complete=true, which keeps root-12 the COURSE locked
// (its own prerequisite) even though root-12-m01 the MODULE's prerequisite (root-10-m01) is
// already satisfied — see docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
{
const { status } = await bypass.request('POST', '/api/student/activities/root-10-m02-a00-lab-bank-intro/complete');
assert.equal(status, 200, 'completing root-10-m02-a00-lab-bank-intro should 200');
}
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-a00-lesson-incident-response/complete');
await bypass.request('POST', '/api/student/activities/root-12-m01-a99-reflection-post-incident/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`); 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'); assert.equal(courseById(chk.courses, 'root-12-real-ops').complete, true, 'root-12 must be complete before the capstone course unlocks');
// Phase 6F (Step 7) added root-09-kubernetes-security as a second, joint course-level
// prerequisite for root-capstone alongside root-12 — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md. root-09-m03 also carries a real cross-course
// module prerequisite on root-07-m04 (Ingress & NetworkPolicy), which itself requires
// root-07-m03 complete — neither touched anywhere else in this file's bypass flow.
for (const suffix of ['a01-lesson-services', 'a02-demonstration-zero-endpoints', 'a03-knowledge-check', 'a04-guided-exercise-service-type', 'a06-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-07-m03-${suffix}/complete`);
assert.equal(status, 200, `completing root-07-m03-${suffix} should 200`);
}
for (const suffix of ['a01-lesson-ingress-networkpolicy', 'a01b-demonstration-ingress-routing', 'a02-knowledge-check', 'a02b-guided-exercise-network-isolation', 'a05-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-07-m04-${suffix}/complete`);
assert.equal(status, 200, `completing root-07-m04-${suffix} should 200`);
}
await completeScenario(bypass, 'ingress-task');
await completeScenario(bypass, 'network-policy');
for (const suffix of ['a01-lesson-security-model', 'a02-demonstration-securitycontext', 'a03-knowledge-check', 'a04-guided-exercise-hardening-plan', 'a09-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-09-m01-${suffix}/complete`);
assert.equal(status, 200, `completing root-09-m01-${suffix} should 200`);
}
for (const sid of ['cks-non-root-enforce', 'cks-drop-capabilities', 'cks-readonly-filesystem', 'cks-pod-security-context']) await completeScenario(bypass, sid);
for (const suffix of ['a01-lesson-serviceaccounts-rbac', 'a01b-demonstration-rbac-denial', 'a02-knowledge-check', 'a02b-guided-exercise-least-privilege-role', 'a09-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-09-m02-${suffix}/complete`);
assert.equal(status, 200, `completing root-09-m02-${suffix} should 200`);
}
for (const sid of ['serviceaccount-pod', 'cks-automount-token', 'rbac-role', 'cks-rbac-clusterrole', 'cks-rbac-least-privilege']) await completeScenario(bypass, sid);
for (const suffix of ['a01-lesson-network-supplychain', 'a01b-demonstration-default-deny', 'a02-knowledge-check', 'a02b-guided-exercise-egress-plan', 'a09-reflection']) {
const { status } = await bypass.request('POST', `/api/student/activities/root-09-m03-${suffix}/complete`);
assert.equal(status, 200, `completing root-09-m03-${suffix} should 200`);
}
for (const sid of ['cks-network-policy', 'cks-egress-namespace', 'cks-tls-ingress', 'cks-image-pull-secret']) await completeScenario(bypass, sid);
await bypass.request('POST', '/api/student/activities/root-09-m04-a01-lesson-cluster-hardening/complete');
for (const sid of ['cks-psa-namespace', 'cks-immutable-secret']) await completeScenario(bypass, sid);
{
const { body: startBody } = await bypass.request('POST', '/api/assessments/rootnode-security-assessment/start');
for (const s of startBody.scenarios) await completeScenario(bypass, s.scenario_id);
}
const { body: chk2 } = await bypass.request('GET', `/api/student/programs/${bypassEnroll.enrollment_id}/courses`);
assert.equal(courseById(chk2.courses, 'root-09-kubernetes-security').complete, true, 'root-09 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']) { 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`); 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`); assert.equal(status, 200, `completing root-capstone-m02-${suffix} should 200`);
+17 -4
View File
@@ -25,7 +25,9 @@ if (!OWNER_EMAIL || !OWNER_PASSWORD) {
} }
const RUN_ID = Date.now(); const RUN_ID = Date.now();
const PROGRAM = 'root-kubernetes-foundations'; const PROGRAM = 'root-kubernetes-foundations';
const ROOT01_MODULES = ['root-01-m01-how-computers-run-software', 'root-01-m02-linux-and-the-shell', 'root-01-m03-navigating-the-filesystem', 'root-01-m04-files-and-text', 'root-01-m05-processes', 'root-01-m06-users-and-permissions-foundations']; // Phase 6F prepended a required orientation module (root-01-m00-welcome-to-root) — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
const ROOT01_MODULES = ['root-01-m00-welcome-to-root', 'root-01-m01-how-computers-run-software', 'root-01-m02-linux-and-the-shell', 'root-01-m03-navigating-the-filesystem', 'root-01-m04-files-and-text', 'root-01-m05-processes', 'root-01-m06-users-and-permissions-foundations'];
const ROOT01_LAB_SCENARIOS = ['linux-inspect-hardware', 'linux-shell-basics', 'linux-filesystem-navigation', 'linux-files-and-text', 'linux-process-management', 'linux-permissions-foundations']; const ROOT01_LAB_SCENARIOS = ['linux-inspect-hardware', 'linux-shell-basics', 'linux-filesystem-navigation', 'linux-files-and-text', 'linux-process-management', 'linux-permissions-foundations'];
let passed = 0, failed = 0; let passed = 0, failed = 0;
@@ -99,11 +101,22 @@ async function main() {
// ── A real self-paced student walking ROOT 01 end to end ──────────────────────────────── // ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
const casey = await newStudent(owner, 'Casey'); const casey = await newStudent(owner, 'Casey');
await test('resume points at ROOT 01 module 1 activity 1, the very first activity in the whole program', async () => { await test('resume points at ROOT 01 module 0 activity 1, the very first activity in the whole program', async () => {
// Phase 6F added a required orientation module (root-01-m00-welcome-to-root) in front of
// what was previously the first module — see docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`); const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations'); assert.equal(body.resume.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.module_id, 'root-01-m01-how-computers-run-software'); assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson'); assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
});
await test('walking the ROOT_ V1 orientation module completes it and unlocks module 1', async () => {
await completeActivity(casey.session, 'root-01-m00-a01-lesson-welcome');
await completeActivity(casey.session, 'root-01-m00-a02-lesson-disposable-lab');
await completeActivity(casey.session, 'root-01-m00-a03-knowledge-check');
const { body } = await casey.session.request('GET', `/api/student/programs/${casey.enrollmentId}/courses`);
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true);
assert.equal(moduleById(body.courses, 'root-01-m01-how-computers-run-software').locked, false);
}); });
// ── 16: direct API cannot bypass — a lab activity cannot be manually marked complete ───── // ── 16: direct API cannot bypass — a lab activity cannot be manually marked complete ─────
+8 -2
View File
@@ -57,6 +57,10 @@ async function completeScenario(session, sid) {
// labs/reflections later would trip the module-lock check for later modules — a real ordering // labs/reflections later would trip the module-lock check for later modules — a real ordering
// constraint, not a test artifact. // constraint, not a test artifact.
async function completeRoot01(session) { async function completeRoot01(session) {
// Phase 6F — a required orientation module now gates root-01-m01, same pattern as root-05-m00.
await completeActivity(session, 'root-01-m00-a01-lesson-welcome');
await completeActivity(session, 'root-01-m00-a02-lesson-disposable-lab');
await completeActivity(session, 'root-01-m00-a03-knowledge-check');
await completeActivity(session, 'root-01-m01-a01-lesson'); await completeActivity(session, 'root-01-m01-a01-lesson');
await completeActivity(session, 'root-01-m01-a02-guided-exercise'); await completeActivity(session, 'root-01-m01-a02-guided-exercise');
await completeScenario(session, 'linux-inspect-hardware'); await completeScenario(session, 'linux-inspect-hardware');
@@ -231,10 +235,12 @@ async function main() {
// ── A real student walking the full ROOT 01->02->03->04 chain end to end ──────────────── // ── A real student walking the full ROOT 01->02->03->04 chain end to end ────────────────
const alice = await newStudent(owner, 'Alice'); const alice = await newStudent(owner, 'Alice');
await test('resume points at ROOT 01 module 1 activity 1 for a brand-new student', async () => { await test('resume points at ROOT 01 orientation module activity 1 for a brand-new student', async () => {
// Phase 6F added a required orientation module in front of root-01-m01 — see
// docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`); 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.course_id, 'root-01-computing-linux-foundations');
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson'); assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
}); });
// ── 30/26/27/28/29: direct API cannot bypass prerequisites (Section 11's real finding) ── // ── 30/26/27/28/29: direct API cannot bypass prerequisites (Section 11's real finding) ──
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env node
// Phase 6F — ROOT_ V1 Curriculum Integration & Pilot Readiness acceptance test. Covers the V1
// manifest/exclusion decisions, the full V1 prerequisite graph (including the new
// root-capstone/root-09 edge), self-paced/instructor-led eligibility, the orientation module,
// and the platform architecture's age-independence. Same conventions as e2e-phase6a-e.js:
// assert-based, no framework, live docker-compose.dev.yml stack.
//
// 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-phase6f.js
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const { completeRootFoundations01Through04, completeThroughRoot10, completeThroughCapstoneEligible } = require('./foundations-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 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-6F-${tag}-${RUN_ID}`, login_identifier: `e2e6f-${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 main() {
console.log('Phase 6F ROOT_ V1 Curriculum Integration & Pilot Readiness acceptance test\n');
const owner = makeSession(GW);
{ const r = await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD }); assert.equal(r.status, 200); }
// ── 1/2: the V1 manifest — inclusion/exclusion, statuses ─────────────────────────────────
let adminStructure;
await test('the V1 manifest\'s course statuses match the live database', async () => {
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
adminStructure = body;
for (const id of ['root-01-computing-linux-foundations', 'root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker', 'root-05-kubernetes-foundations', 'root-06-kubernetes-application-workloads', 'root-07-kubernetes-networking-config', 'root-09-kubernetes-security', 'root-10-kubernetes-troubleshooting-observability', 'root-12-real-ops', 'root-capstone']) {
assert.equal(courseById(adminStructure, id).status, 'ready', `${id} should be ready`);
}
assert.equal(courseById(adminStructure, 'root-08-kubernetes-storage-scheduling').status, 'partial');
assert.equal(courseById(adminStructure, 'root-11-devops-platform-engineering').status, 'partial');
});
await test('ROOT 08 is excluded from V1 (draft) but ROOT 11 stays published as a bonus module', async () => {
// buildCourseTree() never surfaces `visibility` on the shaped course object (it's a SQL
// WHERE-clause filter only, per course-progress.js) — verified directly against Postgres.
assert.equal(psql(`SELECT visibility FROM courses WHERE course_id = 'root-08-kubernetes-storage-scheduling'`), 'draft');
assert.equal(psql(`SELECT visibility FROM courses WHERE course_id = 'root-11-devops-platform-engineering'`), 'published');
});
const fresh = await newStudent(owner, 'Fresh');
await test('a brand-new student never sees ROOT 08 at all (visibility, not just locked)', async () => {
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-08-kubernetes-storage-scheduling'), undefined, 'a draft course must not appear in the student response');
assert.ok(courseById(body.courses, 'root-11-devops-platform-engineering'), 'ROOT 11 must still appear (published)');
});
// ── 3/4: prerequisite graph — no cycles, no missing nodes ────────────────────────────────
await test('the full V1 prerequisite graph has no cycles and no missing nodes (validator)', async () => {
const out = execSync('node infra/scripts/validate-course-content.js', { cwd: `${__dirname}/../..`, stdio: 'pipe' }).toString();
assert.ok(/PASS — 0 error\(s\)/.test(out), `expected 0 validation errors: ${out}`);
});
await test('ROOT 08 (excluded) is not referenced as a prerequisite by anything ELSE in V1', async () => {
for (const c of adminStructure) {
if (c.course_id === 'root-08-kubernetes-storage-scheduling') continue; // ROOT 08's own internal module chain referencing itself doesn't count
assert.ok(!(c.prerequisites || []).includes('root-08-kubernetes-storage-scheduling'), `${c.course_id} must not depend on excluded ROOT 08`);
for (const m of c.modules) assert.ok(!(m.prerequisites || []).some((p) => p.startsWith('root-08-')), `${m.module_id} must not depend on excluded ROOT 08`);
}
});
// ── 5/6: ROOT 01-04 and intended ROOT 05+ modules ready ──────────────────────────────────
await test('every ROOT 01-04 module is ready (re-verified, not inherited from Phase 6E)', async () => {
for (const c of ['root-01-computing-linux-foundations', 'root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
const course = courseById(adminStructure, c);
for (const m of course.modules) assert.equal(m.status, 'ready', `${m.module_id} should be ready`);
}
});
// ── orientation, resume, unlock ───────────────────────────────────────────────────────────
await test('resume points at the new orientation module for a brand-new student', async () => {
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
assert.equal(body.resume.module_id, 'root-01-m00-welcome-to-root');
assert.equal(body.resume.activity_id, 'root-01-m00-a01-lesson-welcome');
});
await test('completing the orientation module unlocks ROOT 01 module 1', async () => {
for (const id of ['root-01-m00-a01-lesson-welcome', 'root-01-m00-a02-lesson-disposable-lab', 'root-01-m00-a03-knowledge-check']) {
const r = await fresh.session.request('POST', `/api/student/activities/${id}/complete`);
assert.equal(r.status, 200);
}
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true);
assert.equal(moduleById(body.courses, 'root-01-m01-how-computers-run-software').locked, false);
});
// ── 11: optional practice never blocks (ROOT 09's advanced CKS labs) ─────────────────────
const optStudent = await newStudent(owner, 'OptionalCheck');
await test('an optional activity left undone never blocks its module or course from completing', async () => {
await completeThroughCapstoneEligible(optStudent.session);
const { body } = await optStudent.session.request('GET', `/api/student/programs/${optStudent.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-09-kubernetes-security').complete, true, 'root-09 complete despite its optional advanced labs never being touched');
assert.equal(courseById(body.courses, 'root-12-real-ops').complete, true);
});
// ── 14: Capstone gating — root-09 AND root-12 both required, direct API cannot bypass ────
await test('root-capstone stays locked with only root-12 done (root-09 also required)', async () => {
const s = await newStudent(owner, 'CapstoneGateA');
await completeThroughRoot10(s.session);
// root-12 only, no root-09:
async function act(id) { const r = await s.session.request('POST', `/api/student/activities/${id}/complete`); assert.equal(r.status, 200, id); }
async function scen(sid) { const r = await s.session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' }); assert.equal(r.status, 200, sid); }
await act('root-12-m01-a00-lesson-incident-response');
for (const sid of ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-enforce-limits']) await scen(sid);
await act('root-12-m01-a99-reflection-post-incident');
const { body } = await s.session.request('GET', `/api/student/programs/${s.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-12-real-ops').complete, true);
assert.equal(courseById(body.courses, 'root-capstone').locked, true, 'root-capstone must stay locked until root-09 is ALSO complete');
const bypass = await s.session.request('POST', '/api/student/activities/root-capstone-m02-a01-lesson-what-it-measures/complete');
assert.equal(bypass.status, 403);
assert.equal(bypass.body.error, 'locked');
});
await test('root-capstone unlocks once both root-09 and root-12 are complete', async () => {
const { body } = await optStudent.session.request('GET', `/api/student/programs/${optStudent.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-capstone').locked, false);
const r = await optStudent.session.request('POST', '/api/student/activities/root-capstone-m02-a01-lesson-what-it-measures/complete');
assert.equal(r.status, 200);
});
// ── 15: V1 version behavior — migrations bumped version, content unaffected ──────────────
await test('bumping a Phase 6F module\'s version again does not drop its content', async () => {
psql(`UPDATE course_modules SET version = version + 1 WHERE module_id = 'root-06-m01-deployments-and-workloads'`);
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
const mod = moduleById(body, 'root-06-m01-deployments-and-workloads');
assert.ok(mod.activities.some((a) => a.activity_id === 'root-06-m01-a00-lab-bank-intro'));
});
// ── 16: instructor notes ─────────────────────────────────────────────────────────────────
await test('instructor notes on the new orientation and intro-lesson content are instructor-only', async () => {
const { body: adminView } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
const lesson = moduleById(adminView, 'root-01-m00-welcome-to-root').activities.find((a) => a.activity_id === 'root-01-m00-a02-lesson-disposable-lab');
assert.ok(lesson.instructor_notes === null || lesson.instructor_notes === undefined || lesson.instructor_notes === '', 'this particular lesson has no Instructor Notes section, which is fine — it must never be fabricated');
const bank = moduleById(adminView, 'root-06-m01-deployments-and-workloads').activities.find((a) => a.activity_id === 'root-06-m01-a00-lab-bank-intro');
assert.ok(bank.instructor_notes && bank.instructor_notes.includes('Instructor Notes'));
// root-06-m01 is LOCKED for fresh (not yet unlocked), not DRAFT — a locked module still
// appears in the tree so a student can see what's ahead, it just never carries
// instructor_notes regardless of lock state.
const { body: studentView } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
const studentBank = moduleById(studentView.courses, 'root-06-m01-deployments-and-workloads');
const studentBankActivity = studentBank.activities.find((a) => a.activity_id === 'root-06-m01-a00-lab-bank-intro');
assert.ok(!studentBankActivity.instructor_notes, 'a student must never receive instructor_notes, locked or not');
});
// ── 17/18: isolation, enrollment ──────────────────────────────────────────────────────────
await test('two students on ROOT_ V1 have fully independent progress', async () => {
const a = await newStudent(owner, 'IsoA');
const b = await newStudent(owner, 'IsoB');
await a.session.request('POST', '/api/student/activities/root-01-m00-a01-lesson-welcome/complete');
const [{ body: aBody }, { body: bBody }] = await Promise.all([
a.session.request('GET', `/api/student/programs/${a.enrollmentId}/courses`),
b.session.request('GET', `/api/student/programs/${b.enrollmentId}/courses`),
]);
assert.notEqual(moduleById(aBody.courses, 'root-01-m00-welcome-to-root').percent_complete, moduleById(bBody.courses, 'root-01-m00-welcome-to-root').percent_complete);
});
await test('a fresh V1 enrollment sees the complete intended course list with no gaps', async () => {
const { body } = await fresh.session.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
const ids = body.courses.map((c) => c.course_id);
for (const id of ['root-01-computing-linux-foundations', 'root-05-kubernetes-foundations', 'root-09-kubernetes-security', 'root-11-devops-platform-engineering', 'root-12-real-ops', 'root-capstone']) {
assert.ok(ids.includes(id), `${id} should be visible to a self-paced V1 student`);
}
assert.ok(!ids.includes('root-08-kubernetes-storage-scheduling'));
});
// ── 19: Lab lifecycle persistence (logout/login) ─────────────────────────────────────────
await test('V1 progress survives logout and a fresh login', async () => {
await fresh.session.request('POST', '/api/logout');
const returning = makeSession(GW);
await returning.request('POST', '/api/student/login', { identifier: `e2e6f-Fresh-${RUN_ID}`, password: 'FreshPassword123!' });
const { body } = await returning.request('GET', `/api/student/programs/${fresh.enrollmentId}/courses`);
assert.equal(moduleById(body.courses, 'root-01-m00-welcome-to-root').complete, true);
});
// ── 22: age-independent architecture — enrollment never requires or stores an age field ──
await test('enrollment succeeds with no age/grade field anywhere in the request or student record', async () => {
const { body: create } = await owner.request('POST', '/api/admin/students', { display_name: `E2E-6F-NoAge-${RUN_ID}`, login_identifier: `e2e6f-noage-${RUN_ID}` });
assert.ok(create.student_id);
const cols = psql(`SELECT column_name FROM information_schema.columns WHERE table_name = 'students' AND column_name ILIKE '%age%'`);
assert.equal(cols, '', 'the students table must not carry an age/grade gate column');
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
}
main().catch((e) => { console.error(e); process.exit(1); });
+41 -1
View File
@@ -15,6 +15,9 @@ async function completeRootFoundations01Through04(session) {
if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`); if (r.status !== 200) throw new Error(`completing scenario ${sid} failed: ${r.status}`);
} }
// Phase 6F — the ROOT_ V1 orientation module (Steps 9-11) is a real, required front-door
// module now gating root-01-m01, same as root-05-m00 already gates root-05-m01.
await act('root-01-m00-a01-lesson-welcome'); await act('root-01-m00-a02-lesson-disposable-lab'); await act('root-01-m00-a03-knowledge-check');
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'); 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' }; 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`); } 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`); }
@@ -64,6 +67,9 @@ async function completeThroughRoot06(session) {
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); 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'); 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');
// Phase 6F gave root-06-m01 (a bare practice bank) a real intro-lesson activity — required,
// prerequisite root-06-m02 complete (just finished above).
await act('root-06-m01-a00-lab-bank-intro');
for (const sid of ['job-task', 'cronjob-task']) await scen(sid); 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'); 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');
} }
@@ -79,7 +85,41 @@ async function completeThroughRoot10(session) {
await scen('rootnode-probe-port-mismatch'); await act('root-10-m01-a06-reflection-real-ops-takeaway'); 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 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'); await scen('crashloop-fix'); await scen('endpoint-fix-task'); await act('root-10-m03-a06-reflection');
// Phase 6F gave root-10-m02 (a bare practice bank) a real intro-lesson activity — required,
// prerequisite root-10-m03 complete (just finished above).
await act('root-10-m02-a00-lab-bank-intro');
for (const sid of ['extract-logs-basics', 'kubectl-cp-basics', 'exec-command-basics', 'node-cordon-basics', 'pod-node-assignment-mcq', 'container-logging-mcq']) await scen(sid); 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 }; // Phase 6F — completes through ROOT 10, then ROOT 07 (m03/m04, needed by root-09-m03's
// cross-course prerequisite), ROOT 09 (all 4 modules + its assessment), and ROOT 12, leaving
// the student's root-capstone COURSE genuinely unlocked (both its real prerequisites —
// root-12-real-ops and root-09-kubernetes-security — satisfied) without touching the Capstone
// itself. See docs/ROOT_V1_CURRICULUM_MANIFEST.md Step 7 for why root-09 is now required.
async function completeThroughCapstoneEligible(session) {
await completeThroughRoot10(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}`); }
for (const suffix of ['a01-lesson-services', 'a02-demonstration-zero-endpoints', 'a03-knowledge-check', 'a04-guided-exercise-service-type', 'a06-reflection']) await act(`root-07-m03-${suffix}`);
await scen('expose-service-basics'); // root-07-m03's own lab — its OWN prerequisite for root-07-m04, easy to miss since ROOT 07's practice bank reuses the same scenario id.
for (const suffix of ['a01-lesson-ingress-networkpolicy', 'a01b-demonstration-ingress-routing', 'a02-knowledge-check', 'a02b-guided-exercise-network-isolation', 'a05-reflection']) await act(`root-07-m04-${suffix}`);
await scen('ingress-task'); await scen('network-policy');
for (const suffix of ['a01-lesson-security-model', 'a02-demonstration-securitycontext', 'a03-knowledge-check', 'a04-guided-exercise-hardening-plan', 'a09-reflection']) await act(`root-09-m01-${suffix}`);
for (const sid of ['cks-non-root-enforce', 'cks-drop-capabilities', 'cks-readonly-filesystem', 'cks-pod-security-context']) await scen(sid);
for (const suffix of ['a01-lesson-serviceaccounts-rbac', 'a01b-demonstration-rbac-denial', 'a02-knowledge-check', 'a02b-guided-exercise-least-privilege-role', 'a09-reflection']) await act(`root-09-m02-${suffix}`);
for (const sid of ['serviceaccount-pod', 'cks-automount-token', 'rbac-role', 'cks-rbac-clusterrole', 'cks-rbac-least-privilege']) await scen(sid);
for (const suffix of ['a01-lesson-network-supplychain', 'a01b-demonstration-default-deny', 'a02-knowledge-check', 'a02b-guided-exercise-egress-plan', 'a09-reflection']) await act(`root-09-m03-${suffix}`);
for (const sid of ['cks-network-policy', 'cks-egress-namespace', 'cks-tls-ingress', 'cks-image-pull-secret']) await scen(sid);
await act('root-09-m04-a01-lesson-cluster-hardening');
for (const sid of ['cks-psa-namespace', 'cks-immutable-secret']) await scen(sid);
const r = await session.request('POST', '/api/assessments/rootnode-security-assessment/start');
for (const s of r.body.scenarios) await scen(s.scenario_id);
await act('root-12-m01-a00-lesson-incident-response');
for (const sid of ['rootnode-runaway-monitor', 'broken-deployment', 'rootnode-enforce-limits']) await scen(sid);
await act('root-12-m01-a99-reflection-post-incident');
}
module.exports = { completeRootFoundations01Through04, completeThroughRoot06, completeThroughRoot10, completeThroughCapstoneEligible };