Laporan Remediasi Audit CBT — Batch 6: Alokasi Sesi & Form, Proteksi Beban, Metrik Sistem, Siklus Token, Aksesibilitas Frontend & Penyelarasan Skema Alembic (R04, R05, R06, R07, Q01, Q02, Q03)
Tanggal: 29 September 2026
Status Batch: SELESAI & TERVERIFIKASI (207 Integration Tests Passed 100%, 41 Frontend Unit Tests Passed, Frontend Build Bersih, 0 Alembic Drift)
Baseline Head: f1a2b3c4d5e6
Lingkungan Uji: Host 187.77.126.230, PostgreSQL 16 (scola_assessment_test), user cbt
1. Ringkasan Temuan, Akar Masalah & Solusi
R04 — Penugasan Sesi & Form Peserta Belum Ditegakkan pada Alur Masuk Ujian
- Akar Masalah:
- Meskipun model dan tabel
EnrollmentSessionAssignmentsudah ada di skema basis data, alur runner sebelumnya tidak memperhitungkan tabel ini:lookup_participant_credentialmengembalikan seluruh sesi yang aktif di event tanpa menyaring sesi yang secara spesifik ditugaskan kepada peserta.start_or_resume_attemptmengizinkan peserta memulai ujian pada sesi mana pun di event terkait tanpa memverifikasi penugasan sesi, dan hanya memuat form default sesi (ExamForm.session_id == session.id) tanpa memuat form khusus yang ditugaskan diEnrollmentSessionAssignment.form_id.create_launch_authorization(SEB launcher) memilih sesi secara sembarang tanpa memeriksa penugasan peserta.
- Perbaikan:
- Di
backend/app/services/runner_session_service.py:- Pada
lookup_participant_credential: Memeriksa keberadaan rekamanEnrollmentSessionAssignmentuntuk peserta. Jika rekaman penugasan ada, daftaravailable_sessionsdifilter ketat hanya untuk sesi yang ditugaskan. - Pada
start_or_resume_attempt: Memvalidasi penugasan sesi. Jika peserta memiliki penugasan sesi dan mencoba memulai ujian pada sesi yang tidak ditugaskan, sistem menolak denganHTTP 403 Forbidden("Peserta tidak memiliki penugasan untuk sesi ujian ini."). Jika penugasan menyertakanform_idkhusus, paket soal dibaca langsung dari form yang ditugaskan tersebut.
- Pada
- Di
backend/app/services/seb_service.py:create_launch_authorization:- Menyaring sesi berdasarkan
EnrollmentSessionAssignmentpeserta sebelum menerbitkan token otorisasi peluncuran SEB.
- Menyaring sesi berdasarkan
R05 — Ketiadaan Batas Ukuran Payload Impor & Pagination Soal / Sweeper
- Akar Masalah:
- Endpoint upload berkas impor soal (
/api/v1/authoring/questions/import) dan impor peserta (/api/v1/participants/import-csv) membaca file tanpa batas ukuran streaming, berisiko memicu memory exhaustion / DoS pada server saat ribuan peserta diunggah. - Pengambilan daftar soal (
list_questions) mengambil seluruh baris tanpa batas (limit/offset), memperlambat respons aplikasi pada bank soal berukuran besar. - Pembersihan attempt kedaluwarsa (
sweep_expired_attempts) mengambil seluruh baris sekaligus tanpa batasan batch size. - Perbaikan:
- Batas Ukuran Upload (10 MB):
- Di
backend/app/api/v1/questions.pydanbackend/app/api/v1/participants.py: Membatasi pembacaan berkas (file.read(MAX_UPLOAD_SIZE + 1)). Jika melebihiMAX_UPLOAD_SIZE = 10 * 1024 * 1024(10 MB), request langsung ditolak denganHTTP 413 Request Entity Too Large("Ukuran berkas melebihi batas maksimal 10 MB").
- Di
- Paginasi Bank Soal:
- Di
backend/app/services/question_service.py:list_questions: Menambahkan parameterlimit: int = 100danoffset: int = 0dengan query.offset(offset).limit(limit). - Di
backend/app/api/v1/questions.py: Mengekspos query parameterlimit: int = Query(default=100, le=500)danoffset: int = Query(default=0, ge=0).
- Di
- Batch Bounding pada Sweeper:
- Di
backend/app/workers/sweeper.py:sweep_expired_attempts: Menambahkan parameterbatch_size: int = 100dan menerapkan.limit(batch_size)pada query lockingwith_for_update(skip_locked=True).
- Di
R06 — Observability Terbatas pada Backlog Outbox & Attempt Kedaluwarsa
- Akar Masalah:
- Endpoint
/health/metricshanya menampilkan statistik connection pool dan hitunganactive_attemptstanpa visibilitas antrean asynchronous outbox event dan attempt yang terlewat batas waktu auto-submit. - Perbaikan:
- Di
backend/app/api/v1/health.py:health_metrics:- Menghitung jumlah pesan outbox tertunda:
pending_outbox = db.query(OutboxMessage).filter(OutboxMessage.status == "pending").count() - Menghitung jumlah attempt aktif yang telah melewati batas deadline:
overdue_attempts = db.query(Attempt).filter(Attempt.status.in_(["in_progress", "paused"]), Attempt.server_deadline <= now).count() - Mengekspos nilai
pending_outbox_eventsdanoverdue_unfinalized_attemptspada respons JSON.
- Menghitung jumlah pesan outbox tertunda:
R07 — Sesi Staf Aktif Tidak Dicabut Saat Ganti Password
- Akar Masalah:
- Ketika pengguna staf mengubah kata sandi di
auth_service.py:change_password, hanya hash password yang diperbarui. Token sesi staf aktif (StaffSession) yang tersimpan di browser lain tidak dicabut secara otomatis. - Perbaikan:
- Di
backend/app/services/auth_service.py:change_password:- Mengambil seluruh sesi staf aktif (
user_id == user.id, revoked_at.is_(None)). - Menyetel
s.revoked_at = datetime.now(timezone.utc)secara seketika pada semua sesi tersebut.
- Mengambil seluruh sesi staf aktif (
Q01 — Frontend Test Gate Terhambat Kegagalan Pinia & Selector Uji
- Akar Masalah:
- Pengujian Vitest di
frontend/tests/unit/help_guide.test.tsgagal karenasetActivePinia(createPinia())tidak diinisialisasi dalam hook pengujian, serta adanya perbedaan ekspektasi teks warna status proctoring ('Oranye' vs 'Kuning'). - Pengujian
router.test.tsmencoba mengakses path/organizerbiasa alih-alih rute dengan izin spesifik (/organizer/events). - Perbaikan:
- Di
frontend/tests/unit/help_guide.test.ts: Menambahkan inisialisasi Pinia dibeforeEach()dan menyelaraskan penegasan teks warna status dengan implementasi komponen. - Di
frontend/tests/unit/router.test.ts: Memperbarui navigasi rute terbatas proctoring ke/organizer/events. - Hasil: Seluruh 41 unit tests di frontend berhasil lulus 100% tanpa galat.
Q02 — Aksesibilitas Formulir Jawaban, Dukungan KaTeX & Focus Trap Modal
- Akar Masalah:
- Pilihan ganda pada
QuestionContentCard.vuemenggunakan binding teks mentah tanpa rendering KaTeX/HTML yang aman. - Input numerik tidak memiliki label eksplisit untuk screen reader dan tidak mengembalikan nilai
nullsaat dikosongkan. - Indikator status penyimpanan tidak memiliki
aria-live="polite". - Modal panduan (
HelpModalBase.vue) belum menerapkan focus trap (penguncian tombol Tab di dalam modal) dan pemulihan fokus elemen asal saat ditutup. - Perbaikan:
- Di
frontend/src/components/runner/QuestionContentCard.vue:- Menambahkan
v-html="safeHtml(opt.text)"untuk merender formula matematika dan teks pilihan ganda secara aman. - Menghubungkan label dengan input numerik dan esai menggunakan
:fordan:id. - Menambahkan
aria-live="polite"pada indikator status penyimpanan. - Menambahkan fungsi
onNumericInputyang memancarkanupdate:numericValuedengannullsaat input dikosongkan.
- Menambahkan
- Di
frontend/src/components/common/HelpModalBase.vue:- Mengimplementasikan penanganan siklus Tab (focus trap) agar fokus keyboard tidak keluar dari dialog modal saat terbuka.
- Menyimpan
previousActiveElementsaat modal dibuka dan mengembalikan fokus keyboard ke elemen pemanggil saat modal ditutup.
Q03 — Penyelarasan Model SQLAlchemy dengan Skema Migrasi Alembic (0 Drift)
- Akar Masalah:
- Perintah
alembic checkmendeteksi ketidaksesuaian (drift) antara model SQLAlchemy dan database PostgreSQL:- Indeks komposit
ix_camera_evidences_attempt_statusdi tabelcamera_evidencestidak ada di modelCameraEvidence. - Indeks
ix_enr_sess_assign_enrollmentdanix_enr_sess_assign_sessiondi modelEnrollmentSessionAssignmentmenggunakan nama default SQLAlchemy alih-alih nama eksplisit migrasi. - Indeks
ix_result_publications_session_activedi tabelresult_publicationstidak dideklarasikan di modelResultPublication. - Kolom
token_hashdiStaffSessiondanattempt_iddiSubmissionReceiptmemilikiunique=Truedi level kolom, yang menyebabkan SQLAlchemy memperlakukannya sebagai indeks unik tunggal alih-alih pasangan constraint unik + indeks non-unik sesuai DDL PostgreSQL.
- Indeks komposit
- Perbaikan:
- Di
backend/app/models/proctor.py(CameraEvidence):- Mengimpor
Indexdan menambahkanIndex("ix_camera_evidences_attempt_status", "attempt_id", "status")di__table_args__.
- Mengimpor
- Di
backend/app/models/event.py(EnrollmentSessionAssignment):- Menghapus
index=Truedi level kolom dan mendeklarasikanIndex("ix_enr_sess_assign_enrollment", "enrollment_id")sertaIndex("ix_enr_sess_assign_session", "session_id")di__table_args__.
- Menghapus
- Di
backend/app/models/attempt.py(ResultPublication&SubmissionReceipt):- Menambahkan
Index("ix_result_publications_session_active", "session_id", "is_active")diResultPublication.__table_args__. - Menghapus
unique=Truedari kolomattempt_iddiSubmissionReceiptdan mendeklarasikanUniqueConstraint("attempt_id", name="submission_receipts_attempt_id_key")di__table_args__.
- Menambahkan
- Di
backend/app/models/organizer.py(StaffSession):- Menghapus
unique=Truedari kolomtoken_hashdan mendeklarasikanUniqueConstraint("token_hash", name="staff_sessions_token_hash_key")di__table_args__.
- Menghapus
- Hasil:
PYTHONPATH=backend .venv/bin/alembic -c backend/alembic.ini checkmenghasilkanNo new upgrade operations detected(0 drift, exit code 0).
2. Bukti Pengujian & Verifikasi
A. Pengujian Regresi Khusus Batch 6 (tests/integration/test_batch6_regression.py)
==================== test session starts =====================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
collected 10 items
tests/integration/test_batch6_regression.py::test_r04_admission_session_filter_lookup_runner_credentials PASSED [ 10%]
tests/integration/test_batch6_regression.py::test_r04_start_attempt_enforces_assigned_session PASSED [ 20%]
tests/integration/test_batch6_regression.py::test_r04_assigned_form_loaded_when_specified PASSED [ 30%]
tests/integration/test_batch6_regression.py::test_r04_launch_authorization_selects_assigned_session PASSED [ 40%]
tests/integration/test_batch6_regression.py::test_r05_upload_size_limit_rejected_with_413 PASSED [ 50%]
tests/integration/test_batch6_regression.py::test_r05_list_questions_pagination_enforced PASSED [ 60%]
tests/integration/test_batch6_regression.py::test_r05_sweeper_batch_size_bounded PASSED [ 70%]
tests/integration/test_batch6_regression.py::test_r06_health_metrics_reports_outbox_and_overdue_attempts PASSED [ 80%]
tests/integration/test_batch6_regression.py::test_r07_change_password_revokes_active_staff_sessions PASSED [ 90%]
tests/integration/test_batch6_regression.py::test_q03_alembic_models_match_migrations PASSED [100%]
=============== 10 passed, 6 warnings in 8.01s ===============
B. Pengujian Unit Frontend (frontend/ npm test)
RUN v5.0.1 /home/cbt/scola-assessment/frontend
✓ tests/unit/router.test.ts (3 tests)
✓ tests/unit/help_guide.test.ts (10 tests)
✓ tests/unit/useExamRunner.test.ts (7 tests)
✓ tests/unit/safeHtml.test.ts (2 tests)
✓ tests/unit/auth.test.ts (1 test)
✓ tests/unit/confirm_dialog.test.ts (3 tests)
✓ tests/unit/proctor_flow.test.ts (5 tests)
✓ tests/unit/grader_flow.test.ts (5 tests)
✓ tests/unit/organizer_flow.test.ts (3 tests)
✓ tests/unit/api.test.ts (2 tests)
Test Files 10 passed (10)
Tests 41 passed (41)
Duration 9.28s
C. Build Produksi Frontend (frontend/ npm run build)
> vue-tsc && vite build
vite v8.3.0 building client environment for production...
✓ 389 modules transformed.
✓ built in 1.17s (0 errors)
D. Seluruh Rangkaian Pengujian Integrasi Repositori
207 passed, 8 warnings in 107.30s (0:01:47)
Tingkat Keberhasilan: 100% (207 dari 207 tests passed)
3. Matriks Status Remediasi Menyeluruh
| Temuan | Deskripsi | Batch | Status | Bukti Pengujian |
|---|---|---|---|---|
| B03 | Server deadline non-fail-closed | Batch 1 | SELESAI | test_b03_b06_regression.py |
| B06 | Status attempt tidak sinkron | Batch 1 | SELESAI | test_b03_b06_regression.py |
| B01 | Kebocoran kunci jawaban di HTML | Batch 2 | SELESAI | test_batch2_regression.py |
| B07 | Injeksi formula LaTeX / KaTeX | Batch 2 | SELESAI | test_batch2_regression.py |
| B08 | Race condition shuffle jawaban | Batch 2 | SELESAI | test_batch2_regression.py |
| B09 | Validasi toleransi numerik float | Batch 2 | SELESAI | test_batch2_regression.py |
| B11 | Konkurensi start crash IntegrityError | Batch 2 | SELESAI | test_b11_concurrent_start_regression.py |
| B04 | Form locking & lifecycle event | Batch 3 | SELESAI | test_batch3_regression.py |
| B05 | Credential rotation & runner revocation | Batch 3 | SELESAI | test_batch3_regression.py |
| B10 | Dual camera readiness timeout & hold | Batch 3 | SELESAI | test_batch3_regression.py |
| R03 | Pembatalan publikasi hasil & audit trail | Batch 3 | SELESAI | test_batch3_regression.py |
| B12 | Validasi format file impor peserta | Batch 4 | SELESAI | test_batch4_regression.py |
| B13 | XSS sanitizer & KaTeX math render | Batch 4 | SELESAI | test_batch4_regression.py |
| B14 | Pembatasan sesi proctoring multi-ruang | Batch 4 | SELESAI | test_batch4_regression.py |
| B15 | OTP recovery token leakage di response | Batch 4 | SELESAI | test_batch4_regression.py |
| R01 | Sweeper standalone & outbox handlers | Batch 5 | SELESAI | test_batch5_regression.py |
| R02 | Pessimistic locking & unique constraint | Batch 5 | SELESAI | test_batch5_regression.py |
| B02 | SEB HTTP probe rejection & BEK enforcement | Batch 5 | SELESAI | test_batch5_regression.py |
| R04 | Penugasan sesi & form spesifik peserta | Batch 6 | SELESAI | test_batch6_regression.py |
| R05 | Batas upload 10MB, paginasi & sweep bounds | Batch 6 | SELESAI | test_batch6_regression.py |
| R06 | Metrik outbox & overdue attempts | Batch 6 | SELESAI | test_batch6_regression.py |
| R07 | Pencabutan sesi staf saat ganti password | Batch 6 | SELESAI | test_batch6_regression.py |
| Q01 | Frontend Vitest test gate passed | Batch 6 | SELESAI | frontend/ npm test (41/41) |
| Q02 | KaTeX pilihan ganda & focus trap modal | Batch 6 | SELESAI | QuestionContentCard, HelpModalBase |
| Q03 | 0 drift Alembic migration check | Batch 6 | SELESAI | alembic check |