Skip to content

fix(subagent): enforce handoff args and provider fallback#6873

Open
Jacobinwwey wants to merge 7 commits intoAstrBotDevs:masterfrom
Jacobinwwey:fix/subagent-handoff-arg-validation-provider-fallback
Open

fix(subagent): enforce handoff args and provider fallback#6873
Jacobinwwey wants to merge 7 commits intoAstrBotDevs:masterfrom
Jacobinwwey:fix/subagent-handoff-arg-validation-provider-fallback

Conversation

@Jacobinwwey
Copy link
Copy Markdown

@Jacobinwwey Jacobinwwey commented Mar 24, 2026

Motivation / 动机

当前 transfer_to_* handoff 参数校验和 provider 选择在边界场景下仍可能产生不透明失败:

  • background_task 输入不规范时,主代理缺少稳定的自纠错指引;
  • input 为空时可能进入无效委派路径;
  • 子代理配置了失效 provider_id 时会导致 handoff 失败而非平滑降级。

本 PR 统一 handoff 参数契约并补充 provider fallback,提升可恢复性与可观测性。

Complement With #5722 / 与 #5722 的互补关系

Modifications / 改动点

Handoff Contract / 参数契约

  • astrbot/core/agent/handoff.py
    • 明确默认 schema:required: ["input"]
    • additionalProperties: false
    • 显式声明 background_taskimage_urls 属性,保证 schema 与执行器行为一致

Validation & Fallback / 校验与回退

  • astrbot/core/astr_agent_tool_exec.py
    • background_task 解析:支持 bool 与常见字符串布尔值;非法值返回结构化错误并给出重试指引
    • input 非空校验:空字符串/空白字符串立即拒绝
    • provider fallback:当 subagent 的 provider_id 不存在时,回退到当前会话 provider 并记录 warning

Tests / 测试覆盖

  • tests/unit/test_astr_agent_tool_exec.py

    • 覆盖空 input 拒绝、缺失 provider 的 fallback 等关键路径
  • tests/unit/test_handoff_background_task_arg.py

    • 参数化覆盖 _parse_background_task_arg 的真值/假值/非法值
    • 覆盖 execute 层路由:truthy -> background handoff、falsey -> foreground handoff
    • 覆盖非法 background_task 早返回,不触发下游 handoff
    • 覆盖 schema 契约一致性(background_task 在 properties 中)
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Test Results / 测试结果

本轮在当前环境执行:

python3 -m py_compile tests/unit/test_handoff_background_task_arg.py

结果:

  • py_compile: passed

说明:

  • 当前容器环境缺少 deprecated 依赖,pytest 在加载 tests/conftest.py 时失败(ModuleNotFoundError: deprecated),未在该环境完成完整单测回归。

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了验证步骤和结果。/ My changes have been well-tested, and verification steps/results have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

@auto-assign auto-assign bot requested review from Raven95676 and anka-afk March 24, 2026 02:20
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 24, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and reliability of subagent handoff calls by introducing stricter argument validation and a resilient provider resolution mechanism. These changes prevent silent failures due to malformed arguments or missing provider configurations, providing immediate and actionable feedback to the main agent for self-correction.

Highlights

  • Handoff Parameter Schema Enforcement: Enforced a strict parameter schema for transfer_to_* handoff calls, requiring an input field and disallowing additional properties.
  • Robust Argument Parsing: Implemented strict parsing for the background_task argument, accepting boolean values and common boolean strings, and providing explicit retry guidance for invalid inputs.
  • Input Validation: Added validation to ensure the input argument for handoff calls is a non-empty string, returning structured error messages for invalid inputs.
  • Provider Fallback Mechanism: Introduced a fallback mechanism for subagent provider_ids, where if a configured provider is not found, the system defaults to the current chat provider and logs a warning.
  • Comprehensive Unit Tests: Added new unit tests to cover the validation logic for background_task parsing, empty input rejection, and the provider fallback behavior.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added area:core The bug / feature is about astrbot's core, backend area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Mar 24, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • With additionalProperties: False added to the handoff schema, background_task is not declared in default_parameters.properties, so any caller that includes it may now be rejected at the schema/validation layer—consider explicitly adding background_task (and any other expected args) to the schema to keep the contract consistent with the executor logic.
  • In _resolve_handoff_provider_id, when provider_manager is missing or lacks get_provider_by_id, you still return the configured provider ID even though it cannot be validated; consider falling back to ctx.get_current_chat_provider_id(umo) in this case so you don’t propagate potentially stale or invalid provider IDs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- With `additionalProperties: False` added to the handoff schema, `background_task` is not declared in `default_parameters.properties`, so any caller that includes it may now be rejected at the schema/validation layer—consider explicitly adding `background_task` (and any other expected args) to the schema to keep the contract consistent with the executor logic.
- In `_resolve_handoff_provider_id`, when `provider_manager` is missing or lacks `get_provider_by_id`, you still return the configured provider ID even though it cannot be validated; consider falling back to `ctx.get_current_chat_provider_id(umo)` in this case so you don’t propagate potentially stale or invalid provider IDs.

## Individual Comments

