assistant/tests/test_workflow.py

156 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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