mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
YAML booleans coerce into the ge=1 integer backstops that bound every subagent and ACP invocation: timeout_seconds: true became 1 (the executor's future.result(timeout=...) then killed every subagent run after one second) and max_turns: true became 1 (the LangGraph recursion budget collapsed to a single super-step). The ACP agent timeout mirrors subagents.timeout_seconds and shared the hole. false was already rejected by ge=1, so only true slipped through - silently. Mirror the app-config boolean guard: reject bools before coercion in SubagentOverrideConfig, CustomSubagentConfig and SubagentsAppConfig (timeout_seconds/max_turns/max_total_per_run) and ACPAgentConfig (timeout_seconds), keeping None, integers, and numeric strings as-is. Signed-off-by: yetuge <2219677952@qq.com>
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
"""YAML booleans must not coerce into the subagent and ACP invocation backstops.
|
|
|
|
`config.yaml` legitimately mixes booleans (`enabled: true`) with integer knobs on
|
|
neighbouring lines, so a typo like `subagents.timeout_seconds: true` loads cleanly —
|
|
and Pydantic coerces it to `1`. The subagent executor bounds every run with
|
|
`future.result(timeout=self.config.timeout_seconds)`, so 1800 seconds becomes a
|
|
1-second kill, and `resolve_recursion_limit(self.config.max_turns)` turns a
|
|
`max_turns: true` typo into a LangGraph budget of a single super-step. The ACP
|
|
agent timeout mirrors `subagents.timeout_seconds` and has the same hole. Mirrors
|
|
the app-config, loop-detection, and tool-output boolean guards already in the repo.
|
|
"""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from deerflow.config.acp_config import ACPAgentConfig
|
|
from deerflow.config.subagents_config import CustomSubagentConfig, SubagentOverrideConfig, SubagentsAppConfig
|
|
|
|
BACKSTOP_FIELDS = ("timeout_seconds", "max_turns")
|
|
GLOBAL_INT_FIELDS = ("timeout_seconds", "max_turns", "max_total_per_run")
|
|
|
|
|
|
@pytest.mark.parametrize("value", [True, False])
|
|
@pytest.mark.parametrize("field", BACKSTOP_FIELDS)
|
|
def test_override_backstops_reject_booleans(field: str, value: bool) -> None:
|
|
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
|
SubagentOverrideConfig(**{field: value})
|
|
|
|
|
|
@pytest.mark.parametrize("value", [True, False])
|
|
@pytest.mark.parametrize("field", BACKSTOP_FIELDS)
|
|
def test_custom_backstops_reject_booleans(field: str, value: bool) -> None:
|
|
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
|
CustomSubagentConfig(description="d", system_prompt="s", **{field: value})
|
|
|
|
|
|
@pytest.mark.parametrize("value", [True, False])
|
|
@pytest.mark.parametrize("field", GLOBAL_INT_FIELDS)
|
|
def test_global_backstops_reject_booleans(field: str, value: bool) -> None:
|
|
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
|
SubagentsAppConfig(**{field: value})
|
|
|
|
|
|
@pytest.mark.parametrize("field", BACKSTOP_FIELDS)
|
|
def test_override_backstops_keep_none_and_numeric_inputs(field: str) -> None:
|
|
config = SubagentOverrideConfig(**{field: None})
|
|
assert getattr(config, field) is None
|
|
config = SubagentOverrideConfig(**{field: "30"})
|
|
assert getattr(config, field) == 30
|
|
|
|
|
|
def test_custom_backstops_keep_numeric_inputs() -> None:
|
|
config = CustomSubagentConfig(description="d", system_prompt="s", max_turns="25", timeout_seconds=600)
|
|
assert config.max_turns == 25
|
|
assert config.timeout_seconds == 600
|
|
|
|
|
|
def test_global_backstops_keep_numeric_inputs() -> None:
|
|
config = SubagentsAppConfig(timeout_seconds="900", max_turns="40", max_total_per_run=8)
|
|
assert config.timeout_seconds == 900
|
|
assert config.max_turns == 40
|
|
assert config.max_total_per_run == 8
|
|
|
|
|
|
@pytest.mark.parametrize("value", [True, False])
|
|
def test_acp_timeout_rejects_booleans(value: bool) -> None:
|
|
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
|
ACPAgentConfig(command="cmd", description="d", timeout_seconds=value)
|
|
|
|
|
|
def test_acp_timeout_keeps_numeric_input() -> None:
|
|
config = ACPAgentConfig(command="cmd", description="d", timeout_seconds="600")
|
|
assert config.timeout_seconds == 600
|