### Comment 1
<location path="astrbot/core/astr_agent_tool_exec.py" line_range="93-102" />
<code_context>
+        )
+
+    @classmethod
+    def _parse_background_task_arg(
+        cls,
+        tool_name: str,
+        value: T.Any,
+    ) -> tuple[bool, mcp.types.CallToolResult | None]:
+        if value is None:
+            return False, None
+        if isinstance(value, bool):
+            return value, None
+        if isinstance(value, str):
+            normalized = value.strip().lower()
+            if normalized in {"true", "1", "yes", "on"}:
+                return True, None
+            if normalized in {"false", "0", "no", "off", ""}:
+                return False, None
+
+        return False, cls._build_handoff_error_result(
+            tool_name=tool_name,
+            error_type="invalid_background_task",
+            fix_hint=(
+                "`background_task` must be a boolean (`true` or `false`)."
+            ),
+            action_hint=(
+                "Retry the same handoff with `background_task` as a boolean value."
+            ),
+        )
</code_context>
<issue_to_address>
**suggestion:** The `background_task` error message conflicts with the accepted string values.

The parser accepts several string forms (e.g. `"true"`, `"1"`, `"yes"`), but the error says `background_task` must be a boolean (`true` or `false`). This is misleading, since callers might think only native booleans are valid. Please either restrict the implementation to real booleans or update the error text to reflect the accepted string values.

```suggestion
        return False, cls._build_handoff_error_result(
            tool_name=tool_name,
            error_type="invalid_background_task",
            fix_hint=(
                "`background_task` must be a boolean (`true` or `false`) or a string "
                "equivalent such as `\"1\"`/`\"0\"`, `\"yes\"`/`\"no\"`, or "
                "`\"on\"`/`\"off\"`."
            ),
            action_hint=(
                "Retry the same handoff with `background_task` set to a boolean or one "
                "of the supported string equivalents (`\"true\"`, `\"false\"`, "
                "`\"1\"`, `\"0\"`, `\"yes\"`, `\"no\"`, `\"on\"`, `\"off\"`)."
            ),
        )
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot
Copy link
Copy Markdown

dosubot bot commented Mar 24, 2026

Related Documentation

1 document(s) may need updating based on files changed in this PR:

AstrBotTeam's Space

pr4697的改动
View Suggested Changes
@@ -85,11 +85,36 @@
 **委派工具参数**
 
 transfer_to_* 工具现支持以下参数:
-- `input`:转交给子代理的任务输入(必填)
+- `input`:转交给子代理的任务输入(**必填**)
+  - 必须为非空字符串,需清晰描述委派任务
+  - 空字符串或仅含空白字符会被拒绝,返回结构化错误消息(错误类型:`missing_or_empty_input`),并提供重试指导
 - `image_urls`(可选):图片 URL 或本地路径数组,用于多模态任务。用户发送给主代理的图片会自动传递给子代理,也可以手动指定 HTTP/HTTPS 公网 URL 或本地文件路径。支持的图片格式包括:png、jpg、jpeg、gif、webp、bmp、tif、tiff、svg、heic。
-- `background_task`:设置任务执行模式
+- `background_task`:设置任务执行模式(可选,默认 `false`)
+  - 接受布尔值(`true` / `false`)或常见布尔字符串表示(`"true"`, `"false"`, `"1"`, `"0"`, `"yes"`, `"no"`, `"on"`, `"off"`)
   - 若为 `true`,任务将以后台模式异步执行,通过 `SubagentRuntime.enqueue()` 方法提交到持久化任务队列,主代理立即返回任务 ID,完成后通过后台任务唤醒机制通知用户
   - 若未设置或为 `false`,则为同步模式,主代理等待子代理完成任务
