diff --git a/loopx/extensions/lark/manager_context.py b/loopx/extensions/lark/manager_context.py index f564ac8264..b555989087 100644 --- a/loopx/extensions/lark/manager_context.py +++ b/loopx/extensions/lark/manager_context.py @@ -46,6 +46,10 @@ def manager_failure_reply(error: Exception) -> tuple[str, str]: "hard_timeout": "处理超过时间限制", "interrupted": "处理已中断", "manager_authorization_unavailable": "当前连接的授权范围不可用", + # The executor's own gate refused the call, which is what an exhausted + # credential or a revoked login looks like from the channel. Naming the + # executor keeps the owner from reading it as a manager defect. + "host_gate": "上游执行器拒绝本次调用(额度或授权),请在管家执行器一侧检查", "manager_channel_executor_rebind_required": ( "管家的执行器已由本机设置更改,需要重新应用一次管家连接" ), diff --git a/tests/test_manager_team_plan_guidance.py b/tests/test_manager_team_plan_guidance.py index 9dd6c01e3b..c12ae7b817 100644 --- a/tests/test_manager_team_plan_guidance.py +++ b/tests/test_manager_team_plan_guidance.py @@ -28,3 +28,23 @@ def test_manager_guidance_orders_one_team_preview_before_any_effect() -> None: # An unstaffable lane is named as a gap rather than invented. assert "as a gap, with the missing registration or grant" in text assert "inventing a lane, an Agent, or a capability" in text + + +def test_the_owner_visible_failure_names_the_executor_that_refused() -> None: + """A host gate is the executor's refusal, not a defect in the manager.""" + + from loopx.extensions.lark.manager_context import manager_failure_reply + + class _Refused(RuntimeError): + error_code = "host_gate" + + code, text = manager_failure_reply(_Refused("upstream refused")) + + assert code == "host_gate" + assert "上游执行器" in text + assert "管家处理失败" not in text + # An unmapped code still falls back to the bounded generic label. + class _Unknown(RuntimeError): + error_code = "some_future_code" + + assert manager_failure_reply(_Unknown("x"))[0] == "processing_failed"