CoolFace
Apppublic

sahil-12kumar/IL_CMS_Tools

sourceHugging Faceupdated 7d agoView on Hugging Face
1likes
test_multi_subject.py237 linesDownload Raw Back to tests
1"""Multi-subject support: AI Tagging and LO Classifier accept several subjects.2 3The taxonomy dicts were already subject-keyed ({subject: {chapter: ...}}), so the4unit tests here cover the pure multi-subject paths in ai_assist / lo_assist that5the new chips UI and the /tag/prep, /tag/apply, /lo/prep, /lo/apply routes rely on.6"""7import json8 9import ai_assist10import lo_assist11 12 13TAX = {14    "Physics": {15        "Kinematics": {16            "Motion in a Straight Line": {17                "Uniform Motion": ["Define uniform motion", "Apply v = u + at"],18            },19        },20    },21    "Chemistry": {22        "Chemical Bonding": {23            "Basics": {24                "Octet Rule": ["State the octet rule"],25            },26        },27    },28}29 30OPTIONS = {31    "objective": ["Theory"],32    "bloom_taxonomy": ["Knowledge"],33    "relevance": ["Relevant to NEET"],34    "concept_level": ["Chapter level"],35    "syllabus": ["NCERT-based"],36    "source": [],37}38 39 40def _fixture_tax():41    return TAX42 43 44# ── ai_assist (AI Tagging) ────────────────────────────────────────────────────45def test_ai_parse_and_validate_multisubject():46    js = json.dumps([47        {"question_id": "ILQ-1", "subject": "Physics",48         "chapter": "Kinematics", "topic": "Motion in a Straight Line",49         "subtopic": "Uniform Motion", "grade": "Grade 11", "difficulty": "easy",50         "objective": "Theory", "bloom_taxonomy": "Knowledge",51         "relevance": "Relevant to NEET", "concept_level": "Chapter level",52         "syllabus": "NCERT-based"},53        {"question_id": "ILQ-2", "subject": "Chemistry",54         "chapter": "Chemical Bonding", "topic": "Basics",55         "subtopic": "Octet Rule", "grade": "Grade 12", "difficulty": "moderate"},56    ])57    rows, warnings = ai_assist.parse_and_validate(js, TAX, OPTIONS, "NEET")58    assert {r["question_id"]: r["subject"] for r in rows} == {59        "ILQ-1": "Physics", "ILQ-2": "Chemistry"}60    assert not warnings61 62 63def test_ai_parse_and_validate_unknown_subject_warns():64    js = json.dumps([{"question_id": "ILQ-9", "subject": "Biology",65                      "chapter": "X", "topic": "Y", "subtopic": "Z"}])66    rows, warnings = ai_assist.parse_and_validate(js, TAX, OPTIONS, "NEET")67    assert rows[0]["subject"] == "Biology"          # kept, only warned68    assert any("TOC path not found" in w for w in warnings)69 70 71# ── lo_assist (LO Classifier) ─────────────────────────────────────────────────72def test_lo_merged(monkeypatch):73    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)74    m = lo_assist.merged(["Physics", "Chemistry"], chapter_filter=["kin"])75    assert list(m["Physics"]) == ["Kinematics"]76    assert m["Chemistry"] == {}                      # "Chemical Bonding" has no "kin"77    full = lo_assist.merged(["Physics", "Chemistry"])78    assert set(full) == {"Physics", "Chemistry"}79 80 81def test_lo_parse_and_validate_multisubject(monkeypatch):82    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)83    js = json.dumps([84        {"question_id": "ILQ-1", "subject": "Physics",85         "chapter": "Kinematics", "topic": "Motion in a Straight Line",86         "subtopic": "Uniform Motion", "lo": "Define uniform motion"},87        {"question_id": "ILQ-2", "subject": "Chemistry",88         "chapter": "Chemical Bonding", "topic": "Basics",89         "subtopic": "Octet Rule", "lo": "State the octet rule"},90    ])91    valid, rejected, warnings = lo_assist.parse_and_validate(js, ["Physics", "Chemistry"])92    assert not rejected and not warnings93    assert {r["question_id"]: r["subject"] for r in valid} == {94        "ILQ-1": "Physics", "ILQ-2": "Chemistry"}95    assert valid[0]["chapter"] == "Kinematics"96    assert valid[1]["chapter"] == "Chemical Bonding"97 98 99def test_lo_parse_and_validate_single_subject_backwards_compat(monkeypatch):100    """A row without a subject defaults to the single selected subject."""101    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)102    js = json.dumps([103        {"question_id": "ILQ-3", "chapter": "Kinematics",104         "topic": "Motion in a Straight Line", "subtopic": "Uniform Motion",105         "lo": "Define uniform motion"},106    ])107    valid, rejected, _ = lo_assist.parse_and_validate(js, "Physics")108    assert not rejected109    assert valid[0]["subject"] == "Physics"110 111 112def test_lo_parse_and_validate_missing_subject_rejected(monkeypatch):113    """With several subjects selected, a row must say which subject it belongs to."""114    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)115    js = json.dumps([116        {"question_id": "ILQ-4", "chapter": "Kinematics",117         "topic": "Motion in a Straight Line", "subtopic": "Uniform Motion",118         "lo": "Define uniform motion"},119    ])120    valid, rejected, _ = lo_assist.parse_and_validate(js, ["Physics", "Chemistry"])121    assert not valid122    assert len(rejected) == 1123    assert "subject" in rejected[0]["reason"]124 125 126def test_lo_parse_and_validate_unknown_subject_rejected(monkeypatch):127    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)128    js = json.dumps([129        {"question_id": "ILQ-5", "subject": "Biology",130         "chapter": "Kinematics", "topic": "Motion in a Straight Line",131         "subtopic": "Uniform Motion", "lo": "Define uniform motion"},132    ])133    valid, rejected, _ = lo_assist.parse_and_validate(js, ["Physics", "Chemistry"])134    assert not valid135    assert len(rejected) == 1136 137 138def test_lo_build_prompt_multisubject(monkeypatch):139    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)140    tree = lo_assist.merged(["Physics", "Chemistry"])141    p = lo_assist.build_prompt(142        [{"question_id": "ILQ-1", "question_text": "A sample question."}],143        ["Physics", "Chemistry"], tree)144    assert "### Physics" in p and "### Chemistry" in p145    assert "subject, question_id, chapter" in p      # output keys now include subject146 147 148# ── lo_assist: question level (easy / moderate / difficult) ───────────────────149def _lo_row(**over):150    row = {"question_id": "ILQ-1", "subject": "Physics", "chapter": "Kinematics",151           "topic": "Motion in a Straight Line", "subtopic": "Uniform Motion",152           "lo": "Define uniform motion"}153    row.update(over)154    return json.dumps([row])155 156 157def test_lo_level_valid(monkeypatch):158    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)159    valid, rejected, warnings = lo_assist.parse_and_validate(160        _lo_row(level="Moderate"), "Physics")161    assert not rejected and not warnings162    assert valid[0]["level"] == "moderate"       # normalised to the sheet vocab163 164 165def test_lo_level_difficulty_alias(monkeypatch):166    """The CMS calls this difficulty_level, so models reach for "difficulty"."""167    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)168    valid, _, warnings = lo_assist.parse_and_validate(169        _lo_row(difficulty="difficult"), "Physics")170    assert valid[0]["level"] == "difficult"171    assert not warnings172 173 174def test_lo_level_invalid_warns_but_keeps_row(monkeypatch):175    """A bad level must not throw away a row whose taxonomy path validated."""176    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)177    valid, rejected, warnings = lo_assist.parse_and_validate(178        _lo_row(level="hard"), "Physics")179    assert not rejected180    assert valid[0]["level"] == ""181    assert len(warnings) == 1 and "hard" in warnings[0]182 183 184def test_lo_level_missing_is_silent(monkeypatch):185    """No level at all is blank WITHOUT a warning — only a wrong one warns."""186    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)187    valid, rejected, warnings = lo_assist.parse_and_validate(_lo_row(), "Physics")188    assert not rejected and not warnings189    assert valid[0]["level"] == ""190 191 192def test_lo_build_prompt_has_level_rubric(monkeypatch):193    monkeypatch.setattr(lo_assist, "taxonomy", _fixture_tax)194    p = lo_assist.build_prompt(195        [{"question_id": "ILQ-1", "question_text": "A sample question."}],196        "Physics", lo_assist.merged(["Physics"]))197    assert "easy | moderate | difficult" in p198    assert "single concept, or a direct formula" in p199    assert "two or three concepts" in p200    assert "multiple concepts combined" in p201    assert "subtopic, lo, level" in p             # level is an OUTPUT key202 203 204def test_lo_bank_cols_has_level_between_lo_and_cc():205    cols = lo_assist.BANK_COLS206    assert cols.index("lo") + 1 == cols.index("level") == cols.index("cc_code") - 1207 208 209def test_lo_bank_append_backfills_a_missing_column(tmp_path, monkeypatch):210    """A bank written before `level` existed must GAIN the column on its next211    save. bank_append deliberately appends by the FILE's header, so without the212    backfill every pre-existing workbook would drop the value forever."""213    from openpyxl import Workbook, load_workbook214 215    monkeypatch.setattr(lo_assist, "BANK_DIR", str(tmp_path))216    old_cols = [c for c in lo_assist.BANK_COLS if c != "level"]   # pre-level bank217    wb = Workbook()218    ws = wb.active219    ws.append(old_cols)220    ws.append(["2026-01-01 09:00", "someone", "Physics", "OLD-1", "old q",221               "Kinematics", "T", "S", "L", "", "", ""])222    wb.save(lo_assist.bank_path("Physics"))223 224    lo_assist.bank_append([{"subject": "Physics", "question_id": "NEW-1",225                            "chapter": "Kinematics", "topic": "T", "subtopic": "S",226                            "lo": "L", "level": "moderate", "cc_code": "",227                            "alt_los": [], "note": ""}], added_by="test")228 229    ws = load_workbook(lo_assist.bank_path("Physics")).active230    header = [str(c.value or "") for c in ws[1]]231    assert header[:len(old_cols)] == old_cols        # existing columns unmoved232    assert header.count("level") == 1                # appended exactly once233    lvl = header.index("level") + 1234    assert ws.cell(row=2, column=lvl).value in (None, "")   # historic row blank235    assert ws.cell(row=2, column=header.index("question_id") + 1).value == "OLD-1"236    assert ws.cell(row=3, column=lvl).value == "moderate"   # new row populated237