CoolFace
Apppublic

sahil-12kumar/IL_CMS_Tools

sourceHugging Faceupdated 8d agoView on Hugging Face
1likes
test_resolve_toc_exam.py244 linesDownload Raw Back to tests
1"""Resolve TOC must report row 1 of a question's "Visible to students in this2exams" list, not the master taxonomy.3 4CMS shows that list on the question page, numbered, e.g. for ILQ-1836594:5 6    1. CBSE Grade 7 > Mathematics > Exponents and Powers > Laws of exponents7    2. CBSE Grade 8 > Mathematics > Power Play. > Laws of Exponents8    3. Grade 8-AP State Board > Mathematics > Exponents and Powers(Inactive) > ...9 10Rows are exam > subject > chapter > topic. The SME wants row 1 — the chapter a11student meets first — so the API's own order decides, NOT the question's stored12chapter_id, which frequently points at a later row. Note "(Inactive)" in row 313is part of the chapter NAME; every row in this panel is student-visible.14 15Both readers — the web app and the standalone extract_toc CLI — carry the same16resolution, so both are pinned here.17"""18import sys19from pathlib import Path20 21import pytest22 23sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'backend' / 'ai_tagger'))24 25MASTER = {'subjectName': 'Maths',26          'chapterName': 'Exponents (Master)',27          'topicName': 'Powers (Master)'}28 29 30def row(exam_id, exam, subject, chap_id, chapter, topic_id, topic):31    return {'examDetails': {'examId': exam_id, 'examName': exam},32            'subjectDetails': {'subjectId': 4, 'subjectName': subject},33            'chapterDetails': {'chapterId': chap_id, 'chapterName': chapter},34            'topicDetails': {'topicId': topic_id, 'topicName': topic}}35 36 37# The first rows of ILQ-1836594's list, in the order CMS numbers them.38PAYLOAD = {'data': [{39    'subtopicName': 'Laws of exponents',40    'subtopicMasterTocNavigation': MASTER,41    'subtopicExamTocNavigation': [42        row(40, 'CBSE Grade 7', 'Mathematics',43            700, 'Exponents and Powers', 800, 'Laws of exponents'),44        row(41, 'CBSE Grade 8', 'Mathematics',45            701, 'Power Play.', 801, 'Laws of Exponents'),46        row(42, 'Grade 8-AP State Board', 'Mathematics',47            702, 'Exponents and Powers(Inactive)', 802, 'Laws of Exponents'),48        row(43, 'Grade 7-AP State Board', 'Mathematics',49            703, 'Exponents and Powers(T)', 803,50            'Exponential Form and Laws of Exponents'),51    ]}]}52 53CHAIN_1 = 'CBSE Grade 7 > Mathematics > Exponents and Powers > Laws of exponents'54 55 56@pytest.fixture(scope='module')57def toc_mod():58    import extract_toc59    return extract_toc60 61 62@pytest.fixture(params=['app', 'cli'])63def mod(request, app_mod, toc_mod):64    """Both copies of the resolver — they must stay in step."""65    return app_mod if request.param == 'app' else toc_mod66 67 68def test_row_one_wins(mod):69    assert mod._exam_toc_names(PAYLOAD) == {70        'exam': 'CBSE Grade 7',71        'subject': 'Mathematics',72        'chapter': 'Exponents and Powers',73        'topic': 'Laws of exponents'}74 75 76def test_chain_is_printed_like_cms(mod):77    assert mod._toc_chain(mod._exam_toc_names(PAYLOAD)) == CHAIN_178 79 80def test_stored_chapter_id_does_not_reorder_the_list(mod, app_mod, monkeypatch):81    """The question's stored chapter_id points at row 3 here; row 1 must still82    be reported. This is the bug the SME hit twice."""83    monkeypatch.setattr(app_mod, '_comm_headers', lambda token, uid: {})84    monkeypatch.setattr(app_mod, '_comm_get', lambda *a, **kw: PAYLOAD)85 86    names = app_mod._resolve_toc_names('tok', 'uid', {87        'grade': [7], 'subject_id': [4],88        'chapter_id': [702], 'topic_id': [802], 'sub_topic_id': [900]})89 90    assert names['chapter'] == 'Exponents and Powers'91    assert names['exam'] == 'CBSE Grade 7'92    assert app_mod._toc_chain(names) == CHAIN_193 94 95def test_an_inactive_looking_name_is_not_filtered(mod):96    """"(Inactive)" lives in the chapter name, so a list whose row 1 carries it97    still reports that row — dropping it would invent a rule CMS doesn't have."""98    payload = {'data': [{'subtopicExamTocNavigation': [99        row(42, 'Grade 8-AP State Board', 'Mathematics',100            702, 'Exponents and Powers(Inactive)', 802, 'Laws of Exponents')]}]}101    assert mod._exam_toc_names(payload)['chapter'] == 'Exponents and Powers(Inactive)'102 103 104def test_subject_comes_from_the_row_not_the_master_tree(app_mod, monkeypatch):105    """Master says "Maths", row 1 says "Mathematics" — the row wins."""106    monkeypatch.setattr(app_mod, '_comm_headers', lambda token, uid: {})107    monkeypatch.setattr(app_mod, '_comm_get', lambda *a, **kw: PAYLOAD)108 109    names = app_mod._resolve_toc_names('tok', 'uid', {110        'grade': [7], 'subject_id': [4],111        'chapter_id': [700], 'topic_id': [800], 'sub_topic_id': [900]})112    assert names['subject'] == 'Mathematics'113    assert names['subtopic'] == 'Laws of exponents'   # still the question's own114 115 116def test_chain_stops_at_the_topic(mod):117    """CMS's list has four levels; the subtopic is a column of its own, not a118    fifth step in the chain."""119    assert 'subtopic' not in mod._toc_chain(120        {'exam': 'CBSE Grade 7', 'subject': 'Mathematics',121         'chapter': 'Exponents and Powers', 'topic': 'Laws of exponents',122         'subtopic': 'Something Else'})123    assert mod._toc_chain(124        {'exam': 'CBSE Grade 7', 'subject': 'Mathematics',125         'chapter': 'Exponents and Powers', 'topic': 'Laws of exponents',126         'subtopic': 'Something Else'}) == CHAIN_1127 128 129def test_level_names_are_found_wherever_the_api_hangs_them(mod):130    """A row is anchored on its `chapterDetails` key — the one part of the shape131    production already relies on (push_tags._nav_entries). Everything else is132    found by name, so flat and snake_case keys resolve too."""133    payload = {'data': [{'subtopicExamTocNavigation': [134        {'exam_name': 'CBSE Grade 7',135         'subjectDetails': {'subject_name': 'Mathematics'},136         'chapterDetails': {'chapterName': 'Exponents and Powers'},137         'topicName': 'Laws of exponents'}]}]}138    assert mod._toc_chain(mod._exam_toc_names(payload)) == CHAIN_1139 140 141def test_a_row_never_borrows_a_sibling_rows_name(mod):142    """Rows sit in a list; the search descends dicts only, so a row missing a143    level reports it blank instead of stealing the next row's."""144    payload = {'data': [{'subtopicExamTocNavigation': [145        {'examDetails': {'examName': 'CBSE Grade 7'},146         'chapterDetails': {'chapterId': 700, 'chapterName': 'Exponents and Powers'}},147        row(41, 'CBSE Grade 8', 'Mathematics',148            701, 'Power Play.', 801, 'Laws of Exponents'),149    ]}]}150    out = mod._exam_toc_names(payload)151    assert out == {'exam': 'CBSE Grade 7', 'chapter': 'Exponents and Powers'}152    assert mod._toc_chain(out) == 'CBSE Grade 7 > Exponents and Powers'153 154 155def test_a_row_with_no_chapter_or_topic_is_skipped(mod):156    payload = {'data': [{'subtopicExamTocNavigation': [157        {'examDetails': {'examName': 'CBSE Grade 7'}, 'chapterDetails': {}},158        row(41, 'CBSE Grade 8', 'Mathematics',159            701, 'Power Play.', 801, 'Laws of Exponents'),160    ]}]}161    assert mod._exam_toc_names(payload)['chapter'] == 'Power Play.'162 163 164@pytest.mark.parametrize('payload', [165    {'data': []},166    {'data': [{'subtopicMasterTocNavigation': MASTER}]},     # master-only question167    {'data': [{'subtopicExamTocNavigation': None}]},168])169def test_no_rows_falls_back_to_master(mod, payload):170    assert mod._exam_toc_names(payload) == {}171 172 173def test_nav_cache_collapses_repeat_subtopics(app_mod, monkeypatch):174    """A pasted batch is usually one chapter, so the same subtopics repeat. The175    per-job cache must turn those into a single community call."""176    calls = []177 178    def fake_get(path, headers, params=None, json_body=None, method='GET'):179        calls.append((params or {}).get('nodeIds'))180        return PAYLOAD181 182    monkeypatch.setattr(app_mod, '_comm_headers', lambda token, uid: {})183    monkeypatch.setattr(app_mod, '_comm_get', fake_get)184 185    cache = {}186    for _ in range(20):187        names = app_mod._resolve_toc_names('tok', 'uid', {188            'grade': [7], 'subject_id': [4], 'chapter_id': [700],189            'topic_id': [800], 'sub_topic_id': [900]}, nav_cache=cache)190        assert names['chapter'] == 'Exponents and Powers'   # cached rows resolve191    assert calls == [900], f'expected 1 call, got {len(calls)}'192 193 194def test_nav_cache_is_per_subtopic_not_global(app_mod, monkeypatch):195    """Different subtopics must still each be fetched — the cache keys on the196    subtopic, it doesn't just memoise the first answer."""197    calls = []198 199    def fake_get(path, headers, params=None, json_body=None, method='GET'):200        calls.append((params or {}).get('nodeIds'))201        return PAYLOAD202 203    monkeypatch.setattr(app_mod, '_comm_headers', lambda token, uid: {})204    monkeypatch.setattr(app_mod, '_comm_get', fake_get)205 206    cache = {}207    for stid in (900, 901, 900, 902, 901):208        app_mod._resolve_toc_names('tok', 'uid', {209            'grade': [7], 'subject_id': [4], 'sub_topic_id': [stid]},210            nav_cache=cache)211    assert calls == [900, 901, 902]212 213 214def test_workers_stay_in_a_sane_range(app_mod):215    assert 1 <= app_mod.RESOLVE_TOC_WORKERS <= 32216 217 218@pytest.mark.parametrize('raw, expected', [219    (None, 12), ('', 12), ('garbage', 12),      # bad input keeps the default220    ('1', 1), ('24', 24),221    ('0', 1), ('-5', 1), ('999', 32),           # clamped222])223def test_worker_env_override_is_clamped(app_mod, monkeypatch, raw, expected):224    if raw is None:225        monkeypatch.delenv('RESOLVE_TOC_WORKERS', raising=False)226    else:227        monkeypatch.setenv('RESOLVE_TOC_WORKERS', raw)228    assert app_mod._env_int('RESOLVE_TOC_WORKERS', 12, 1, 32) == expected229 230 231def test_master_names_survive_when_there_are_no_rows(app_mod, monkeypatch):232    payload = {'data': [{'subtopicName': 'Laws of exponents',233                         'subtopicMasterTocNavigation': MASTER}]}234    monkeypatch.setattr(app_mod, '_comm_headers', lambda token, uid: {})235    monkeypatch.setattr(app_mod, '_comm_get', lambda *a, **kw: payload)236 237    names = app_mod._resolve_toc_names('tok', 'uid', {238        'grade': [7], 'subject_id': [4], 'chapter_id': [], 'topic_id': [],239        'sub_topic_id': [900]})240 241    assert names['chapter'] == 'Exponents (Master)'242    assert names['topic'] == 'Powers (Master)'243    assert names['exam'] == ''244