import pytest from rest_framework.exceptions import ValidationError from accounts.models import User from math_life.models import Story, StoryChoice, StoryRun, StoryVersion from math_life.services import ( make_choice, score_mathbti, start_story, validate_story_content, ) @pytest.fixture def mathbti_definition(): return { "axes": [{"id": "style"}, {"id": "purpose"}], "scoring": {"axes": ["style", "purpose"], "cutoff": 1}, "questions": [ { "id": 1, "axis": "style", "options": [{"score": 0}, {"score": 2}], }, { "id": 2, "axis": "purpose", "options": [{"score": 0}, {"score": 2}], }, ], } def test_score_mathbti_按轴汇总并生成身份编码(mathbti_definition): code, scores = score_mathbti(mathbti_definition, [1, 0]) assert code == "10" assert scores == {"style": 2, "purpose": 0} def test_score_mathbti_答案数量不一致时拒绝(mathbti_definition): with pytest.raises(ValidationError, match="答案数量"): score_mathbti(mathbti_definition, [1]) def test_score_mathbti_负数选项索引必须拒绝(mathbti_definition): with pytest.raises(ValidationError, match="答案无效"): score_mathbti(mathbti_definition, [-1, 0]) def test_validate_story_content_报告缺失引用与不可达节点(): document = { "start_node": "start", "nodes": { "start": {"choices": [{"text": "错误入口", "next": "missing"}]}, "orphan": {"choices": []}, }, } errors = validate_story_content(document) assert any("不存在的节点 missing" in error for error in errors) assert any("不可达节点: orphan" in error for error in errors) @pytest.fixture def story_setup(db): user = User.objects.create_user( username="story_user", password="StrongPass_2026", nickname="剧情用户", ) story = Story.objects.create(slug="test-story", title="测试人生") version = StoryVersion.objects.create( story=story, version=1, is_published=True, content={ "start_node": "start", "nodes": { "start": { "scene": "起点", "choices": [ { "text": "向左", "next": "left_end", "effects": {"energy": -1, "favorability": {"高斯": 2}}, }, { "text": "向右", "next": "right_end", "effects": {"energy": 2}, }, ], }, "left_end": {"scene": "左结局", "choices": []}, "right_end": {"scene": "右结局", "choices": []}, }, }, ) return user, story, version @pytest.mark.django_db def test_make_choice_应用嵌套效果完成结局且幂等(story_setup): user, story, _ = story_setup started = start_story(user, story) result = make_choice( run_id=started["run_id"], user=user, choice_index=0, idempotency_key="choice-1", ) replay = make_choice( run_id=started["run_id"], user=user, choice_index=0, idempotency_key="choice-1", ) assert result["status"] == StoryRun.Status.COMPLETED assert result["current_node"] == "left_end" assert result["state"] == {"energy": -1, "favorability": {"高斯": 2}} assert replay["current_node"] == "left_end" assert StoryChoice.objects.count() == 1 @pytest.mark.django_db def test_make_choice_负数索引必须拒绝且存档不变(story_setup): user, story, _ = story_setup started = start_story(user, story) with pytest.raises(ValidationError, match="选项不存在"): make_choice( run_id=started["run_id"], user=user, choice_index=-1, idempotency_key="invalid-choice", ) run = StoryRun.objects.get(id=started["run_id"]) assert run.current_node == "start" assert StoryChoice.objects.count() == 0