Files
wehub-resource-sync e4dcfc49aa
Tests / Lint and Format (push) Waiting to run
Tests / Web Node Tests (push) Waiting to run
Tests / Import Check (Python 3.11) (push) Waiting to run
Tests / Import Check (Python 3.12) (push) Waiting to run
Tests / Import Check (Python 3.13) (push) Waiting to run
Tests / Import Check (Python 3.14) (push) Waiting to run
Tests / Python Tests (Python 3.11) (push) Blocked by required conditions
Tests / Python Tests (Python 3.12) (push) Blocked by required conditions
Tests / Python Tests (Python 3.13) (push) Blocked by required conditions
Tests / Python Tests (Python 3.14) (push) Blocked by required conditions
Tests / Test Summary (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

347 lines
10 KiB
Python

"""Unit tests for ``ask_user`` payload building.
Covers the v3 schema (option = ``{label, description}``, ``header``,
``multi_select``), the v2 plain-string-options shape, and the legacy
single-question shorthand which auto-wraps into a one-element list.
"""
from __future__ import annotations
from deeptutor.tools.ask_user import (
MAX_HEADER_CHARS,
MAX_OPTION_CHARS,
MAX_OPTION_DESC_CHARS,
MAX_OPTIONS,
MAX_QUESTION_CHARS,
MAX_QUESTIONS,
build_ask_user_payload,
)
def _labels(question) -> tuple[str, ...]:
return tuple(o.label for o in question.options)
# ----------------------------- legacy shape -----------------------------
def test_legacy_rejects_empty_question() -> None:
payload, err = build_ask_user_payload(question=" ")
assert payload is None
assert err and "prompt" in err
def test_legacy_question_only_wraps_to_single() -> None:
payload, err = build_ask_user_payload(question="What's your level?")
assert err is None
assert payload is not None
assert len(payload.questions) == 1
only = payload.questions[0]
assert only.id == "q1"
assert only.prompt == "What's your level?"
assert only.options == ()
assert only.allow_free_text is True
assert only.multi_select is False
assert only.header is None
def test_legacy_options_strip_and_dedupe() -> None:
payload, err = build_ask_user_payload(
question="Pick",
options=[" A ", "B", "A", "", "C"],
)
assert err is None
assert payload is not None
assert _labels(payload.questions[0]) == ("A", "B", "C")
def test_legacy_caps_option_count() -> None:
payload, _ = build_ask_user_payload(
question="Pick",
options=[f"opt-{i}" for i in range(MAX_OPTIONS + 5)],
)
assert payload is not None
assert len(payload.questions[0].options) == MAX_OPTIONS
def test_legacy_clips_oversized_question() -> None:
payload, _ = build_ask_user_payload(question="q" * (MAX_QUESTION_CHARS + 50))
assert payload is not None
prompt = payload.questions[0].prompt
assert prompt.endswith("…")
assert len(prompt) <= MAX_QUESTION_CHARS + 1
def test_legacy_clips_oversized_option() -> None:
payload, _ = build_ask_user_payload(
question="q",
options=["x" * (MAX_OPTION_CHARS + 100)],
)
assert payload is not None
assert payload.questions[0].options[0].label.endswith("…")
def test_legacy_rejects_non_list_options() -> None:
payload, err = build_ask_user_payload(question="q", options="not-a-list")
assert payload is None
assert err and "array" in err
# ------------------------------- v2 shape -------------------------------
def test_v2_multiple_questions_assigned_default_ids() -> None:
payload, err = build_ask_user_payload(
questions=[
{"prompt": "scope?"},
{"prompt": "depth?"},
{"prompt": "format?"},
]
)
assert err is None
assert payload is not None
assert payload.question_ids == ("q1", "q2", "q3")
def test_v2_respects_explicit_ids() -> None:
payload, _ = build_ask_user_payload(
questions=[
{"id": "scope", "prompt": "A"},
{"id": "depth", "prompt": "B"},
]
)
assert payload is not None
assert payload.question_ids == ("scope", "depth")
def test_v2_disambiguates_duplicate_ids() -> None:
payload, _ = build_ask_user_payload(
questions=[
{"id": "x", "prompt": "A"},
{"id": "x", "prompt": "B"},
]
)
assert payload is not None
assert payload.question_ids == ("x", "x_2")
def test_v2_caps_question_count() -> None:
payload, _ = build_ask_user_payload(
questions=[{"prompt": f"q{i}"} for i in range(MAX_QUESTIONS + 4)]
)
assert payload is not None
assert len(payload.questions) == MAX_QUESTIONS
def test_v2_intro_included_and_clipped() -> None:
payload, _ = build_ask_user_payload(
questions=[{"prompt": "hi"}],
intro="To tailor the research:",
)
assert payload is not None
assert payload.intro == "To tailor the research:"
def test_v2_empty_intro_becomes_none() -> None:
payload, _ = build_ask_user_payload(
questions=[{"prompt": "hi"}],
intro=" ",
)
assert payload is not None
assert payload.intro is None
def test_v2_rejects_non_object_question() -> None:
payload, err = build_ask_user_payload(questions=["bare string"])
assert payload is None
assert err and "object" in err
def test_v2_rejects_empty_questions_list() -> None:
payload, err = build_ask_user_payload(questions=[])
assert payload is None
assert err and "at least one" in err
def test_v2_rejects_non_array_questions() -> None:
payload, err = build_ask_user_payload(questions={"prompt": "x"})
assert payload is None
assert err and "array" in err
def test_v2_accepts_question_alias_for_prompt() -> None:
"""LLMs sometimes use ``question`` instead of ``prompt`` inside a v2 item."""
payload, err = build_ask_user_payload(questions=[{"question": "what?", "options": ["a", "b"]}])
assert err is None
assert payload is not None
assert payload.questions[0].prompt == "what?"
assert _labels(payload.questions[0]) == ("a", "b")
def test_v2_allow_free_text_default_true() -> None:
payload, _ = build_ask_user_payload(questions=[{"prompt": "x"}])
assert payload is not None
assert payload.questions[0].allow_free_text is True
def test_v2_allow_free_text_can_be_disabled() -> None:
payload, _ = build_ask_user_payload(
questions=[{"prompt": "x", "options": ["a"], "allow_free_text": False}]
)
assert payload is not None
assert payload.questions[0].allow_free_text is False
def test_v2_placeholder_stored() -> None:
payload, _ = build_ask_user_payload(questions=[{"prompt": "x", "placeholder": "type here"}])
assert payload is not None
assert payload.questions[0].placeholder == "type here"
# ----------------------- v3 additions (Claude parity) ---------------------
def test_v3_object_options_with_description() -> None:
payload, err = build_ask_user_payload(
questions=[
{
"prompt": "Audience?",
"options": [
{"label": "Execs (Recommended)", "description": "Conclusion-first"},
{"label": "Engineers", "description": "Technical detail"},
],
}
]
)
assert err is None
assert payload is not None
opts = payload.questions[0].options
assert opts[0].label == "Execs (Recommended)"
assert opts[0].description == "Conclusion-first"
assert opts[1].description == "Technical detail"
def test_v3_mixed_string_and_object_options() -> None:
payload, _ = build_ask_user_payload(
questions=[{"prompt": "x", "options": ["plain", {"label": "rich", "description": "d"}]}]
)
assert payload is not None
opts = payload.questions[0].options
assert opts[0].label == "plain"
assert opts[0].description is None
assert opts[1].label == "rich"
def test_v3_option_description_clipped() -> None:
payload, _ = build_ask_user_payload(
questions=[
{
"prompt": "x",
"options": [{"label": "a", "description": "d" * (MAX_OPTION_DESC_CHARS + 50)}],
}
]
)
assert payload is not None
desc = payload.questions[0].options[0].description
assert desc is not None
assert desc.endswith("…")
assert len(desc) <= MAX_OPTION_DESC_CHARS + 1
def test_v3_header_stored_and_truncated() -> None:
payload, _ = build_ask_user_payload(
questions=[
{"prompt": "x", "header": "Scope"},
{"prompt": "y", "header": "H" * (MAX_HEADER_CHARS + 10)},
]
)
assert payload is not None
assert payload.questions[0].header == "Scope"
assert len(payload.questions[1].header or "") == MAX_HEADER_CHARS
def test_v3_multi_select_parsed_with_camel_alias() -> None:
payload, _ = build_ask_user_payload(
questions=[
{"prompt": "a", "multi_select": True},
{"prompt": "b", "multiSelect": True},
{"prompt": "c"},
]
)
assert payload is not None
assert payload.questions[0].multi_select is True
assert payload.questions[1].multi_select is True
assert payload.questions[2].multi_select is False
def test_v3_drops_model_supplied_other_option() -> None:
payload, _ = build_ask_user_payload(
questions=[{"prompt": "x", "options": ["A", "Other", "其他", "B"]}]
)
assert payload is not None
assert _labels(payload.questions[0]) == ("A", "B")
def test_v3_keeps_other_option_when_free_text_disabled() -> None:
"""Without the automatic free-text row there is no duplicate to drop."""
payload, _ = build_ask_user_payload(
questions=[{"prompt": "x", "options": ["A", "Other"], "allow_free_text": False}]
)
assert payload is not None
assert _labels(payload.questions[0]) == ("A", "Other")
# ------------------------- frontend contract shape ------------------------
def test_to_dict_shape_is_v3_for_frontend() -> None:
payload, _ = build_ask_user_payload(
questions=[
{
"id": "scope",
"prompt": "Q1",
"header": "Scope",
"options": [{"label": "a", "description": "why a"}],
},
{"id": "depth", "prompt": "Q2", "multi_select": True},
],
intro="hi",
)
assert payload is not None
assert payload.to_dict() == {
"intro": "hi",
"questions": [
{
"id": "scope",
"prompt": "Q1",
"header": "Scope",
"multi_select": False,
"options": [{"label": "a", "description": "why a"}],
"allow_free_text": True,
"placeholder": None,
},
{
"id": "depth",
"prompt": "Q2",
"header": None,
"multi_select": True,
"options": [],
"allow_free_text": True,
"placeholder": None,
},
],
}
def test_to_dict_legacy_path_also_emits_v3() -> None:
payload, _ = build_ask_user_payload(question="hi", options=["a", "b"])
assert payload is not None
d = payload.to_dict()
assert d["intro"] is None
assert len(d["questions"]) == 1
assert d["questions"][0]["prompt"] == "hi"
assert d["questions"][0]["options"] == [
{"label": "a", "description": None},
{"label": "b", "description": None},
]