156 lines
7.7 KiB
Python
156 lines
7.7 KiB
Python
"""业务流程编排:缺项收集、多步串行推进、角色准入、提示词拼装。"""
|
||
|
||
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
|