"""任务状态判定:审批归类、用户回答的肯定 / 否定、产物占位文字、终态(取消优先于已提交)。""" from __future__ import annotations import pytest from backend.orchestrator import orchestrator as orc class TestClassifyApproval: @pytest.mark.parametrize("text", ["已通过", "审批通过", "同意", "已审批", "办结", "批准", "已生效", "归档"]) def test_approved(self, text): assert orc._classify_approval(text) == "approved" @pytest.mark.parametrize("text", ["已驳回", "退回", "拒绝", "否决", "作废", "撤销"]) def test_rejected(self, text): assert orc._classify_approval(text) == "rejected" @pytest.mark.parametrize("text", ["待审批", "未审批", "审批中", "待审核", "审核中", "处理中", "已提交"]) def test_pending(self, text): assert orc._classify_approval(text) == "pending" @pytest.mark.parametrize("text", ["审批不通过", "未通过", "审核不通过"]) def test_negated_approval_is_rejected_not_approved(self, text): # 「审批不通过」同时含「通过」和「不通过」,驳回词必须优先,否则驳回会被当成通过 assert orc._classify_approval(text) == "rejected" @pytest.mark.parametrize("text", ["", " ", None, "页面上什么状态都没写"]) def test_unknown(self, text): assert orc._classify_approval(text) == "unknown" def test_pending_beats_approved_when_both_present(self): # 「已提交,等待审批通过」还没批下来,不能记成 approved assert orc._classify_approval("已提交,等待审批通过") == "pending" class TestAnswerPolarity: @pytest.mark.parametrize("answer", ["是", "好的", "可以", "行", "去办吧", "嗯", "对", "同意", "确认提交", "已处理", "继续", "ok", "YES"]) def test_affirmative(self, answer): assert orc._is_affirmative(answer) is True assert orc._is_negative(answer) is False @pytest.mark.parametrize("answer", ["否", "不用了", "别办", "取消", "算了", "先不办", "no"]) def test_negative(self, answer): assert orc._is_negative(answer) is True assert orc._is_affirmative(answer) is False def test_cancel_phrase_from_the_confirm_card(self): # 确认卡片上「取消,不要提交」按钮送出的原话 assert orc._is_negative("取消,不要提交") is True assert orc._is_affirmative("取消,不要提交") is False @pytest.mark.parametrize("answer", ["不错,就这样", "不用担心,继续"]) def test_polite_phrases_are_not_refusals(self, answer): # 「不错」「不用担心」以「不」开头但表示同意,不能当成拒绝 assert orc._is_negative(answer) is False @pytest.mark.parametrize("answer", ["课程是高等数学", "听课地点在教学楼", "我来接管"]) def test_unrelated_answer_is_neither(self, answer): # 回归:「是 / 好 / 行 / 办 / 去 / 对」这类单字词曾经在句中任意位置命中, # 补充资料的回答会被当成「同意先办前置事项」,把任务带去办别的业务 assert orc._is_affirmative(answer) is False assert orc._is_negative(answer) is False def test_single_character_yes_still_works_at_the_start(self): for answer in ("是", "好", "行", "办吧", "去办", "对"): assert orc._is_affirmative(answer) is True class TestMeaningfulArtifact: @pytest.mark.parametrize("value", ["已提交成功", "计科2301班", "0", "查课听课记录"]) def test_real_values(self, value): assert orc._meaningful(value) is True @pytest.mark.parametrize("value", ["未获取", "未找到入口", "无法读取表单", "失败", "暂无", "N/A", "none", "null", "", " ", None]) def test_placeholder_values_do_not_count(self, value): # Agent 有时会把「未获取到表单字段」写进产物,不能据此判定这一步成功了 assert orc._meaningful(value) is False class TestCriteriaMet: def test_any_declared_artifact_is_enough(self): step = {"success_artifacts_any": ["form_fields", "menus"]} assert orc._criteria_met(step, {"menus": "门户有 8 个模块"}) is True def test_placeholder_artifact_does_not_satisfy(self): step = {"success_artifacts_any": ["form_fields"]} assert orc._criteria_met(step, {"form_fields": "未获取"}) is False def test_no_declared_artifacts_means_not_met(self): assert orc._criteria_met({}, {"anything": "x"}) is False def test_unrelated_artifacts_do_not_satisfy(self): step = {"success_artifacts_any": ["form_fields"]} assert orc._criteria_met(step, {"menus": "x"}) is False class TestVerdictOverrides: """Agent 的 is_successful 不稳定,客观产物要能纠正它的自评。""" STEP = {"step_id": "fill", "success_artifacts_any": ["submitted_record", "result_message"]} def test_success_is_left_alone(self): verdict, summary, note = orc.apply_verdict_overrides("success", True, self.STEP, {}, "ok") assert (verdict, summary, note) == ("success", "ok", None) def test_user_cancel_is_not_a_failure(self): # 回归:这一步没声明 success_artifacts_any,取消后 Agent 自评失败就直接落到「办理未完成」; # 同一个取消动作曾经一次记成已完成、一次记成办理未完成 step = {"step_id": "fill"} # 没有 success_artifacts_any verdict, _, note = orc.apply_verdict_overrides("failed", True, step, {"cancelled": "取消,不要提交"}, "未提交") assert verdict == "success" assert note[0] == "用户取消" def test_cancel_override_works_even_with_declared_criteria(self): verdict, _, _ = orc.apply_verdict_overrides("failed", True, self.STEP, {"cancelled": "取消"}, "未提交") assert verdict == "success" def test_placeholder_cancel_value_does_not_override(self): verdict, _, note = orc.apply_verdict_overrides("failed", True, self.STEP, {"cancelled": "无法确定"}, "失败") assert verdict == "failed" and note is None def test_declared_criteria_still_override_a_self_assessed_failure(self): artifacts = {"result_message": "提交成功"} verdict, summary, note = orc.apply_verdict_overrides("failed", True, self.STEP, artifacts, "没把握") assert verdict == "success" assert "产物已满足本步完成判据" in summary assert note[0] == "判定覆盖" def test_genuine_failure_is_not_rescued(self): verdict, _, note = orc.apply_verdict_overrides("failed", True, self.STEP, {"menus": "找了一圈"}, "没找到入口") assert verdict == "failed" and note is None def test_agent_that_never_finished_is_not_rescued(self): # 跑满步数 / 中途报错(agent_done=False)时不能靠产物翻案 verdict, _, _ = orc.apply_verdict_overrides("failed", False, self.STEP, {"cancelled": "取消"}, "超步数") assert verdict == "failed" class TestFinalStatus: SUBMIT_STEP = {"step_id": "fill", "on_success_status": "submitted"} def test_submitted_when_submit_artifact_present(self): assert orc._final_status([self.SUBMIT_STEP], {"submitted_record": "课程:高等数学"}) == "submitted" def test_result_message_also_counts_as_submitted(self): assert orc._final_status([self.SUBMIT_STEP], {"result_message": "提交成功"}) == "submitted" def test_cancelled_wins_over_submit_artifacts(self): # 用户在确认卡点了取消;即使 Agent 顺手存了 submitted_record 也不能记成已提交 artifacts = {"cancelled": "用户说不要提交", "submitted_record": "课程:高等数学"} assert orc._final_status([self.SUBMIT_STEP], artifacts) == "completed" def test_submit_step_without_artifacts_is_only_completed(self): assert orc._final_status([self.SUBMIT_STEP], {"summary": "填完了没提交"}) == "completed" def test_read_only_step_never_reports_submitted(self): steps = [{"step_id": "explore", "success_artifacts_any": ["menus"]}] assert orc._final_status(steps, {"menus": "8 个模块", "submitted_record": "x"}) == "completed" def test_custom_submitted_artifact_keys_are_honoured(self): step = {"on_success_status": "submitted", "submitted_artifacts_any": ["ticket_no"]} assert orc._final_status([step], {"ticket_no": "A123"}) == "submitted" assert orc._final_status([step], {"submitted_record": "x"}) == "completed" class TestTrackingConfig: def test_reads_business_tracking_block(self, business): cfg = orc._tracking_cfg(business) assert cfg["interval_minutes"] == 60 assert cfg["max_checks"] == 24 assert cfg["step"]["step_id"] == "track_status" # 没写 step_id 时给默认值 def test_business_without_tracking(self): assert orc._tracking_cfg({"id": "x"}) is None def test_tracking_without_step_is_ignored(self): assert orc._tracking_cfg({"tracking": {"interval_minutes": 30}}) is None def test_next_check_is_in_the_future(): from datetime import datetime nxt = datetime.strptime(orc._next_check_str(60), "%Y-%m-%d %H:%M:%S") assert nxt > datetime.now() def test_next_check_never_schedules_in_the_past(): from datetime import datetime nxt = datetime.strptime(orc._next_check_str(0), "%Y-%m-%d %H:%M:%S") assert nxt > datetime.now()