+  - 无效值(非布尔类型或无法识别的字符串)会被拒绝,返回结构化错误消息(错误类型:`invalid_background_task`),并提供重试指导
+
+**参数验证与错误处理(PR #6873)**
+
+[PR #6873](https://github.com/AstrBotDevs/AstrBot/pull/6873) 强化了 handoff 工具的参数验证,引入严格的 schema 约束和结构化错误响应:
+
+- **严格 schema 约束**:
+  - handoff 工具 schema 现强制要求 `required: ["input"]` 和 `additionalProperties: false`
+  - `input` 参数为必填,不允许传递未知参数
+  - 传递未知参数时,工具会拒绝请求并返回明确错误消息
+
+- **结构化错误响应**:
+  - 参数验证失败时返回包含 `error_type`、`fix` 提示和 `action` 指导的结构化错误消息
+  - 错误类型包括:
+    - `invalid_background_task`:`background_task` 参数无效(非布尔值或无法识别的字符串)
+    - `missing_or_empty_input`:`input` 参数缺失或为空字符串/空白字符
+  - 错误消息包含重试示例,指导 Agent 立即使用正确参数重试
+
+- **Provider 回退机制**:
+  - 若子代理配置的 `provider_id` 在 provider manager 中不存在,系统自动回退到当前会话的 provider ID
+  - 回退时记录警告日志,便于诊断配置过期或缺失的场景
+  - 确保子代理在 provider 配置失效时不会静默失败
 
 更详细的参数说明和使用示例,请参阅官方 SubAgent 文档的[委派工具参数](https://docs.astrbot.app/zh/use/subagent.html#委派工具参数)和[多模态图片传递](https://docs.astrbot.app/zh/use/subagent.html#多模态图片传递)章节。
 

[Accept] [Decline]

Note: You must be authenticated to accept/decline updates.

How did I do? Any feedback?  Join Discord

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the subagent handoff mechanism by enforcing a stricter argument schema, adding validation for input and background_task parameters, and implementing a fallback for missing provider IDs. These changes improve the robustness and reliability of agent handoffs. The new helper methods for parsing and validation in astrbot/core/astr_agent_tool_exec.py are well-structured. The accompanying tests cover the new logic, though I've suggested an improvement to increase test coverage for _parse_background_task_arg by using parameterization.

Comment on lines +348 to +367
def test_parse_background_task_arg_rejects_invalid_value():
is_bg, error = FunctionToolExecutor._parse_background_task_arg(
"transfer_to_subagent",
"not-a-bool",
)

assert is_bg is False
assert error is not None
assert isinstance(error, mcp.types.CallToolResult)
assert "invalid_background_task" in error.content[0].text


def test_parse_background_task_arg_accepts_string_true():
is_bg, error = FunctionToolExecutor._parse_background_task_arg(
"transfer_to_subagent",
"true",
)

assert is_bg is True
assert error is None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These two tests for _parse_background_task_arg can be consolidated into a single, more comprehensive parameterized test using pytest.mark.parametrize. This would improve test coverage by including all valid string representations for True and False (e.g., '1', 'yes', 'on', '0', 'no', 'off'), as well as various invalid cases, making the test suite more robust and easier to maintain.

@pytest.mark.parametrize(
    ("value", "expected_bool", "expect_error"),
    [
        # Valid True values
        (True, True, False),
        ("true", True, False),
        ("1", True, False),
        ("yes", True, False),
        ("on", True, False),
        (" TRUE ", True, False),
        # Valid False values
        (False, False, False),
        ("false", False, False),
        ("0", False, False),
        ("no", False, False),
        ("off", False, False),
        ("", False, False),
        (" FALSE ", False, False),
        (None, False, False),
        # Invalid values
        ("not-a-bool", False, True),
        ("y", False, True),
        ("t", False, True),
        (123, False, True),
        ({}, False, True),
    ],
)
def test_parse_background_task_arg(value, expected_bool, expect_error):
    is_bg, error = FunctionToolExecutor._parse_background_task_arg(
        "transfer_to_subagent",
        value,
    )

    assert is_bg is expected_bool
    if expect_error:
        assert error is not None
        assert isinstance(error, mcp.types.CallToolResult)
        assert "invalid_background_task" in error.content[0].text
    else:
        assert error is None

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@Jacobinwwey
Copy link
Copy Markdown
Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In _resolve_handoff_provider_id, when provider_manager is missing or does not expose get_provider_by_id, you currently return the configured provider_id as-is, which can still be stale; consider falling back to ctx.get_current_chat_provider_id(umo) in that branch to avoid reintroducing silent misrouting in environments without a provider manager.
  • _build_handoff_error_resultembeds a concrete example payload string (withbackground_task: falseand onlyinput/background_task` fields); to avoid future drift if the handoff schema or typical usage changes, consider constructing this example dynamically from the current schema or at least centralizing the example in a single constant.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_resolve_handoff_provider_id`, when `provider_manager` is missing or does not expose `get_provider_by_id`, you currently return the configured `provider_id` as-is, which can still be stale; consider falling back to `ctx.get_current_chat_provider_id(umo)` in that branch to avoid reintroducing silent misrouting in environments without a provider manager.
- _build_handoff_error_result` embeds a concrete example payload string (with `background_task: false` and only `input`/`background_task` fields); to avoid future drift if the handoff schema or typical usage changes, consider constructing this example dynamically from the current schema or at least centralizing the example in a single constant.

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/handoff.py" line_range="42-43" />
<code_context>
     def default_parameters(self) -> dict:
         return {
             "type": "object",
+            "required": ["input"],
+            "additionalProperties": False,
             "properties": {
                 "input": {
</code_context>
<issue_to_address>
**issue (bug_risk):** Reconsider `additionalProperties: False` given runtime use of extra arguments like `background_task`.

With `additionalProperties: False`, any parameters not declared in `properties` will now be rejected, while the executor still expects fields like `background_task` (and possibly `image_urls`). This mismatch can break existing clients that send these fields. Either add all such fields to `properties` or keep `additionalProperties` relaxed until the schema and runtime behavior are aligned.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Jacobinwwey
Copy link
Copy Markdown
Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • _build_handoff_error_result hardcodes a background_task example in the guidance text even for non-background_task errors (e.g., missing_or_empty_input); consider parameterizing or making the example match the specific error type so the agent receives clearer, less confusing retry instructions.
  • In _resolve_handoff_provider_id, if provider_manager is missing or lacks get_provider_by_id, the code returns the configured provider_id without checking it exists; you might want to fall back to ctx.get_current_chat_provider_id in this case as well to avoid silently using a stale provider ID.
  • There is some duplication between the background_task parsing tests in test_astr_agent_tool_exec.py and the new parametrized tests in test_handoff_background_task_arg.py; consider consolidating into the parametrized suite to keep coverage while reducing redundancy.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- _build_handoff_error_result hardcodes a background_task example in the guidance text even for non-background_task errors (e.g., missing_or_empty_input); consider parameterizing or making the example match the specific error type so the agent receives clearer, less confusing retry instructions.
- In _resolve_handoff_provider_id, if provider_manager is missing or lacks get_provider_by_id, the code returns the configured provider_id without checking it exists; you might want to fall back to ctx.get_current_chat_provider_id in this case as well to avoid silently using a stale provider ID.
- There is some duplication between the background_task parsing tests in test_astr_agent_tool_exec.py and the new parametrized tests in test_handoff_background_task_arg.py; consider consolidating into the parametrized suite to keep coverage while reducing redundancy.

## Individual Comments

### Comment 1
<location path="astrbot/core/astr_agent_tool_exec.py" line_range="86-93" />
<code_context>
+            return False, None
+        if isinstance(value, bool):
+            return value, None
+        if isinstance(value, str):
+            normalized = value.strip().lower()
+            if normalized in {"true", "1", "yes", "on"}:
+                return True, None
+            if normalized in {"false", "0", "no", "off", ""}:
+                return False, None
+
+        return False, cls._build_handoff_error_result(
+            tool_name=tool_name,
+            error_type="invalid_background_task",
</code_context>
<issue_to_address>
**suggestion:** Consider either supporting integer 0/1 for `background_task` or tightening the error message to avoid implying they are accepted.

Right now, `background_task` only accepts booleans or specific *string* values; numeric 0/1 are rejected even though the hint suggests broader equivalence. This can be confusing for callers whose JSON booleans end up as 0/1. Either broaden parsing to accept `int` 0/1 or update the hint to clearly state that only booleans and string equivalents are valid.
</issue_to_address>

### Comment 2
<location path="tests/unit/test_astr_agent_tool_exec.py" line_range="348-357" />
<code_context>
     assert image_urls == []
+
+
+def test_parse_background_task_arg_rejects_invalid_value():
+    is_bg, error = FunctionToolExecutor._parse_background_task_arg(
+        "transfer_to_subagent",
+        "not-a-bool",
+    )
+
+    assert is_bg is False
+    assert error is not None
+    assert isinstance(error, mcp.types.CallToolResult)
+    assert "invalid_background_task" in error.content[0].text
+
+
+def test_parse_background_task_arg_accepts_string_true():
+    is_bg, error = FunctionToolExecutor._parse_background_task_arg(
+        "transfer_to_subagent",
+        "true",
+    )
+
+    assert is_bg is True
+    assert error is None
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Background task parsing is tested twice in two files; consider consolidating to the parametrized suite to avoid duplication and future drift.

These tests duplicate the existing parametrized coverage in `tests/unit/test_handoff_background_task_arg.py` and risk drifting out of sync if behavior changes. Consider either removing them in favor of the parametrized suite or moving all `_parse_background_task_arg` tests into a single dedicated file as the source of truth.

Suggested implementation:

```python
    )

    assert image_urls == []

```

If there are additional `_parse_background_task_arg` tests further down in this file (e.g., for `"true"` or other values), they should also be removed from `test_astr_agent_tool_exec.py` and kept only in `tests/unit/test_handoff_background_task_arg.py` to maintain a single, consolidated test suite for this behavior.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_handoff_background_task_arg.py" line_range="7-16" />
<code_context>
+@pytest.mark.parametrize(
</code_context>
<issue_to_address>
**suggestion (testing):** Background task parsing is well-covered at the helper level; consider adding an integration-level test for invalid `background_task` via `execute` to ensure the early-yield behavior is preserved.

The current parametrized test covers `_parse_background_task_arg`, but not how `FunctionToolExecutor.execute` behaves when `background_task` is invalid. Please add a small async test that calls `execute` (or `_execute_handoff`, if that’s the public entry) with an invalid `background_task` and asserts that it yields a single `CallToolResult` error and returns without invoking the handoff or any downstream calls.

Suggested implementation:

```python
import mcp
import pytest
from unittest.mock import AsyncMock

from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor

```

```python
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor


@pytest.mark.asyncio
async def test_execute_invalid_background_task_early_error(monkeypatch):
    """Integration-level test: invalid background_task should yield a single error and not hand off."""
    # Arrange: create an executor instance
    executor = FunctionToolExecutor()

    # If execute delegates into a separate handoff method, guard it so the test fails if it is invoked.
    # Adjust the attribute name if the handoff entrypoint differs (e.g. `handoff`, `_handoff`, etc.).
    if hasattr(executor, "_execute_handoff"):
        monkeypatch.setattr(
            executor,
            "_execute_handoff",
            AsyncMock(side_effect=AssertionError("_execute_handoff should not be called for invalid background_task")),
        )

    # Act: call execute with an invalid background_task value that should fail parsing
    results = [
        result async for result in executor.execute(
            tool_name="some_tool",
            arguments={},
            background_task="not-a-bool",  # intentionally invalid
        )
    ]

    # Assert: exactly one CallToolResult is yielded, it is an error, and no downstream calls are made.
    assert len(results) == 1
    assert isinstance(results[0], mcp.CallToolResult)
    assert getattr(results[0], "is_error", True), "Expected the single CallToolResult to represent an error"


@pytest.mark.parametrize(

```

The new integration-level test assumes:
1. `FunctionToolExecutor()` can be constructed without arguments. If the real initializer requires parameters, update the test's `executor = FunctionToolExecutor()` line to pass appropriate arguments or use a helper/factory already used elsewhere in your tests.
2. `FunctionToolExecutor.execute` is an `async` generator that accepts `tool_name`, `arguments`, and `background_task` keyword arguments. If the signature differs, align the call in the test with the actual method signature.
3. The early-yield error path does not invoke `_execute_handoff`. If the actual handoff entrypoint has a different name (e.g., `handoff`, `_handoff`, `_execute_tool_handoff`), update the `hasattr` / `monkeypatch.setattr` to reference the correct attribute, or remove that block if there is no such method.
4. `mcp.CallToolResult` has an `is_error` attribute or similar. If the error indication is expressed differently (e.g., `result.error is not None`, `result.type == "error"`, etc.), adjust the final assertion to match your concrete API.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +348 to +357
def test_parse_background_task_arg_rejects_invalid_value():
is_bg, error = FunctionToolExecutor._parse_background_task_arg(
"transfer_to_subagent",
"not-a-bool",
)

assert is_bg is False
assert error is not None
assert isinstance(error, mcp.types.CallToolResult)
assert "invalid_background_task" in error.content[0].text
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Background task parsing is tested twice in two files; consider consolidating to the parametrized suite to avoid duplication and future drift.

These tests duplicate the existing parametrized coverage in tests/unit/test_handoff_background_task_arg.py and risk drifting out of sync if behavior changes. Consider either removing them in favor of the parametrized suite or moving all _parse_background_task_arg tests into a single dedicated file as the source of truth.

Suggested implementation:

    )

    assert image_urls == []

If there are additional _parse_background_task_arg tests further down in this file (e.g., for "true" or other values), they should also be removed from test_astr_agent_tool_exec.py and kept only in tests/unit/test_handoff_background_task_arg.py to maintain a single, consolidated test suite for this behavior.

Comment on lines +7 to +16
@pytest.mark.parametrize(
("value", "expected_bool", "expect_error"),
[
(True, True, False),
("true", True, False),
("1", True, False),
("yes", True, False),
("on", True, False),
(" TRUE ", True, False),
(False, False, False),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Background task parsing is well-covered at the helper level; consider adding an integration-level test for invalid background_task via execute to ensure the early-yield behavior is preserved.

The current parametrized test covers _parse_background_task_arg, but not how FunctionToolExecutor.execute behaves when background_task is invalid. Please add a small async test that calls execute (or _execute_handoff, if that’s the public entry) with an invalid background_task and asserts that it yields a single CallToolResult error and returns without invoking the handoff or any downstream calls.

Suggested implementation:

import mcp
import pytest
from unittest.mock import AsyncMock

from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor


@pytest.mark.asyncio
async def test_execute_invalid_background_task_early_error(monkeypatch):
    """Integration-level test: invalid background_task should yield a single error and not hand off."""
    # Arrange: create an executor instance
    executor = FunctionToolExecutor()

    # If execute delegates into a separate handoff method, guard it so the test fails if it is invoked.
    # Adjust the attribute name if the handoff entrypoint differs (e.g. `handoff`, `_handoff`, etc.).
    if hasattr(executor, "_execute_handoff"):
        monkeypatch.setattr(
            executor,
            "_execute_handoff",
            AsyncMock(side_effect=AssertionError("_execute_handoff should not be called for invalid background_task")),
        )

    # Act: call execute with an invalid background_task value that should fail parsing
    results = [
        result async for result in executor.execute(
            tool_name="some_tool",
            arguments={},
            background_task="not-a-bool",  # intentionally invalid
        )
    ]

    # Assert: exactly one CallToolResult is yielded, it is an error, and no downstream calls are made.
    assert len(results) == 1
    assert isinstance(results[0], mcp.CallToolResult)
    assert getattr(results[0], "is_error", True), "Expected the single CallToolResult to represent an error"


@pytest.mark.parametrize(

The new integration-level test assumes:

  1. FunctionToolExecutor() can be constructed without arguments. If the real initializer requires parameters, update the test's executor = FunctionToolExecutor() line to pass appropriate arguments or use a helper/factory already used elsewhere in your tests.
  2. FunctionToolExecutor.execute is an async generator that accepts tool_name, arguments, and background_task keyword arguments. If the signature differs, align the call in the test with the actual method signature.
  3. The early-yield error path does not invoke _execute_handoff. If the actual handoff entrypoint has a different name (e.g., handoff, _handoff, _execute_tool_handoff), update the hasattr / monkeypatch.setattr to reference the correct attribute, or remove that block if there is no such method.
  4. mcp.CallToolResult has an is_error attribute or similar. If the error indication is expressed differently (e.g., result.error is not None, result.type == "error", etc.), adjust the final assertion to match your concrete API.

Copy link
Copy Markdown

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The _parse_background_task_arg behavior is covered both in test_astr_agent_tool_exec.py (two focused tests) and test_handoff_background_task_arg.py (parametrized suite); consider consolidating these into a single location to avoid duplicated coverage and future drift.
  • Given that _build_handoff_error_result is now the central path for handoff validation failures, consider including a lightweight machine-readable field (e.g., a JSON snippet or error_type: line that can be reliably parsed) in addition to the human-readable guidance to make it easier for callers to programmatically react to different failure types.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_parse_background_task_arg` behavior is covered both in `test_astr_agent_tool_exec.py` (two focused tests) and `test_handoff_background_task_arg.py` (parametrized suite); consider consolidating these into a single location to avoid duplicated coverage and future drift.
- Given that `_build_handoff_error_result` is now the central path for handoff validation failures, consider including a lightweight machine-readable field (e.g., a JSON snippet or `error_type:` line that can be reliably parsed) in addition to the human-readable guidance to make it easier for callers to programmatically react to different failure types.

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/handoff.py" line_range="42-43" />
<code_context>
     def default_parameters(self) -> dict:
         return {
             "type": "object",
+            "required": ["input"],
+            "additionalProperties": False,
             "properties": {
                 "input": {
</code_context>
<issue_to_address>
**issue (bug_risk):** Making `input` required and disallowing additional properties conflicts with the `background_task` argument handling.

With `input` required and `additionalProperties=False`, any `background_task` key in `tool_args` is now invalid JSON Schema, even though `FunctionToolExecutor.execute` still reads it and `_build_handoff_error_result` shows it in the example. Either add `background_task` (with the correct type) to `properties` or relax `additionalProperties` to allow it; if it’s no longer supported, remove it from both the error example and the `tool_args` handling to keep schema and behavior aligned.
</issue_to_address>

Hi @Jacobinwwey! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +42 to +43
"required": ["input"],
"additionalProperties": False,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Making input required and disallowing additional properties conflicts with the background_task argument handling.

With input required and additionalProperties=False, any background_task key in tool_args is now invalid JSON Schema, even though FunctionToolExecutor.execute still reads it and _build_handoff_error_result shows it in the example. Either add background_task (with the correct type) to properties or relax additionalProperties to allow it; if it’s no longer supported, remove it from both the error example and the tool_args handling to keep schema and behavior aligned.

@Jacobinwwey
Copy link
Copy Markdown
Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • With default_parameters now setting additionalProperties: False while only input is declared in properties, any existing/expected handoff args like background_task, image_urls, etc. will be rejected at the schema level; consider explicitly listing all supported handoff parameters in properties so they remain usable.
  • The handoff error responses embed error_type, fix, and action only inside a free-form text blob; if downstream tooling needs to react programmatically, consider returning these as separate structured fields (e.g., in a JSON payload or metadata) to make automated handling easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- With `default_parameters` now setting `additionalProperties: False` while only `input` is declared in `properties`, any existing/expected handoff args like `background_task`, `image_urls`, etc. will be rejected at the schema level; consider explicitly listing all supported handoff parameters in `properties` so they remain usable.
- The handoff error responses embed `error_type`, `fix`, and `action` only inside a free-form text blob; if downstream tooling needs to react programmatically, consider returning these as separate structured fields (e.g., in a JSON payload or metadata) to make automated handling easier.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Jacobinwwey
Copy link
Copy Markdown
Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/unit/test_handoff_background_task_arg.py" line_range="12-21" />
<code_context>
+from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
+
+
+@pytest.mark.parametrize(
+    ("value", "expected_bool", "expect_error"),
+    [
+        (True, True, False),
+        ("true", True, False),
+        ("1", True, False),
+        ("yes", True, False),
+        ("on", True, False),
+        (" TRUE ", True, False),
+        (False, False, False),
+        ("false", False, False),
+        ("0", False, False),
+        ("no", False, False),
+        ("off", False, False),
+        ("", False, False),
+        (" FALSE ", False, False),
+        (None, False, False),
+        ("not-a-bool", False, True),
+        ("y", False, True),
+        ("t", False, True),
+        (123, False, True),
+        ({}, False, True),
+    ],
+)
+def test_parse_background_task_arg(value, expected_bool, expect_error):
+    is_bg, error = FunctionToolExecutor._parse_background_task_arg(
+        "transfer_to_subagent",
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding dedicated tests for `_parse_background_task_arg` integration via `execute` with valid values

The parameterized helper tests cover value parsing well, but `FunctionToolExecutor.execute` is only exercised for the invalid string case. Please add `execute`-level tests where:

- A truthy background value (e.g. `True` or "true") results in exactly one `_execute_handoff_background` call, zero `_execute_handoff` calls.
- A falsey background value (e.g. `False`, "false", or "0") results in exactly one `_execute_handoff` call, zero `_execute_handoff_background` calls.

These can follow the pattern of `test_execute_invalid_background_task_early_error`, using monkeypatching and `call_count` assertions, and verifying that no error `CallToolResult` is returned.

Suggested implementation:

```python
    if expect_error:
        assert error is not None
        assert isinstance(error, mcp.types.CallToolResult)
        text_content = error.content[0]
        assert isinstance(text_content, mcp.types.TextContent)
        assert "invalid_background_task" in text_content.text


@pytest.mark.parametrize("background_value", [True, "true"])
def test_execute_truthy_background_uses_background_handoff(monkeypatch, background_value):
    handoff_call_count = 0
    background_call_count = 0

    def fake_execute_handoff(*args, **kwargs):
        nonlocal handoff_call_count
        handoff_call_count += 1
        # Return a non-error CallToolResult shape; adjust as needed for the real API.
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="foreground handoff")],
        )

    def fake_execute_handoff_background(*args, **kwargs):
        nonlocal background_call_count
        background_call_count += 1
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="background handoff")],
        )

    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff",
        fake_execute_handoff,
    )
    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff_background",
        fake_execute_handoff_background,
    )

    # Construct minimal arguments following the pattern of other tests in this module.
    # These may need to be adjusted to match the actual FunctionToolExecutor.execute signature.
    agent = Agent()
    tool = HandoffTool(name="transfer_to_subagent")
    context = ContextWrapper(agent=agent)
    arguments = {"background_task": background_value}

    result = FunctionToolExecutor.execute(
        agent=agent,
        tool=tool,
        context=context,
        arguments=arguments,
    )

    assert handoff_call_count == 0
    assert background_call_count == 1

    # Ensure no "invalid_background_task" error CallToolResult was returned.
    results = result if isinstance(result, (list, tuple)) else [result]
    for res in results:
        if isinstance(res, mcp.types.CallToolResult) and res.content:
            first = res.content[0]
            if isinstance(first, mcp.types.TextContent):
                assert "invalid_background_task" not in first.text


@pytest.mark.parametrize("background_value", [False, "false", "0"])
def test_execute_falsey_background_uses_foreground_handoff(monkeypatch, background_value):
    handoff_call_count = 0
    background_call_count = 0

    def fake_execute_handoff(*args, **kwargs):
        nonlocal handoff_call_count
        handoff_call_count += 1
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="foreground handoff")],
        )

    def fake_execute_handoff_background(*args, **kwargs):
        nonlocal background_call_count
        background_call_count += 1
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="background handoff")],
        )

    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff",
        fake_execute_handoff,
    )
    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff_background",
        fake_execute_handoff_background,
    )

    agent = Agent()
    tool = HandoffTool(name="transfer_to_subagent")
    context = ContextWrapper(agent=agent)
    arguments = {"background_task": background_value}

    result = FunctionToolExecutor.execute(
        agent=agent,
        tool=tool,
        context=context,
        arguments=arguments,
    )

    assert handoff_call_count == 1
    assert background_call_count == 0

    results = result if isinstance(result, (list, tuple)) else [result]
    for res in results:
        if isinstance(res, mcp.types.CallToolResult) and res.content:
            first = res.content[0]
            if isinstance(first, mcp.types.TextContent):
                assert "invalid_background_task" not in first.text

```

The new tests assume:
1. `FunctionToolExecutor.execute` has a signature like:
   `execute(agent: Agent, tool: HandoffTool, context: ContextWrapper, arguments: dict)`.
2. `Agent`, `HandoffTool`, and `ContextWrapper` can be constructed with the minimal arguments shown.

To integrate with the actual codebase, you may need to:
1. Adjust the construction of `agent`, `tool`, and `context` to match their real constructors, or switch to using existing fixtures/helpers already used in `test_execute_invalid_background_task_early_error`.
2. Update the `execute` call to match the real parameter names and ordering.
3. If the real non-error `CallToolResult` shape differs, tweak the fake implementations of `_execute_handoff` and `_execute_handoff_background` to return whatever the production code expects.
4. If the production error detection differs from checking for `"invalid_background_task"` in `TextContent`, align the “no error” assertions with the pattern already used in `test_execute_invalid_background_task_early_error`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +12 to +21
@pytest.mark.parametrize(
("value", "expected_bool", "expect_error"),
[
(True, True, False),
("true", True, False),
("1", True, False),
("yes", True, False),
("on", True, False),
(" TRUE ", True, False),
(False, False, False),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding dedicated tests for _parse_background_task_arg integration via execute with valid values

The parameterized helper tests cover value parsing well, but FunctionToolExecutor.execute is only exercised for the invalid string case. Please add execute-level tests where:

  • A truthy background value (e.g. True or "true") results in exactly one _execute_handoff_background call, zero _execute_handoff calls.
  • A falsey background value (e.g. False, "false", or "0") results in exactly one _execute_handoff call, zero _execute_handoff_background calls.

These can follow the pattern of test_execute_invalid_background_task_early_error, using monkeypatching and call_count assertions, and verifying that no error CallToolResult is returned.

Suggested implementation:

    if expect_error:
        assert error is not None
        assert isinstance(error, mcp.types.CallToolResult)
        text_content = error.content[0]
        assert isinstance(text_content, mcp.types.TextContent)
        assert "invalid_background_task" in text_content.text


@pytest.mark.parametrize("background_value", [True, "true"])
def test_execute_truthy_background_uses_background_handoff(monkeypatch, background_value):
    handoff_call_count = 0
    background_call_count = 0

    def fake_execute_handoff(*args, **kwargs):
        nonlocal handoff_call_count
        handoff_call_count += 1
        # Return a non-error CallToolResult shape; adjust as needed for the real API.
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="foreground handoff")],
        )

    def fake_execute_handoff_background(*args, **kwargs):
        nonlocal background_call_count
        background_call_count += 1
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="background handoff")],
        )

    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff",
        fake_execute_handoff,
    )
    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff_background",
        fake_execute_handoff_background,
    )

    # Construct minimal arguments following the pattern of other tests in this module.
    # These may need to be adjusted to match the actual FunctionToolExecutor.execute signature.
    agent = Agent()
    tool = HandoffTool(name="transfer_to_subagent")
    context = ContextWrapper(agent=agent)
    arguments = {"background_task": background_value}

    result = FunctionToolExecutor.execute(
        agent=agent,
        tool=tool,
        context=context,
        arguments=arguments,
    )

    assert handoff_call_count == 0
    assert background_call_count == 1

    # Ensure no "invalid_background_task" error CallToolResult was returned.
    results = result if isinstance(result, (list, tuple)) else [result]
    for res in results:
        if isinstance(res, mcp.types.CallToolResult) and res.content:
            first = res.content[0]
            if isinstance(first, mcp.types.TextContent):
                assert "invalid_background_task" not in first.text


@pytest.mark.parametrize("background_value", [False, "false", "0"])
def test_execute_falsey_background_uses_foreground_handoff(monkeypatch, background_value):
    handoff_call_count = 0
    background_call_count = 0

    def fake_execute_handoff(*args, **kwargs):
        nonlocal handoff_call_count
        handoff_call_count += 1
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="foreground handoff")],
        )

    def fake_execute_handoff_background(*args, **kwargs):
        nonlocal background_call_count
        background_call_count += 1
        return mcp.types.CallToolResult(
            content=[mcp.types.TextContent(type="text", text="background handoff")],
        )

    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff",
        fake_execute_handoff,
    )
    monkeypatch.setattr(
        FunctionToolExecutor,
        "_execute_handoff_background",
        fake_execute_handoff_background,
    )

    agent = Agent()
    tool = HandoffTool(name="transfer_to_subagent")
    context = ContextWrapper(agent=agent)
    arguments = {"background_task": background_value}

    result = FunctionToolExecutor.execute(
        agent=agent,
        tool=tool,
        context=context,
        arguments=arguments,
    )

    assert handoff_call_count == 1
    assert background_call_count == 0

    results = result if isinstance(result, (list, tuple)) else [result]
    for res in results:
        if isinstance(res, mcp.types.CallToolResult) and res.content:
            first = res.content[0]
            if isinstance(first, mcp.types.TextContent):
                assert "invalid_background_task" not in first.text

