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