fix(quiz): jangan belah soal saat '#' muncul di dalam code fence
_split_flashcard_parts() pakai state-machine per-baris agar komentar '#'
di dalam blok kode (python/shell/bash/yaml) tidak dianggap heading baru.
Sebelumnya re.split('^#{1,3}\s+') membelah soal menjadi dua, sehingga
opsi meleset ke 'soal hantu' dan pilihan di layar tidak sesuai markdown.
This commit is contained in:
parent
ac3fdb8dff
commit
5cc4ca472b
|
|
@ -829,6 +829,39 @@ def _extract_body_image(text):
|
|||
return None, text
|
||||
|
||||
|
||||
def _split_flashcard_parts(text):
|
||||
"""Split quiz markdown into per-question blocks on heading lines
|
||||
(`^#{1,3}\\s+`), but ignore heading-looking lines that live INSIDE a
|
||||
fenced code block (``` ... ```).
|
||||
|
||||
Re-implementasi dari `re.split(r'^#{1,3}\\s+', text, MULTILINE)` yang lama
|
||||
agar komentar `#` di dalam kode (python/shell/bash/yaml) tidak membelah
|
||||
soal. Penggunaan state-machine per-baris, bukan placeholder, supaya tidak
|
||||
ada risiko tabrakan string.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
buf: list[str] = []
|
||||
in_fence = False
|
||||
for line in text.split('\n'):
|
||||
stripped = line.strip()
|
||||
# Toggle fence state pada baris yang HANYA berisi ``` (atau ```lang)
|
||||
if stripped.startswith('```'):
|
||||
in_fence = not in_fence
|
||||
buf.append(line)
|
||||
continue
|
||||
# Heading pembatas soal — hanya di luar fence
|
||||
if not in_fence and re.match(r'^#{1,3}\s+', line):
|
||||
if buf:
|
||||
parts.append('\n'.join(buf))
|
||||
buf = [line]
|
||||
continue
|
||||
buf.append(line)
|
||||
if buf:
|
||||
parts.append('\n'.join(buf))
|
||||
# Baris pertama biasanya kosong (teks sebelum heading `###` pertama)
|
||||
return [p for p in parts if p.strip()]
|
||||
|
||||
|
||||
def _parse_flashcards(text):
|
||||
"""Parse a string of markdown with headings and options into a list of dicts.
|
||||
|
||||
|
|
@ -848,9 +881,12 @@ def _parse_flashcards(text):
|
|||
"""
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
# Split by headings starting with #, ##, or ###
|
||||
parts = re.split(r'^#{1,3}\s+', text, flags=re.MULTILINE)
|
||||
|
||||
# Split by headings starting with #, ##, or ### — but ONLY outside fenced
|
||||
# code blocks. A line like `# ini komentar` inside ```python must NOT be
|
||||
# treated as a new question (it would split the question and steal its
|
||||
# options, producing phantom questions / mismatched choices).
|
||||
parts = _split_flashcard_parts(text)
|
||||
flashcards = []
|
||||
|
||||
for part in parts:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
from services.lesson_service import _parse_flashcards
|
||||
|
||||
|
||||
def test_mcq_code_fence_with_hash_comment_is_single_question():
|
||||
"""Soal yang prompt-nya berisi code fence dengan komentar `#` (python/shell)
|
||||
tidak boleh di-split menjadi soal ganda — opsi tetap milik soal tersebut.
|
||||
|
||||
REGRESI: splitter `^#{1,3}\\s+` di _parse_flashcards membelah teks
|
||||
pada baris `# ini komentar`, sehingga opsi meleset ke 'soal hantu'.
|
||||
"""
|
||||
md_text = """### Berapa hasil dari kode berikut?
|
||||
```python
|
||||
# ini komentar python
|
||||
x = 5
|
||||
print(x)
|
||||
```
|
||||
- [] 3
|
||||
- [x] 5
|
||||
- [] 5.0
|
||||
"""
|
||||
quiz = _parse_flashcards(md_text)
|
||||
# Harus 1 soal, bukan 2 (soal asli + 'ini komentar python' sebagai phantom)
|
||||
assert len(quiz) == 1, f"ter-split menjadi {len(quiz)} soal: {[q.get('question', q.get('front')) for q in quiz]}"
|
||||
q = quiz[0]
|
||||
assert q['type'] == 'mcq'
|
||||
assert len(q['options']) == 3, f"opsi cuma {len(q['options'])}: {q['options']}"
|
||||
assert q['options'][1]['is_correct'] is True
|
||||
|
||||
|
||||
def test_mcq_code_fence_with_hash_comment_in_middle():
|
||||
"""Kasus `#` komentar muncul di tengah kode (bukan baris pertama)."""
|
||||
md_text = """### Apa output?
|
||||
```bash
|
||||
echo "hi" # comment di sini
|
||||
echo "bye"
|
||||
```
|
||||
- [x] hi
|
||||
- [] bye
|
||||
"""
|
||||
quiz = _parse_flashcards(md_text)
|
||||
assert len(quiz) == 1, f"ter-split menjadi {len(quiz)} soal"
|
||||
assert len(quiz[0]['options']) == 2
|
||||
Loading…
Reference in New Issue