Files
ced4568andClaude Sonnet 5 2864fe03dc 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]>
2026-08-26 18:33:21 -05:00

384 lines
26 KiB
JavaScript

#!/usr/bin/env node
// Phase 6E — ROOT Foundations Completion acceptance test. Covers ROOT 02 (Linux Administration
// & Networking), ROOT 03 (Git & Collaborative Development), ROOT 04 (Containers & Docker), and
// the now-complete ROOT01->02->03->04->05 prerequisite chain. Same conventions as
// e2e-phase6a/6b/6c/6d.js: assert-based, no framework, live docker-compose.dev.yml stack, no
// real Lab container launched here (see infra/scripts/drills/test-phase6e-foundations-rehearsal.js
// for the real-Lab/real-Docker/real-Git counterpart).
//
// 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-phase6e.js
const assert = require('assert');
const { execSync } = require('child_process');
const { makeSession } = require('./session-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const OWNER_EMAIL = process.env.OWNER_EMAIL;
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (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 findActivity(courses, id) { for (const c of courses) for (const m of c.modules) { const a = m.activities.find((x) => x.activity_id === id); if (a) return a; } 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-6E-${tag}-${RUN_ID}`, login_identifier: `e2e6e-${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 completeActivity(session, id) {
const r = await session.request('POST', `/api/student/activities/${id}/complete`);
assert.equal(r.status, 200, `completing ${id} should 200: ${JSON.stringify(r.body)}`);
}
async function completeScenario(session, sid) {
const r = await session.request('POST', '/api/progress/record', { scenario_id: sid, status: 'completed' });
assert.equal(r.status, 200, `completing scenario ${sid} should 200`);
}
// NOTE: each course's modules carry real intra-course prerequisites (m02 requires m01 complete,
// etc — see migrations 025-028), so every helper below completes ONE module fully (including
// its lab scenario(s) and reflection) before touching the next, mirroring how a real student
// must actually progress. Completing all modules' early activities first and circling back for
// 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');
await completeActivity(session, 'root-01-m01-a04-reflection');
const labs = { m02: 'linux-shell-basics', m03: 'linux-filesystem-navigation', m04: 'linux-files-and-text', m05: 'linux-process-management', m06: 'linux-permissions-foundations' };
for (const m of ['m02', 'm03', 'm04', 'm05', 'm06']) {
await completeActivity(session, `root-01-${m}-a01-lesson`);
await completeActivity(session, `root-01-${m}-a02-demonstration`);
await completeActivity(session, `root-01-${m}-a03-knowledge-check`);
await completeActivity(session, `root-01-${m}-a04-guided-exercise`);
await completeScenario(session, labs[m]);
await completeActivity(session, `root-01-${m}-a06-reflection`);
}
const r1 = await session.request('POST', '/api/assessments/rootnode-linux-foundations-assessment/start');
for (const sc of r1.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function completeRoot02(session) {
await completeActivity(session, 'root-02-m01-a01-lesson');
await completeActivity(session, 'root-02-m01-a02-demonstration');
await completeActivity(session, 'root-02-m01-a03-knowledge-check');
await completeActivity(session, 'root-02-m01-a04-guided-exercise');
await completeScenario(session, 'linux-service-inspection');
await completeScenario(session, 'linux-service-lifecycle');
await completeActivity(session, 'root-02-m01-a07-reflection');
await completeActivity(session, 'root-02-m02-a01-lesson');
await completeActivity(session, 'root-02-m02-a02-demonstration');
await completeActivity(session, 'root-02-m02-a03-knowledge-check');
await completeActivity(session, 'root-02-m02-a04-guided-exercise');
await completeScenario(session, 'linux-storage-inspection');
await completeActivity(session, 'root-02-m02-a06-reflection');
await completeActivity(session, 'root-02-m03-a01-lesson');
await completeActivity(session, 'root-02-m03-a02-demonstration');
await completeActivity(session, 'root-02-m03-a03-knowledge-check');
await completeActivity(session, 'root-02-m03-a04-guided-exercise');
await completeActivity(session, 'root-02-m03-a05-reflection');
await completeActivity(session, 'root-02-m04-a01-lesson');
await completeActivity(session, 'root-02-m04-a02-demonstration');
await completeActivity(session, 'root-02-m04-a03-knowledge-check');
await completeActivity(session, 'root-02-m04-a04-guided-exercise');
await completeScenario(session, 'linux-network-inspection');
await completeActivity(session, 'root-02-m04-a06-reflection');
await completeActivity(session, 'root-02-m05-a01-lesson');
await completeActivity(session, 'root-02-m05-a02-demonstration');
await completeActivity(session, 'root-02-m05-a03-knowledge-check');
await completeActivity(session, 'root-02-m05-a04-guided-exercise');
await completeScenario(session, 'linux-dns-http-diagnosis');
await completeActivity(session, 'root-02-m05-a06-reflection');
await completeActivity(session, 'root-02-m06-a01-lesson');
await completeActivity(session, 'root-02-m06-a02-demonstration');
await completeActivity(session, 'root-02-m06-a03-knowledge-check');
await completeActivity(session, 'root-02-m06-a04-guided-exercise');
await completeScenario(session, 'linux-troubleshooting-framework');
await completeActivity(session, 'root-02-m06-a06-reflection');
const r2 = await session.request('POST', '/api/assessments/rootnode-linux-networking-assessment/start');
for (const sc of r2.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function completeRoot03(session) {
await completeActivity(session, 'root-03-m01-a01-lesson');
await completeActivity(session, 'root-03-m01-a02-knowledge-check');
await completeActivity(session, 'root-03-m01-a03-reflection');
await completeActivity(session, 'root-03-m02-a01-lesson');
await completeActivity(session, 'root-03-m02-a02-demonstration');
await completeActivity(session, 'root-03-m02-a03-knowledge-check');
await completeActivity(session, 'root-03-m02-a04-guided-exercise');
await completeActivity(session, 'root-03-m02-a05-reflection');
await completeActivity(session, 'root-03-m03-a01-lesson');
await completeActivity(session, 'root-03-m03-a02-demonstration');
await completeActivity(session, 'root-03-m03-a03-knowledge-check');
await completeActivity(session, 'root-03-m03-a04-guided-exercise');
await completeScenario(session, 'git-everyday-basics');
await completeActivity(session, 'root-03-m03-a06-reflection');
await completeActivity(session, 'root-03-m04-a01-lesson');
await completeActivity(session, 'root-03-m04-a02-demonstration');
await completeActivity(session, 'root-03-m04-a03-knowledge-check');
await completeActivity(session, 'root-03-m04-a04-guided-exercise');
await completeScenario(session, 'git-branching-and-merge');
await completeActivity(session, 'root-03-m04-a06-reflection');
await completeActivity(session, 'root-03-m05-a01-lesson');
await completeActivity(session, 'root-03-m05-a02-demonstration');
await completeActivity(session, 'root-03-m05-a03-knowledge-check');
await completeActivity(session, 'root-03-m05-a04-guided-exercise');
await completeScenario(session, 'git-remote-clone-push');
await completeActivity(session, 'root-03-m05-a06-reflection');
await completeActivity(session, 'root-03-m06-a01-lesson');
await completeActivity(session, 'root-03-m06-a02-demonstration');
await completeActivity(session, 'root-03-m06-a03-knowledge-check');
await completeActivity(session, 'root-03-m06-a04-reflection');
const r3 = await session.request('POST', '/api/assessments/rootnode-git-fundamentals-assessment/start');
for (const sc of r3.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function completeRoot04(session) {
await completeActivity(session, 'root-04-m01-a01-lesson');
await completeActivity(session, 'root-04-m01-a02-demonstration');
await completeActivity(session, 'root-04-m01-a03-knowledge-check');
await completeActivity(session, 'root-04-m01-a04-reflection');
await completeActivity(session, 'root-04-m02-a01-lesson');
await completeActivity(session, 'root-04-m02-a02-demonstration');
await completeActivity(session, 'root-04-m02-a03-knowledge-check');
await completeActivity(session, 'root-04-m02-a04-guided-exercise');
await completeActivity(session, 'root-04-m02-a05-reflection');
const labs4 = { m03: 'docker-cli-basics', m04: 'docker-container-networking', m05: 'docker-container-storage', m06: 'docker-build-image', m07: 'docker-compose-multiservice' };
for (const m of ['m03', 'm04', 'm05', 'm06', 'm07']) {
await completeActivity(session, `root-04-${m}-a01-lesson`);
await completeActivity(session, `root-04-${m}-a02-demonstration`);
await completeActivity(session, `root-04-${m}-a03-knowledge-check`);
await completeActivity(session, `root-04-${m}-a04-guided-exercise`);
await completeScenario(session, labs4[m]);
await completeActivity(session, `root-04-${m}-a06-reflection`);
}
await completeActivity(session, 'root-04-m08-a01-lesson');
await completeActivity(session, 'root-04-m08-a02-knowledge-check');
await completeActivity(session, 'root-04-m08-a03-reflection');
const r4 = await session.request('POST', '/api/assessments/rootnode-containers-assessment/start');
for (const sc of r4.body.scenarios) await completeScenario(session, sc.scenario_id);
}
async function main() {
console.log('Phase 6E ROOT Foundations Completion acceptance test\n');
const owner = makeSession(GW);
await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
let structure;
// ── 1/8/15: hierarchy for ROOT 02/03/04 ─────────────────────────────────────────────────
await test('ROOT 02/03/04 each have their full designed module set, in sequence, all READY', async () => {
const { status, body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
assert.equal(status, 200);
structure = body;
const expected = {
'root-02-linux-administration-networking': ['root-02-m01-linux-system-administration', 'root-02-m02-storage-and-filesystems', 'root-02-m03-networking-mental-model', 'root-02-m04-linux-network-inspection', 'root-02-m05-dns-and-http', 'root-02-m06-linux-troubleshooting'],
'root-03-git-collaborative-development': ['root-03-m01-why-version-control-exists', 'root-03-m02-git-mental-model', 'root-03-m03-everyday-git', 'root-03-m04-branching-and-merging', 'root-03-m05-remote-repositories', 'root-03-m06-engineering-git-workflow'],
'root-04-containers-and-docker': ['root-04-m01-why-containers-exist', 'root-04-m02-images-and-containers', 'root-04-m03-docker-cli', 'root-04-m04-container-networking', 'root-04-m05-container-storage', 'root-04-m06-dockerfile-and-image-build', 'root-04-m07-docker-compose', 'root-04-m08-containers-to-kubernetes'],
};
for (const [courseId, moduleIds] of Object.entries(expected)) {
const course = courseById(structure, courseId);
assert.equal(course.status, 'ready', `${courseId} must be ready`);
assert.deepEqual(course.modules.map((m) => m.module_id), moduleIds, `${courseId} module order mismatch`);
for (const m of course.modules) assert.equal(m.status, 'ready', `${m.module_id} must be ready`);
}
});
// ── 2/9/16: content references real ────────────────────────────────────────────────────
await test('every ROOT 02/03/04 module has real, loaded lesson content', async () => {
for (const modId of ['root-02-m03-networking-mental-model', 'root-03-m02-git-mental-model', 'root-04-m01-why-containers-exist']) {
const mod = moduleById(structure, modId);
const lesson = mod.activities.find((a) => a.activity_type === 'lesson');
assert.ok(lesson.body && lesson.body.length > 300, `${modId}'s lesson must have real content`);
}
});
// ── 4/5/6/7: ROOT 02 practicals, networking, troubleshooting, assessment ────────────────
await test('ROOT 02 assessment pool matches the documented design (2 required + 4 random)', async () => {
const spec = JSON.parse(psql(`SELECT pool_spec FROM assessment_definitions WHERE assessment_definition_id = 'rootnode-linux-networking-assessment'`));
assert.deepEqual(spec.required, ['linux-network-inspection', 'linux-troubleshooting-framework']);
assert.equal(spec.random.reduce((n, g) => n + g.count, 0), 4);
});
// ── 10/11/12/13: ROOT 03 Git — real repository state validation ─────────────────────────
await test('ROOT 03 assessment pool matches the documented design (2 required + 3 random)', async () => {
const spec = JSON.parse(psql(`SELECT pool_spec FROM assessment_definitions WHERE assessment_definition_id = 'rootnode-git-fundamentals-assessment'`));
assert.deepEqual(spec.required, ['git-everyday-basics', 'git-branching-and-merge']);
assert.equal(spec.random.reduce((n, g) => n + g.count, 0), 3);
});
// ── 16/22/23/24/25: ROOT 04 nested Docker, Compose ──────────────────────────────────────
await test('ROOT 04 assessment pool matches the documented design (2 required + 4 random)', async () => {
const spec = JSON.parse(psql(`SELECT pool_spec FROM assessment_definitions WHERE assessment_definition_id = 'rootnode-containers-assessment'`));
assert.deepEqual(spec.required, ['docker-cli-basics', 'docker-build-image']);
assert.equal(spec.random.reduce((n, g) => n + g.count, 0), 4);
});
// ── 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 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-m00-a01-lesson-welcome');
});
// ── 30/26/27/28/29: direct API cannot bypass prerequisites (Section 11's real finding) ──
await test('direct API cannot mark a ROOT 02 activity complete while ROOT 01 is incomplete (found and fixed this phase)', async () => {
const r = await alice.session.request('POST', '/api/student/activities/root-02-m01-a01-lesson/complete');
assert.equal(r.status, 403);
assert.equal(r.body.error, 'locked');
});
await test('direct API cannot mark a ROOT 06 activity complete while ROOT 05 is incomplete (pre-existing gap, same fix)', async () => {
const r = await alice.session.request('POST', '/api/student/activities/root-06-m02-a01-lesson-deployments/complete');
assert.equal(r.status, 403);
assert.equal(r.body.error, 'locked');
});
await test('ROOT 01 completes and unlocks ROOT 02, while ROOT 03/04/05 remain locked (real content now gates them)', async () => {
await completeRoot01(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, true);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, false);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, true, 'ROOT 03 now genuinely gates on ROOT 02 having real content');
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
});
await test('ROOT 02 completes and unlocks ROOT 03, not ROOT 04 yet', async () => {
await completeRoot02(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').complete, true);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, false);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
});
await test('ROOT 03 completes and unlocks ROOT 04, not ROOT 05 yet', async () => {
await completeRoot03(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').complete, true);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, false);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, true);
});
await test('ROOT 04 completes and unlocks ROOT 05 — the complete foundations chain is proven', async () => {
await completeRoot04(alice.session);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').complete, true);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, false);
});
// ── 39: existing ROOT 05+ content remains intact ────────────────────────────────────────
await test('ROOT 06 stays locked behind ROOT 05 exactly as before (unaffected by the foundations chain)', async () => {
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-06-kubernetes-application-workloads').locked, true);
});
// ── 40: Capstone behavior unchanged ─────────────────────────────────────────────────────
await test('the capstone unlock gate is unaffected by the new foundations chain', async () => {
const r = await alice.session.request('POST', '/api/assessments/rootnode-capstone/start');
assert.equal(r.status, 403);
assert.equal(r.body.error, 'locked');
});
// ── 31: optional work does not block (is_required still generalizes) ───────────────────
await test('the pre-existing is_required rule is unaffected by this phase', async () => {
const mod = moduleById(structure, 'root-10-m01-health-probes-and-troubleshooting');
assert.equal(mod.activities.filter((a) => a.is_required === false).length, 1);
});
// ── 18/19: self-paced content never defers to a live instructor ────────────────────────
await test('self-paced ROOT 02/03/04 content never defers explanation to a live instructor', async () => {
for (const courseId of ['root-02-linux-administration-networking', 'root-03-git-collaborative-development', 'root-04-containers-and-docker']) {
const course = courseById(structure, courseId);
for (const mod of course.modules) for (const a of mod.activities) {
if (!a.body) continue;
assert.ok(!/ask your instructor/i.test(a.body) && !/your instructor will explain/i.test(a.body), `${a.activity_id} must not defer to a live instructor`);
}
}
});
// ── 20/36: instructor notes hidden from students, visible to instructors ───────────────
await test('instructor notes on ROOT 02/03/04 content are visible to instructors and never to students', async () => {
const lesson = findActivity(structure, 'root-03-m02-a01-lesson');
assert.ok(lesson.instructor_notes && lesson.instructor_notes.length > 0);
const { body } = await alice.session.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
const studentLesson = findActivity(body.courses, 'root-03-m02-a01-lesson');
assert.equal(studentLesson.instructor_notes, undefined);
});
// ── 37: instructor RBAC ─────────────────────────────────────────────────────────────────
const { body: strayInstructor } = await owner.request('POST', '/api/admin/instructors', { email: `e2e6e-stray-${RUN_ID}@example.com`, display_name: 'Stray Instructor 6E', role: 'instructor', password: 'StrayInstructorPass6E!' });
const stray = makeSession(GW);
await stray.request('POST', '/api/auth/instructor/login', { email: `e2e6e-stray-${RUN_ID}@example.com`, password: 'StrayInstructorPass6E!' });
await test('a plain instructor with no relationship to the student cannot see their foundations progress', async () => {
const { status } = await stray.request('GET', `/api/admin/students/${alice.studentId}/courses?program_id=${PROGRAM}`);
assert.equal(status, 403);
});
await test('owner/admin can see any self-paced student\'s complete foundations progress', async () => {
const { status, body } = await owner.request('GET', `/api/admin/students/${alice.studentId}/courses?program_id=${PROGRAM}`);
assert.equal(status, 200);
assert.equal(courseById(body, 'root-04-containers-and-docker').complete, true);
});
// ── 33/34: progress persistence across logout/login ─────────────────────────────────────
await test('foundations progress survives logout and a fresh login', async () => {
await alice.session.request('POST', '/api/logout');
const returning = makeSession(GW);
await returning.request('POST', '/api/student/login', { identifier: `e2e6e-Alice-${RUN_ID}`, password: 'AlicePassword123!' });
const { body } = await returning.request('GET', `/api/student/programs/${alice.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').complete, true);
});
// ── 35: two students isolated ───────────────────────────────────────────────────────────
const bob = await newStudent(owner, 'Bob');
await test('a second, fresh student sees independent, empty foundations progress and a fully-locked chain', async () => {
const { body } = await bob.session.request('GET', `/api/student/programs/${bob.enrollmentId}/courses`);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').complete, false);
assert.equal(courseById(body.courses, 'root-01-computing-linux-foundations').locked, false);
assert.equal(courseById(body.courses, 'root-02-linux-administration-networking').locked, true);
assert.equal(courseById(body.courses, 'root-03-git-collaborative-development').locked, true);
assert.equal(courseById(body.courses, 'root-04-containers-and-docker').locked, true);
assert.equal(courseById(body.courses, 'root-05-kubernetes-foundations').locked, true);
});
// ── content version stability ────────────────────────────────────────────────────────────
await test('bumping a ROOT 03 module\'s version does not drop its content', async () => {
psql(`UPDATE course_modules SET version = version + 1 WHERE module_id = 'root-03-m04-branching-and-merging'`);
const { body } = await owner.request('GET', `/api/admin/programs/${PROGRAM}/courses`);
const mod = moduleById(body, 'root-03-m04-branching-and-merging');
assert.equal(mod.activities.length, 6);
assert.ok(mod.activities[0].body && mod.activities[0].body.length > 300);
});
// ── content validation as part of this suite ────────────────────────────────────────────
await test('infra/scripts/validate-course-content.js reports zero errors against the current database', () => {
execSync('node infra/scripts/validate-course-content.js', { cwd: `${__dirname}/../..`, stdio: 'pipe' });
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
}
main().catch((e) => { console.error('FATAL', e); process.exit(1); });