The new tests assume:

  1. FunctionToolExecutor.execute has a signature like:
    execute(agent: Agent, tool: HandoffTool, context: ContextWrapper, arguments: dict).
  2. Agent, HandoffTool, and ContextWrapper can be constructed with the minimal arguments shown.

To integrate with the actual codebase, you may need to:

  1. Adjust the construction of agent, tool, and context to match their real constructors, or switch to using existing fixtures/helpers already used in test_execute_invalid_background_task_early_error.
  2. Update the execute call to match the real parameter names and ordering.
  3. If the real non-error CallToolResult shape differs, tweak the fake implementations of _execute_handoff and _execute_handoff_background to return whatever the production code expects.
  4. If the production error detection differs from checking for "invalid_background_task" in TextContent, align the “no error” assertions with the pattern already used in test_execute_invalid_background_task_early_error.

Copy link
Copy Markdown

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/unit/test_astr_agent_tool_exec.py" line_range="348-357" />
<code_context>
+@pytest.mark.asyncio
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen empty-input test by asserting the handoff agent is never invoked

To fully validate the early-rejection behavior, also assert that `_fake_tool_loop_agent` is never invoked (e.g., via a flag or counter). This will confirm the error is raised before any downstream handoff execution occurs for empty input.

Suggested implementation:

