import json import httpx import pytest from wisegrade.llm import DeepSeekClient, FactResult, ModelError from wisegrade.storage import scan def test_scan_selected_student_folder_prefills_cover_identity(tmp_path, monkeypatch): from wisegrade import storage folder = tmp_path / "test" / "示例" folder.mkdir(parents=True) for name in ("阶段报告1.pdf", "阶段报告2.pdf", "阶段报告3.pdf", "最终报告.pdf", "汇报PPT.pdf"): (folder / name).write_bytes(b"fixture") monkeypatch.setattr(storage, "first_text", lambda path: "学号:9000000001\n姓名:学生甲\n题目:机器人系统设计") result = scan("test/示例", root=tmp_path) assert len(result["projects"]) == 1 project = result["projects"][0] assert project["relative_dir"] == "test/示例" assert project["student_no"] == "9000000001" assert project["name"] == "学生甲" assert project["topic"] == "机器人系统设计" assert project["status"] == "needs_confirmation" assert len(project["files"]) == 5 assert project["missing"] == [] assert scan("test", root=tmp_path)["projects"] == result["projects"] def test_scan_conflicting_covers_need_confirmation(tmp_path, monkeypatch): from wisegrade import storage folder = tmp_path / "示例" folder.mkdir() (folder / "阶段报告1.pdf").write_bytes(b"fixture") (folder / "最终报告.pdf").write_bytes(b"fixture") monkeypatch.setattr(storage, "first_text", lambda path: "学号:" + ("9000000001" if path.name.startswith("阶段") else "9000000002")) project = scan("示例", root=tmp_path)["projects"][0] assert project["student_no"] == "" assert project["warnings"] assert project["status"] == "needs_confirmation" def test_scan_parent_skips_empty_folders(tmp_path, monkeypatch): from wisegrade import storage (tmp_path / "empty").mkdir() folder = tmp_path / "9000000001_学生甲_机器人系统" folder.mkdir() (folder / "最终报告.pdf").write_bytes(b"fixture") monkeypatch.setattr(storage, "first_text", lambda path: "") projects = scan(".", root=tmp_path)["projects"] assert len(projects) == 1 assert projects[0]["status"] == "recognized" @pytest.mark.parametrize("code, hint", [(401, "认证"), (429, "限流"), (503, "服务")]) def test_model_http_error_is_actionable_and_redacted(code, hint): def respond(request): return httpx.Response(code, json={"error": "SECRET_ECHO"}, request=request) client = DeepSeekClient(api_key="SECRET_KEY", retries=0, transport=httpx.MockTransport(respond)) with pytest.raises(ModelError) as caught: client.json_call("测试", {}, FactResult) assert str(code) in str(caught.value) assert hint in str(caught.value) assert "SECRET" not in str(caught.value) def test_model_connect_error_explains_network_without_secret(): def respond(request): raise httpx.ConnectError("SECRET_PROXY", request=request) client = DeepSeekClient(api_key="SECRET_KEY", retries=0, transport=httpx.MockTransport(respond)) with pytest.raises(ModelError, match="网络.*DNS|DNS.*网络") as caught: client.json_call("测试", {}, FactResult) assert "SECRET" not in str(caught.value) def test_json_call_supplies_fact_schema(): def respond(request): body = json.loads(request.content) assert "student_no" in body["messages"][0]["content"] assert "goals" in body["messages"][0]["content"] return httpx.Response(200, json={"choices": [{"message": {"content": '{"goals": ["控制系统"]}'}}]}, request=request) client = DeepSeekClient(api_key="test", retries=0, transport=httpx.MockTransport(respond)) assert client.json_call("提取事实", {}, FactResult).goals == ["控制系统"]