diff --git a/decider/systemone.py b/decider/systemone.py index 02b1d18..9950e1d 100644 --- a/decider/systemone.py +++ b/decider/systemone.py @@ -54,7 +54,9 @@ def render_question(spec): raise ValueError(f"score criteria: an ordered list of 2..{MAX_LEVELS} level descriptions") names = list(range(len(crit))); opts = [f"{i}: {_txt(c)}" for i, c in enumerate(crit)] elif t in ("noul", "bool"): - names = [False, True]; c = crit or {} + if crit is not None and not isinstance(crit, dict): + raise ValueError("noul criteria: a map of optional true/false descriptions") + names = [False, True]; c = crit if crit is not None else {} f, tr = c.get("false", c.get(False)), c.get("true", c.get(True)) opts = ["no" if f in (None, "") else f"no: {_txt(f)}", "yes" if tr in (None, "") else f"yes: {_txt(tr)}"] else: diff --git a/tests/test_serve_http.py b/tests/test_serve_http.py index f9ab991..a328f30 100644 --- a/tests/test_serve_http.py +++ b/tests/test_serve_http.py @@ -142,6 +142,20 @@ async def fn(cl): assert r.status_code == 422 and r.json() == {"detail": "choice criteria: a map of 2..255 options"} +@pytest.mark.parametrize("question_type", ["noul", "bool"]) +@pytest.mark.parametrize("criteria", [["bad"], []]) +def test_invalid_noul_criteria_is_422_with_detail(served, question_type, criteria): + eng, run = served + + async def fn(cl): + return await cl.post("/v1/systemone", json={"state": "s", "questions": {"q": { + "type": question_type, "instructions": "Is this true?", "criteria": criteria}}}) + r = run(fn) + assert r.status_code == 422 + assert r.json() == {"detail": "noul criteria: a map of optional true/false descriptions"} + assert eng.stats["forwards"] == 0 + + def test_shared_path_is_used_for_long_multi_question_states(served, monkeypatch): eng, run = served monkeypatch.setattr(serve, "SHARED", True); monkeypatch.setattr(serve, "SHARED_MIN_TOKENS", 100) diff --git a/tests/test_systemone.py b/tests/test_systemone.py index 842c18f..869e0c4 100644 --- a/tests/test_systemone.py +++ b/tests/test_systemone.py @@ -36,6 +36,13 @@ def test_noul_with_and_without_criteria(): assert desc["options"] == ["no: anything else", "yes: money back"] +@pytest.mark.parametrize("question_type", ["noul", "bool"]) +@pytest.mark.parametrize("criteria", [["bad"], [], "bad", False]) +def test_noul_criteria_must_be_a_map(question_type, criteria): + with pytest.raises(ValueError, match="noul criteria: a map of optional true/false descriptions"): + s1.render_question({"type": question_type, "instructions": "Refund asked?", "criteria": criteria}) + + def test_render_state_serialises_json_and_indexes_long_arrays(): assert s1.render_state("plain text") == "plain text" short = json.loads(s1.render_state({"items": [{"a": 1}, {"a": 2}]}))