e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from deeptutor.agents.question.mimic_source import (
|
|
_coerce_difficulty,
|
|
_coerce_question_type,
|
|
_parse_sync,
|
|
)
|
|
|
|
|
|
def test_coerce_question_type_maps_to_canonical_taxonomy() -> None:
|
|
assert _coerce_question_type("CHOICE") == "choice"
|
|
assert _coerce_question_type("coding") == "coding"
|
|
assert _coerce_question_type("fill_in_blank") == "fill_in_blank"
|
|
# Anything off-taxonomy degrades to a free-text written question.
|
|
assert _coerce_question_type("nonsense") == "written"
|
|
assert _coerce_question_type(None) == "written"
|
|
assert _coerce_question_type("") == "written"
|
|
|
|
|
|
def test_coerce_difficulty_validates_levels() -> None:
|
|
assert _coerce_difficulty("Hard") == "hard"
|
|
assert _coerce_difficulty("easy") == "easy"
|
|
assert _coerce_difficulty("") == "medium"
|
|
assert _coerce_difficulty("trivial") == "medium"
|
|
assert _coerce_difficulty(None) == "medium"
|
|
|
|
|
|
def test_parse_sync_maps_extracted_type_difficulty_and_answer(tmp_path: Path) -> None:
|
|
# "parsed" mode with a pre-existing *_questions.json skips MinerU + the LLM
|
|
# extractor entirely, so this is a pure mapping test.
|
|
paper = tmp_path / "exam"
|
|
paper.mkdir()
|
|
(paper / "exam_questions.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"questions": [
|
|
{
|
|
"question_number": "1",
|
|
"question_text": "Which is correct?\nA. a\nB. b\nC. c\nD. d",
|
|
"question_type": "choice",
|
|
"difficulty": "easy",
|
|
"answer": "B",
|
|
},
|
|
{
|
|
"question_number": "2",
|
|
"question_text": "Explain backpropagation.",
|
|
"question_type": "WeirdType", # invalid → written
|
|
"difficulty": "impossible", # invalid → medium
|
|
"answer": "",
|
|
},
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
templates, trace = _parse_sync(paper, 10, "parsed", tmp_path / "out")
|
|
|
|
assert len(templates) == 2
|
|
first, second = templates
|
|
|
|
assert first.question_type == "choice"
|
|
assert first.difficulty == "easy"
|
|
assert first.reference_answer == "B"
|
|
assert first.source == "mimic"
|
|
assert first.reference_question.startswith("Which is correct?")
|
|
|
|
# Off-taxonomy values fall back to safe defaults.
|
|
assert second.question_type == "written"
|
|
assert second.difficulty == "medium"
|
|
assert second.reference_answer is None
|
|
|
|
assert trace["template_count"] == "2"
|
|
|
|
|
|
def test_parse_sync_respects_max_questions(tmp_path: Path) -> None:
|
|
paper = tmp_path / "exam"
|
|
paper.mkdir()
|
|
(paper / "exam_questions.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"questions": [
|
|
{"question_text": f"Q{i}", "question_type": "short_answer"} for i in range(5)
|
|
]
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
templates, _ = _parse_sync(paper, 2, "parsed", tmp_path / "out")
|
|
assert len(templates) == 2
|
|
|
|
|
|
def test_parse_sync_upload_mode_uses_parse_service_and_isolates_output(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
"""Upload mode goes through the shared ParseService and writes the
|
|
questions JSON to the session output dir, never the shared parse cache."""
|
|
from deeptutor.agents.question import mimic_source
|
|
from deeptutor.services.parsing.types import ParsedDocument
|
|
|
|
# The parse cache dir the (fake) ParseService returns as the parsed workdir.
|
|
cache_dir = tmp_path / "parse_cache" / "abc"
|
|
cache_dir.mkdir(parents=True)
|
|
(cache_dir / "exam.md").write_text("# parsed", encoding="utf-8")
|
|
|
|
class _FakeService:
|
|
def parse(self, source_path, **kwargs):
|
|
return ParsedDocument(
|
|
markdown="# parsed", workdir=cache_dir, engine="fake", source_hash="h"
|
|
)
|
|
|
|
monkeypatch.setattr(mimic_source, "get_parse_service", lambda: _FakeService())
|
|
|
|
captured: dict = {}
|
|
|
|
def _fake_extract(paper_dir, output_dir=None):
|
|
captured["paper_dir"] = paper_dir
|
|
captured["output_dir"] = output_dir
|
|
# Mimic the real extractor: write the questions JSON to output_dir.
|
|
out = Path(output_dir)
|
|
(out / "exam_questions.json").write_text(
|
|
json.dumps({"questions": [{"question_text": "Q1", "question_type": "written"}]}),
|
|
encoding="utf-8",
|
|
)
|
|
return True
|
|
|
|
monkeypatch.setattr(mimic_source, "extract_questions_from_paper", _fake_extract)
|
|
|
|
output_base = tmp_path / "session-out"
|
|
pdf = tmp_path / "exam.pdf"
|
|
pdf.write_bytes(b"%PDF-1.4")
|
|
|
|
templates, trace = _parse_sync(pdf, 10, "upload", output_base)
|
|
|
|
assert len(templates) == 1
|
|
# Parsed content is read from the cache workdir...
|
|
assert captured["paper_dir"] == str(cache_dir)
|
|
# ...but the questions JSON is written to the session dir, not the cache.
|
|
assert captured["output_dir"] == str(output_base)
|
|
assert (output_base / "exam_questions.json").exists()
|
|
assert not (cache_dir / "exam_questions.json").exists()
|