Local API stress (lib.mjs / api_stress.mjs): - setupSession now does login -> /admin/api/reset and returns sid="main". Drops the dead /admin/api/quizzes + /admin/api/sessions calls left over from the multi-quiz codex era. - bootServer writes the fixture pool (STRESS_POOL by default) to a tmp file and passes QUIZ_POOL_PATH so the v1.2 server has a session at startup. - happyPath: drop the post-connect lobby_update wait (race with snapshot dispatch) and stop double-driving the lifecycle (next() already opens the next question, an explicit open() afterwards is a no-op). - cross_session: rewritten as "cookie not honored on a non-existent sid" since v1.2 hosts a single canonical session. Live accuracy stress (live_accuracy.mjs): - Per-student lobby-snapshot timeout (12s) with WS error/close rejection, so a stalled handshake no longer hangs Promise.all until the outer shell timeout (which produced the exit=124 cycles). - Open all student WSs in parallel (mirrors what real students do); the batch-of-8 throttle was masking the question we wanted answered. - Instructor WS open also bounded by a 15s race so any failure surfaces as actionable error text instead of a silent stall. Bootstrap (deploy/bootstrap.sh): - Stage 1 provisions a 2GB swap file (idempotent) with vm.swappiness=10. 1GB-RAM ECS instances OOM-kill uvicorn under WS-burst start-of-class pressure; swap absorbs the spike without affecting steady state. - Pool seeding prefers examples/demo10_pool.json over the 2-question example so a fresh deploy boots with a usable demo. Pool fixture (examples/demo10_pool.json): - 10-question generic-knowledge demo pool, gitignore exception added.
Quiz portal stress harness
Adversarial frontend + API stress tests for the quiz portal. Built 2026-05-02.
Files
lib.mjs— shared helpers: server boot, cookie jar,StudentandAdminWS wrappers, the fixedSTRESS_POOL.api_stress.mjs— pure WS adversarial scenarios (no browser): happy path with 20 concurrent students, late join, mid-question disconnect, sleep/wake to next question (the phone-screen-sleep scenario), cookie tampering, cross-session cookie reuse, duplicate student_id, bad submits (out-of-order, wrong idx, resubmit), close-boundary race, malformed-JSON fuzz, flaky reconnect.ui_stress.mjs— Playwright/Chromium scenarios that exercise the real SPA: happy UI flow, sleep/wake by closing+reopening browser context with persisted cookie, cookie-tamper viadocument.cookie, two browsers with same student_id.run_loop.sh— bash wrapper that runsapi_stress.mjsevery cycle andui_stress.mjseveryUI_EVERYcycles (default 5), with a fresh random seed each time. Logs JSON summary lines toruns/summary.jsonland full output toruns/run-<timestamp>.jsonl.
Quick start
# One-shot
node api_stress.mjs # uses Date.now() seed
node api_stress.mjs 12345 8210 # explicit seed + port
node ui_stress.mjs # browser-based; HEADLESS=0 to watch
# Long-running loop in tmux
tmux new -d -s quiz_stress 'cd /home/ameer/RD/Projects/Apps/quiz/tests/stress && bash run_loop.sh'
tmux attach -t quiz_stress # to watch
tmux send -t quiz_stress C-c # to stop
Each cycle boots a fresh uvicorn on its own port and clean DB, runs scenarios, then tears down. Failures are recorded in the failures array of the per-cycle summary line.
Known findings (tracked outside this dir)
- Codex bug:
app/room.pystudent_ws(line ~87) andinstructor_wscallawait websocket.receive_json()whose JSON parsing can raiseJSONDecodeError, but the surroundingtry/exceptonly catchesWebSocketDisconnect. Result: a single malformed message kills that client's WS handler. The fuzz scenario inapi_stress.mjsflags this consistently. Fix: wrap the receive intry/except (JSONDecodeError, RuntimeError):and either close cleanly or send{"type":"error","code":"bad_message"}and continue.
Adding scenarios
Write an async function name(server) { ... } in api_stress.mjs (or (server, browser) for UI), add it to the SCENARIOS map / array, and re-run. Use expect(cond, scenario, msg, extra) for assertions and note(scenario, msg) for warnings that shouldn't fail the suite. Critical pattern: pre-register waitFor waiters BEFORE the action that triggers the message — Student.waitFor(type) only resolves on NEW messages, not cached ones, to avoid stale-state false passes.