diff --git a/core/types.py b/core/types.py index a3f2938..c8838c7 100644 --- a/core/types.py +++ b/core/types.py @@ -23,3 +23,31 @@ class LLMResponse: max_inter_token_ms: float | None cache_hit: bool call_id: str + + +@dataclass(frozen=True) +class GeneratedQuestion: + """单条生成/加载的题目。 + + 跨层共享类型,被 core/evolution/ 和 app/harness/、app/question_gen/ 使用。 + frozen=True 确保题目不可变。 + + 属性: + question_id: 题目唯一标识。 + video_id: 所属视频标识。 + task_type: 题型(如 "Action Reasoning")。 + question: 题目文本。 + options: 选项元组(如 ("A. ...", "B. ...", "C. ...", "D. ..."))。 + answer: 正确答案字母(如 "B")。 + source_nodes: 来源节点 ID 元组。 + difficulty: 难度等级。 + """ + + question_id: str + video_id: str + task_type: str + question: str + options: tuple[str, ...] + answer: str + source_nodes: tuple[str, ...] + difficulty: str diff --git a/tests/unit/test_core_types.py b/tests/unit/test_core_types.py index 45947e2..0439f02 100644 --- a/tests/unit/test_core_types.py +++ b/tests/unit/test_core_types.py @@ -1,9 +1,10 @@ """core/types.py 单元测试。""" + from __future__ import annotations import pytest -from core.types import LLMResponse +from core.types import GeneratedQuestion, LLMResponse class TestLLMResponse: @@ -42,10 +43,58 @@ class TestLLMResponse: def test_cache_hit_response_has_none_ttft(self) -> None: resp = LLMResponse( - content="cached", thinking="", model="m", provider="p", - prompt_tokens=0, completion_tokens=0, latency_ms=1, - ttft_ms=None, max_inter_token_ms=None, cache_hit=True, call_id="c", + content="cached", + thinking="", + model="m", + provider="p", + prompt_tokens=0, + completion_tokens=0, + latency_ms=1, + ttft_ms=None, + max_inter_token_ms=None, + cache_hit=True, + call_id="c", ) assert resp.ttft_ms is None assert resp.max_inter_token_ms is None assert resp.cache_hit is True + + +class TestGeneratedQuestion: + @pytest.fixture() + def sample_question(self) -> GeneratedQuestion: + return GeneratedQuestion( + question_id="719-1", + video_id="B7Hh0PY1kks", + task_type="Action Reasoning", + question="What are the differing motivations?", + options=("A. Option 1", "B. Option 2", "C. Option 3", "D. Option 4"), + answer="B", + source_nodes=(), + difficulty="medium", + ) + + def test_frozen_prevents_mutation(self, sample_question: GeneratedQuestion) -> None: + with pytest.raises(AttributeError): + sample_question.question = "篡改" + + def test_all_fields_accessible(self, sample_question: GeneratedQuestion) -> None: + assert sample_question.question_id == "719-1" + assert sample_question.video_id == "B7Hh0PY1kks" + assert sample_question.task_type == "Action Reasoning" + assert sample_question.question == "What are the differing motivations?" + assert sample_question.options == ( + "A. Option 1", + "B. Option 2", + "C. Option 3", + "D. Option 4", + ) + assert sample_question.answer == "B" + assert sample_question.source_nodes == () + assert sample_question.difficulty == "medium" + + def test_options_is_tuple(self, sample_question: GeneratedQuestion) -> None: + assert isinstance(sample_question.options, tuple) + + def test_source_nodes_is_tuple(self, sample_question: GeneratedQuestion) -> None: + assert isinstance(sample_question.source_nodes, tuple)