- e2e-phase7.js (17/17): organizations/locations, guardians, curriculum packages
(reference-only), offering capacity/transitions, registration lifecycle, agreements,
registration-to-enrollment conversion, RBAC, age-guidance-independence, and confirmation
that existing ROOT_ V1 self-paced/classroom flows are unaffected.
- test-phase7-winter-camp.js (18/18): the full Winter Break Camp operational path with NO
payment code invoked — organization/location/curriculum-package/cohort/offering setup,
guardian + adult registration, agreement acceptance, a simulated admin-side commerce
confirmation, conversion to real enrollments and real cohort membership, real account
activation via real one-time tokens, a real classroom session, and a REAL Lab task
(linux-inspect-hardware) completed by the minor student inside a real disposable Lab
container, confirmed to survive a real logout/login.
Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32 (including its own embedded Phase 5A
checks) on independent fresh stacks.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- 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]>
Adds platform/test/e2e-phase6e.js (23 checks): full ROOT 02/03/04 hierarchy/content/assessment-
pool verification, the complete ROOT 01->02->03->04->05 chain proven on one student (each
course's completion unlocking exactly the next), the direct-API lock bypass proven blocked on
both a Phase 6E course pair and a pre-existing unrelated pair, existing ROOT 06+/Capstone/
is_required behavior confirmed unaffected, self-paced no-instructor-deferral scan,
instructor-notes exclusion, RBAC, persistence, isolation, version stability, content validation.
23/23 pass against both a warm and a freshly-reset stack.
Adds platform/test/foundations-helper.js — a shared completeRootFoundations01Through04/
completeThroughRoot06/completeThroughRoot10 helper, extracted once multiple suites needed the
identical real-completion sequence as test setup, rather than duplicating it four times.
Adds infra/scripts/drills/{run,test}-phase6e-foundations-rehearsal — the real Platform API/
Gateway/Provisioner/disposable-Lab/nested-Docker/Git/k3s dress rehearsal: one real Lab task per
new course (nginx inspection, a real merge conflict, a real container-networking fix) plus the
full chain to a genuine ROOT 05 unlock, with k3s reconfirmed healthy and untouched at the end.
13/13 pass against a real, freshly-built stack.
Fixes the regression fallout from the (correct) course-routes.js lock fix: several earlier
suites' own historical shortcuts had accumulated small ordering/scenario gaps the missing
enforcement had been silently covering for — none were platform bugs. e2e-phase6a/6b/6c/6d.js
now inject the real ROOT 01-04 (and where needed ROOT 05/06/10) completion sequence via
foundations-helper.js before touching ROOT 05+ content; e2e-phase6b.js's bypass-student flow now
completes deployment-rollback/job-task/cronjob-task/container-logging-mcq explicitly (real
scenarios added in later phases that its original ALL_STAGES list never included); e2e-phase6a/
6d.js's own ROOT 01/05 completion tests now complete each module fully (through its lab and
reflection) before moving to the next, since those courses' own modules carry real intra-course
prerequisites. Two now-obsolete Phase-6D-era assertions in e2e-phase6d.js (ROOT 02-04 stay
PLANNED / stay vacuously unlocked) are updated to assert the opposite, with a comment explaining
Phase 6E intentionally changed that fact. All four suites re-verified green fresh.
Also fixes a real, reproducible dash-vs-bash bug caught while testing the new ROOT 01
rehearsal's ordering fix: `&>file` misparses under this image's actual /bin/sh (dash), breaking
a later `$!`-based kill — the rehearsal script now writes multi-line completion loops with
explicit status assertions, matching the discipline the other suites already use.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds platform/test/e2e-phase6d.js (20 checks): ROOT 01 hierarchy, ROOT 02-04 honestly empty
(not padded), activity ordering, lab-cannot-be-manually-completed, full course completion via
lesson/lab/assessment, the prerequisite chain proven on two students (one who completed ROOT 01,
one fresh), existing ROOT 05+/Capstone state unaffected, is_required unaffected, self-paced
no-instructor-deferral check, instructor-notes exclusion, RBAC, logout/login persistence,
cross-student isolation, version-bump stability, and content validation. 20/20 pass against both
a warm and a freshly-reset dev stack.
Adds infra/scripts/drills/{run,test}-phase6d-foundations-rehearsal — the real disposable-Lab
dress rehearsal: login -> enroll -> lesson/guided-exercise -> real Lab launch -> real bash
hardware inspection (nproc/free/df, written to a real file) -> real validation -> a genuine
permission denial (a second Linux user actually denied via `su -`) then a real chmod fix ->
remaining modules -> assessment -> course completion -> ROOT 02 unlock -> logout/login
persistence -> instructor visibility of the resulting learning state. 11/11 pass against a real
Lab container, no mocking. (First run caught a real triple-escaping bug in the hardware-report
step's inline heredoc — fixed by writing the script to a file and docker-cp-ing it in instead.)
Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js that this phase's own
ROOT 01 addition broke: both suites asserted `resume` landed on ROOT 05's gold module as "the
first real content," which was true before this phase and is now ROOT 01. Both suites' original
intent (resume must never land on a bare-lab bank) is preserved, just against the current front
door. Both suites re-verified green (21/21 and 24/24) against a fresh stack.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds platform/test/e2e-phase6c.js (17 checks): upgraded-module activity
chains, documented-exception promotions, optional-lab non-blocking behavior,
no-fake-completion-from-viewing, idempotent completion, instructor-notes
exclusion and RBAC on new content, security-assessment pool/draw/cross-student
isolation/completion, the capstone gate's documented indifference to ROOT 09,
ROOT 11's honest partial/lesson-only rendering, and version-bump content
stability — plus running validate-course-content.js as part of the suite.
All 17 pass against both a warm and a freshly-reset dev stack.
Adds infra/scripts/drills/{run,test}-phase6c-security-config-rehearsal —
Section 22's real-k3s dress rehearsal: login -> program -> course -> lesson ->
demonstration -> knowledge-check -> guided-exercise -> real Lab -> real
kubectl (create Secret, apply Pod, wait for Ready) -> validation -> progress
persistence across logout/login -> instructor visibility of the resulting
learning state, walking root-07-m02-configuration-and-secrets (upgraded from
PARTIAL this phase). 10/10 pass against a real k3s Lab, no mocking.
Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js
that Phase 6C's own legitimate content changes broke: the gold module
(root-10-m01) now has 7 activities (6 required + 1 new optional lab, not 6),
and root-05-m01 now has 2 required manual-marker activities the course-
completion path must also complete (its new bookending lesson/reflection).
Both suites are back to green (21/21 and 24/24) against a fresh stack.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
e2e-phase6b.js (24 checks): hierarchy for the 13 new modules across
ROOT 05-10/12/Capstone, prerequisite chains through the newly-inserted
front-door modules, lesson/knowledge-check completion in brand-new
modules, a newly-wired scenario (job-task) completing correctly, the
optional-vs-required (is_required) completion rule, persistence/
isolation/RBAC/draft-hiding proven against the new content, and the
capstone server-side unlock fix in both directions (a direct bypass
attempt rejected with blocking_stage_ids, an ordinary exam unaffected,
a legitimately unlocked attempt still accepted and its completion
reflected in the course content model).
test-phase6b-root-course-rehearsal.js + wrapper (11 checks, real k3s):
one continuous student narrative through a brand-new module (ROOT 07
Services & Service Discovery, not the Phase 6A gold module) — real Lab
launch, the Lab's own setup_commands, a real 'kubectl expose' against
the live cluster, real validate, logout/login/Resume Learning, Lab
destroy/recreate with academic progress intact — plus an instructor
narrative proving Curriculum view, Prepare Labs, and classroom/learning
tooling all still work against the expanded curriculum.
e2e-phase6a.js updated for two of this phase's own intentional,
documented behavior changes (not defects): resume now points at
root-05's new mental-model module instead of the old bare-lab bank, and
root-10's gold module now unlocks the new troubleshooting-framework
module rather than the bare-lab bank directly.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
POST /api/assessments/:definitionId/start only ever checked that the
definition existed and was active — it never checked unlock_rule. GET
/api/capstone correctly computed and displayed lock state, and the
student UI never exposed a start action while locked, but a direct POST
to this route bypassed the check entirely (documented as a known gap in
docs/PHASE6A_ROOT_COURSE_FOUNDATION.md §10). Student UI state is not the
enforcement boundary.
Fixed by binding to the exact same computeCurriculumState() gate
/api/capstone already relies on, rather than re-parsing
definition.unlock_rule independently — one computation of 'is this
unlocked,' not two that could drift. Only a definition the curriculum
path actually names as ITS capstone is gated this way; an ordinary exam
is completely unaffected. Returns a clean 403 { error: 'locked',
blocking_stage_ids } — the same shape /api/capstone already exposes.
Found while fixing this: e2e-phase5b.js's own capstone-completion test
had been passing only because of this exact gap — it never included the
real-ops stage in its completion loop before attempting to start the
capstone, and the missing unlock check let it succeed anyway. Fixed
alongside the platform code (REAL_OPS added to ALL_NON_CAPSTONE), not
routed around.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
e2e-phase6a.js (21 checks): hierarchy/ordering, honest READY/PARTIAL/
PLANNED states, all three completion behaviors (manual marker/
scenario-derived/assessment-derived), prerequisite locking (including
the zero-content-is-vacuously-satisfied rule), resume, persistence
across logout/login, cross-student isolation, RBAC for self-paced
no-cohort students, draft-content hiding, and content-version
stability.
test-phase6a-course-rehearsal.js + run-phase6a-course-rehearsal.sh: a
real-k3s dress rehearsal of the gold-standard module end to end — a
student reads the real lesson content, launches a real disposable Lab,
the Lab's own POST /api/scenarios/:id/setup (called through the
Gateway's existing /lab/:token/* proxy exactly as the browser UI does)
stands up the broken api-gateway deployment, a real kubectl patch fixes
the readiness probe, and the module completes and unlocks the next one.
No mocked KubeKosh anywhere in this suite.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- platform/test/e2e-phase5c.js (29 checks): activation-vs-confirmation routing,
duplicate/concurrent webhook idempotency, temporary/permanent/exhausted retry
classification, resend/regenerate token invalidation, activation/reset single-use and
expiration, HTML-injection regression, production token-secrecy gating via a real container
recreate with COOKIE_SECURE=1, minor/contact-email resolution, RBAC scoping, metrics/audit
hygiene, and a real Platform API restart.
- infra/scripts/drills/test-phase5c-enrollment-activation.js (10 checks): one continuous
story — website enrollment through a real rendered/extracted activation email, a real Lab
launch and scenario completion, a second enrollment's confirmation email, a full
password-reset round trip, and the minor/no-email contact-email path.
- infra/scripts/gws-email-test.js: the opt-in (RUN_GWS_EMAIL_TEST=true) live Google Workspace
delivery test — does nothing without the explicit flag, sends exactly one message to one
operator-configured recipient when enabled.
- platform/test/e2e-phase5b.js (32 checks): program seed, entitlement gating
(pending/future/expired/cancelled), cross-student isolation, real Lab launch/destroy/
reaper-reconciliation/recreate persistence, server-derived idempotent completion via a full
real capstone completion, the integration boundary (auth, idempotency, real concurrency,
unknown/inactive program rejection, minor/no-email path, no card data), RBAC scoping, a
regression test for the process-crash bug this suite found, Phase 5A smoke, and a real
Platform API restart.
- infra/scripts/drills/test-phase5b-selfpaced-rehearsal.js (15 checks): one continuous story —
activation, a simulated website enrollment event, real Lab work, Lab destroy, a new session,
Continue Learning, a fresh Lab, full real capstone completion, Completed transition, and a
second uninvolved student proven unable to reach any of it.
- platform/test/e2e-phase5a.js (21 checks): the two-student RBAC matrix (student can't reach
any /api/admin/* route, assigned/unassigned/viewer instructor access) and the full password
security checklist (correct/wrong/unknown-identifier/deactivated/immediate-deactivation/
rate-limit/rate-limit-survives-a-real-restart/change-password/session-revocation/
forgot-password anti-enumeration/admin-issued reset/CSRF), plus a classroom lifecycle smoke
pass. Replaces the throwaway smoke5a/smoke5b scripts used during development.
- infra/scripts/drills/test-phase5a-rehearsal.js (15 checks): 5 pilot students with real
seeded pre-existing progress/attempt/achievement history, a live classroom, help requests,
announcements, a REAL Platform API restart mid-rehearsal proving classroom/session/presence
recovery, then converting two students mid-rehearsal and confirming zero data was orphaned.
- Both run against a live docker-compose.dev.yml stack over real HTTP, not mocks.
- infra/scripts/simulate-two-host/: real two-Docker-network simulation
(Gateway genuinely isolated from any Lab bridge network) proving the
Lab Host Router fix - 13/13 real assertions (HTTP+WebSocket through the
Router, Alice/Bob isolation, garbage/destroyed tokens, mTLS-only Router
access).
- infra/scripts/drills/: Lab Host loss/rebuild (9/9), deployment rollback,
CI-fails-closed (6 required failure modes), fake cohort dress rehearsal
(14/14, one continuous narrative through the real app).
- infra/ci/gitea-workflows/build-and-test.yml: new security-scan job
(npm audit, gitleaks, hadolint, shellcheck, trivy - all free/open-source),
severity policy in docs/SECURITY_SCANNING.md (CRITICAL/HIGH blocks,
MEDIUM/LOW reported only). Gates build-images alongside regression.
Found and fixed two real pre-existing bugs via the CI-fails-closed drill:
staging-smoke-test.sh and production-smoke-test.sh both had a classic
set -e gotcha (curl ... && echo "OK" as one statement - a failing command
that isn't last in an && chain doesn't trigger -e) that let an
unreachable/broken endpoint fall through silently instead of failing the
smoke test.
platform/test/e2e-phase3a.js updated for the new paginated
/api/admin/audit-events response shape (Phase 4.1 pagination work).
24 cases: attempt telemetry (start/fail/pass counters, duration), progressive hint sequencing
and exhaustion, hint persistence through a real Lab reset, curriculum stage lock/unlock,
capstone lock-before-prerequisites/unlock-after/random-draw-includes-required-Real-Ops/
cannot-double-start/completion-records-achievement, exam assignment/draw/timer/hint-policy,
cross-student session-read rejection (404, not 403 - consistent with the existing don't-confirm-
existence posture), two-student isolation, timer survives a Lab reset, an assessment session
survives a REAL docker compose restart api, and instructor analytics/needs-attention/RBAC.
Found and fixed two real product bugs in the process: result needed to become nullable (an
in-progress attempt has none yet) and hint_count needed a real DEFAULT 0 (Phase 2B left it
NULL/undefaulted since it wasn't measured yet - it is now, and NULL + 1 stays NULL in SQL).
New e2e-phase3a.js (21 cases): no account enumeration, CSRF positive/negative, login rate
limiting, RBAC across all three roles, cohort-scoped instructor access (visibility + route
access), last-owner protection, live role/status re-evaluation (no separate revoke needed),
password change (old password rejected after, other sessions revoked), audit log content
(including a no-secrets-in-metadata assertion), student session RBAC/logout, and session
persistence across a REAL docker compose restart api mid-test.
New session-helper.js — a shared cookie-jar test helper (Node's fetch doesn't persist
cookies across calls) that also auto-attaches X-CSRF-Token from whichever rn_csrf_*
cookie it's holding, replacing the single-cookie-string trackers e2e-phase2a.js and
e2e-phase2b.js each had their own copy of.
e2e-phase2a.js and e2e-phase2b.js updated for the new auth model: admin login is now a real
bootstrapped OWNER_EMAIL/OWNER_PASSWORD account instead of the old shared ADMIN_PASSWORD.
All cases still passing: 10/10 and 12/12 respectively.
Full acceptance test: admin auth (+ student-can't-call-admin check),
cohort create, 3-student add, curriculum assign, concurrent Prepare Labs
(3/3 ready, instant-reuse timing check), two students completing
different scenarios, dashboard reflecting completions, admin reset with
progress-intact assertion, admin destroy-labs with progress-intact
assertion for everyone, and a regression check that no Lab container
publishes a host port. 12/12 passing.
Replaces the Phase 2A pilot ?sid= query param with an HttpOnly,
SameSite=Lax session cookie. Gateway now derives student identity from
the session (via Platform API's /api/session/whoami) instead of trusting
a client-suppliable id, and checks ownership on every proxied HTTP and
WebSocket Lab request, not just the initial navigation.
Fixes a routing bug found during testing: the fallback route was using
'does this session own a running environment' to decide whether an
unprefixed /api/* call belonged to the Lab UI or the dashboard — true in
both places. Replaced with an explicit LAB_OWNED_PATHS allowlist of the
exact unprefixed paths the existing Lab UI actually calls.
e2e-phase2a.js rewritten to use a cookie jar instead of headers/query
params, with explicit impersonation-rejection tests added. 10/10 passing.
platform/test/e2e-phase2a.js — 15 assert-based tests against a live
platform/docker-compose.dev.yml stack, exercising the full student flow
through the Gateway exactly as a browser would: roster -> login ->
curriculum -> launch (real k3s boot) -> Gateway HTTP routing -> invalid-token
rejection -> cross-student ownership rejection (?sid= mismatch) ->
same-student access -> progress relay (solve in one student's lab, confirm
it shows in their curriculum and NOT the other student's) -> reset (stable
launch_path, progress survives).
All 15 pass. This is slow (each real environment launch is a genuine k3s
cold start) and deliberately not meant for every commit — see
platform/README.md for when to run it.
Does not cover what needs a browser/WebSocket client — real Lab image
terminal (node-pty, kubectl execution, scenario validation) and idle-reaper
timing were verified manually; see
docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md §8 for exactly what and how.
Co-Authored-By: Claude Sonnet 5 <[email protected]>