"""业务流程编排:缺项收集、多步串行推进、角色准入、提示词拼装。""" from __future__ import annotations import pytest from backend.orchestrator import orchestrator as orc from backend.orchestrator.orchestrator import Orchestrator class TestMissingInputs: def test_lists_only_required_and_unfilled(self, business): task = {"inputs": {"class_name": "计科2301班"}} missing = {m["name"] for m in orc._missing_inputs(task, business)} assert missing == {"observe_date", "course_name"} # location 不是必填 def test_nothing_missing_when_all_required_present(self, business): task = {"inputs": {"class_name": "计科2301班", "observe_date": "2026-09-17", "course_name": "高等数学"}} assert orc._missing_inputs(task, business) == [] def test_empty_string_counts_as_missing(self, business): task = {"inputs": {"class_name": "", "observe_date": "2026-09-17", "course_name": "高等数学"}} assert [m["name"] for m in orc._missing_inputs(task, business)] == ["class_name"] def test_labels_are_joined_for_the_user_facing_question(self, business): missing = orc._missing_inputs({"inputs": {}}, business) assert orc._missing_labels(missing) == "班级、听课日期、课程名称" def test_business_without_required_inputs(self, knowledge): assert orc._missing_inputs({"inputs": {}}, knowledge["businesses"][1]) == [] class TestStepSelection: def test_disabled_steps_are_skipped(self, business): assert [s["step_id"] for s in orc._enabled_steps(business)] == ["fill_and_submit"] def test_active_step_is_the_first_enabled_one(self, business): assert orc._active_step(business)["step_id"] == "fill_and_submit" def test_step_index_advances_through_the_workflow(self): business = {"workflow": [{"step_id": "a"}, {"step_id": "b"}, {"step_id": "c"}]} assert orc._current_step(business, {"step_index": 0})["step_id"] == "a" assert orc._current_step(business, {"step_index": 2})["step_id"] == "c" def test_step_index_beyond_the_end_clamps_to_the_last_step(self): business = {"workflow": [{"step_id": "a"}, {"step_id": "b"}]} assert orc._current_step(business, {"step_index": 99})["step_id"] == "b" def test_missing_step_index_starts_at_the_beginning(self, business): assert orc._current_step(business, {})["step_id"] == "fill_and_submit" def test_business_with_no_enabled_steps(self): assert orc._current_step({"workflow": [{"step_id": "a", "enabled": False}]}, {}) is None class TestSystemResolution: def test_business_system_is_used_by_default(self, knowledge, business): assert orc._system_for(business, knowledge)["id"] == "xgxt" def test_step_can_override_the_system(self, knowledge, business): step = {"step_id": "x", "system": "lib"} assert orc._system_for(business, knowledge, step)["id"] == "lib" def test_all_systems_a_business_touches(self, knowledge, business): business = {**business, "workflow": [{"step_id": "a", "system": "lib"}, {"step_id": "b"}]} assert {s["id"] for s in orc._systems_for_business(business, knowledge)} == {"xgxt", "lib"} def test_unknown_system_id_does_not_raise(self, knowledge): assert orc._system_by_id(knowledge, "不存在的系统") == {} or isinstance(orc._system_by_id(knowledge, "不存在的系统"), dict) class TestRoleAdmission: @staticmethod def _patch_account(monkeypatch, account): from backend.db import database as db monkeypatch.setattr(db, "get_account", lambda student_id, system_id: account) def test_matching_role_passes(self, knowledge, business, monkeypatch): self._patch_account(monkeypatch, {"role": "辅导员", "username": "t001"}) assert Orchestrator._role_admission({"student_id": "s1"}, knowledge, business) is None def test_wrong_role_is_refused_with_both_roles_named(self, knowledge, business, monkeypatch): self._patch_account(monkeypatch, {"role": "学生", "username": "s001"}) msg = Orchestrator._role_admission({"student_id": "s1"}, knowledge, business) assert msg and "辅导员" in msg and "学生" in msg and "办不了" in msg def test_no_account_does_not_block(self, knowledge, business, monkeypatch): # 没录过账号时走系统的 fallback 登录(测试环境是快捷登录),不能在这里拦掉 self._patch_account(monkeypatch, None) assert Orchestrator._role_admission({"student_id": "s1"}, knowledge, business) is None def test_account_without_role_does_not_block(self, knowledge, business, monkeypatch): self._patch_account(monkeypatch, {"role": "", "username": "t001"}) assert Orchestrator._role_admission({"student_id": "s1"}, knowledge, business) is None def test_business_without_a_role_requirement_never_blocks(self, knowledge, monkeypatch): self._patch_account(monkeypatch, {"role": "学生", "username": "s001"}) generic = knowledge["businesses"][1] assert Orchestrator._role_admission({"student_id": "s1"}, knowledge, generic) is None class TestTemplateRendering: def test_fills_declared_values(self): assert orc._render("填写 {class_name} 的日志", {"class_name": "计科2301班"}) == "填写 计科2301班 的日志" def test_unknown_placeholder_is_left_alone(self): # 缺字段时不能抛 KeyError,也不能把占位符变成 None assert orc._render("填写 {class_name} 和 {未知字段}", {"class_name": "x"}) == "填写 x 和 {未知字段}" def test_template_without_placeholders(self): assert orc._render("没有占位符", {}) == "没有占位符" class TestPromptBlocks: def test_inputs_block_lists_user_supplied_values(self, business): block = orc._inputs_block(business, {"class_name": "计科2301班", "course_name": "高等数学"}) assert "计科2301班" in block and "高等数学" in block def test_inputs_block_hides_internal_keys(self, business): block = orc._inputs_block(business, {"class_name": "计科2301班", "business_id": "counselor_worklog", "step_index": 1, "pending_blocker": {"id": "x"}}) assert "计科2301班" in block for internal in ("business_id", "step_index", "pending_blocker"): assert internal not in block def test_artifacts_block_carries_previous_step_results(self): block = orc._artifacts_block({"entry_url": "https://example.edu/new", "form_fields": "班级、日期"}) assert "entry_url" in block and "https://example.edu/new" in block def test_empty_artifacts_add_nothing(self): assert orc._artifacts_block({}) == "" class TestLoginInstructions: def test_with_an_account_the_prompt_uses_placeholders_not_the_password(self, knowledge): system = knowledge["systems"][0] account = {"username": "t001", "password_enc": "irrelevant", "role": "辅导员"} text = orc._login_instructions(system, account, "辅导员") assert "x_username_xgxt" in text and "x_password_xgxt" in text assert "irrelevant" not in text def test_without_an_account_it_describes_the_fallback(self, knowledge): text = orc._login_instructions(knowledge["systems"][0], None, "辅导员") assert text # 快捷登录说明 assert "x_password_xgxt" not in text @pytest.mark.parametrize("task,expected", [ ({"inputs": {"business_id": "counselor_worklog"}}, "counselor_worklog"), ({"inputs": {}}, None), ]) def test_business_by_id(knowledge, task, expected): found = orc._business_by_id(knowledge, task["inputs"].get("business_id")) assert (found or {}).get("id") == expected