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:
@@ -59,9 +59,18 @@ async function main() {
|
||||
});
|
||||
|
||||
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`);
|
||||
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 () => {
|
||||
|
||||
@@ -78,6 +78,9 @@ async function main() {
|
||||
|
||||
// ── 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 () => {
|
||||
// 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}`);
|
||||
await completeScenario(session, 'linux-inspect-hardware', envId);
|
||||
await completeActivity(session, 'root-01-m01-a04-reflection');
|
||||
|
||||
@@ -89,7 +89,10 @@ async function main() {
|
||||
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');
|
||||
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 () => {
|
||||
@@ -143,10 +146,12 @@ async function main() {
|
||||
// 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 —
|
||||
// "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.module_id, 'root-01-m01-how-computers-run-software');
|
||||
assert.equal(body.resume.activity_id, 'root-01-m01-a01-lesson');
|
||||
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('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, 'deployment-rollback');
|
||||
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']) {
|
||||
await casey.request('POST', `/api/student/activities/root-06-m03-${suffix}/complete`);
|
||||
}
|
||||
|
||||
@@ -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-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 () => {
|
||||
@@ -131,9 +135,11 @@ async function main() {
|
||||
// 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
|
||||
// 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`);
|
||||
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 () => {
|
||||
@@ -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-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
|
||||
// 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.
|
||||
@@ -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 () => {
|
||||
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 {
|
||||
const { body: studentView } = await casey.request('GET', `/api/student/programs/${enrollmentId}/courses`);
|
||||
const mod = moduleById(studentView.courses, 'root-08-m01-storage-fundamentals');
|
||||
assert.ok(!mod.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals'));
|
||||
const mod = moduleById(studentView.courses, 'root-09-m01-security-model-and-securitycontext');
|
||||
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 modAdmin = moduleById(adminView, 'root-08-m01-storage-fundamentals');
|
||||
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-08-m01-a01-lesson-storage-fundamentals'));
|
||||
const modAdmin = moduleById(adminView, 'root-09-m01-security-model-and-securitycontext');
|
||||
assert.ok(modAdmin.activities.some((a) => a.activity_id === 'root-09-m01-a01-lesson-security-model'));
|
||||
} 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`);
|
||||
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, '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']) {
|
||||
@@ -375,11 +399,63 @@ async function main() {
|
||||
// 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');
|
||||
// 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-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');
|
||||
|
||||
// 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']) {
|
||||
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`);
|
||||
|
||||
@@ -25,7 +25,9 @@ if (!OWNER_EMAIL || !OWNER_PASSWORD) {
|
||||
}
|
||||
const RUN_ID = Date.now();
|
||||
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'];
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
@@ -99,11 +101,22 @@ async function main() {
|
||||
// ── A real self-paced student walking ROOT 01 end to end ────────────────────────────────
|
||||
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`);
|
||||
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.activity_id, 'root-01-m01-a01-lesson');
|
||||
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('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 ─────
|
||||
|
||||
@@ -57,6 +57,10 @@ async function completeScenario(session, sid) {
|
||||
// labs/reflections later would trip the module-lock check for later modules — a real ordering
|
||||
// constraint, not a test artifact.
|
||||
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-a02-guided-exercise');
|
||||
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 ────────────────
|
||||
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`);
|
||||
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) ──
|
||||
|
||||
@@ -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); });
|
||||
@@ -15,6 +15,9 @@ async function completeRootFoundations01Through04(session) {
|
||||
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');
|
||||
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`); }
|
||||
@@ -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);
|
||||
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);
|
||||
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 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');
|
||||
// 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);
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user