```python
@pytest.mark.asyncio
async def test_execute_handoff_rejects_empty_input():
    tool_loop_called = False

    async def _fake_get_current_chat_provider_id(_umo):
        return "provider-id"

    async def _fake_tool_loop_agent(**_kwargs):
        nonlocal tool_loop_called
        tool_loop_called = True
        return SimpleNamespace(completion_text="ok")

    context = SimpleNamespace(
        get_current_chat_provider_id=_fake_get_current_chat_provider_id,
        tool_loop_agent=_fake_tool_loop_agent,

```

To fully implement the strengthened test, you should also:
1. After the code that triggers the empty-input rejection (likely the call to the handoff execution function and the corresponding `pytest.raises` block), add:
   `assert tool_loop_called is False`
2. Ensure this assertion is placed at the end of `test_execute_handoff_rejects_empty_input`, after all other assertions that verify the rejection behavior.
</issue_to_address>

Hi @Jacobinwwey! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +348 to +357
@pytest.mark.asyncio
async def test_execute_handoff_rejects_empty_input():
async def _fake_get_current_chat_provider_id(_umo):
return "provider-id"

async def _fake_tool_loop_agent(**_kwargs):
return SimpleNamespace(completion_text="ok")

context = SimpleNamespace(
get_current_chat_provider_id=_fake_get_current_chat_provider_id,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen empty-input test by asserting the handoff agent is never invoked

To fully validate the early-rejection behavior, also assert that _fake_tool_loop_agent is never invoked (e.g., via a flag or counter). This will confirm the error is raised before any downstream handoff execution occurs for empty input.

Suggested implementation:

@pytest.mark.asyncio
async def test_execute_handoff_rejects_empty_input():
    tool_loop_called = False

    async def _fake_get_current_chat_provider_id(_umo):
        return "provider-id"

    async def _fake_tool_loop_agent(**_kwargs):
        nonlocal tool_loop_called
        tool_loop_called = True
        return SimpleNamespace(completion_text="ok")

    context = SimpleNamespace(
        get_current_chat_provider_id=_fake_get_current_chat_provider_id,
        tool_loop_agent=_fake_tool_loop_agent,

To fully implement the strengthened test, you should also:

  1. After the code that triggers the empty-input rejection (likely the call to the handoff execution function and the corresponding pytest.raises block), add:
    assert tool_loop_called is False
  2. Ensure this assertion is placed at the end of test_execute_handoff_rejects_empty_input, after all other assertions that verify the rejection behavior.

@Jacobinwwey
Copy link
Copy Markdown
Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Jacobinwwey
Copy link
Copy Markdown
Author

[Passed the review by Sourcery]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants