From 23e69a0c94e1b7b58218d1215eaabfb3b0331714 Mon Sep 17 00:00:00 2001 From: yetuge <182123929+yetuge@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:42:06 +0800 Subject: [PATCH] fix(config): reject boolean integer settings in scheduler_config (#6258) #6229 gave the scheduler its own integer knobs, but they accept YAML booleans: recursion_limit: true loads as 1, so every scheduler-launched run dies at its first LangGraph super-step - the same hole the app-config guard already closes for Gateway runs (#6171), now on the same-named field. max_concurrent_runs: true serializes the scheduler and poll_interval_seconds: true polls five times a second. Mirror the app-config boolean guard: a before-validator rejects booleans with a field-specific error across all six integer fields, keeping integers and numeric strings as-is. lease_seconds and queue_timeout_seconds already reject true via their higher ge floors, but with a misleading "should be greater than or equal to" message - the guard gives every field the same boolean-specific error. Signed-off-by: yetuge <2219677952@qq.com> --- .../deerflow/config/scheduler_config.py | 17 +++++++- .../test_scheduler_config_boolean_integers.py | 42 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_scheduler_config_boolean_integers.py diff --git a/backend/packages/harness/deerflow/config/scheduler_config.py b/backend/packages/harness/deerflow/config/scheduler_config.py index 3f860c8c5d..b3541ced1b 100644 --- a/backend/packages/harness/deerflow/config/scheduler_config.py +++ b/backend/packages/harness/deerflow/config/scheduler_config.py @@ -1,4 +1,6 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationInfo, field_validator + +from deerflow.config._boolean_guards import reject_boolean class SchedulerConfig(BaseModel): @@ -21,3 +23,16 @@ class SchedulerConfig(BaseModel): "AppConfig.max_recursion_limit are clamped." ), ) + + @field_validator( + "poll_interval_seconds", + "lease_seconds", + "max_concurrent_runs", + "queue_timeout_seconds", + "min_once_delay_seconds", + "recursion_limit", + mode="before", + ) + @classmethod + def _reject_boolean_scheduler_integers(cls, value: object, info: ValidationInfo) -> object: + return reject_boolean(value, info, kind="an integer") diff --git a/backend/tests/test_scheduler_config_boolean_integers.py b/backend/tests/test_scheduler_config_boolean_integers.py new file mode 100644 index 0000000000..829c37b43d --- /dev/null +++ b/backend/tests/test_scheduler_config_boolean_integers.py @@ -0,0 +1,42 @@ +"""YAML booleans must not coerce into the scheduler's integer knobs. + +`config.yaml` legitimately mixes booleans (`enabled: true`) with integer knobs on +neighbouring lines, so a typo like `scheduler.recursion_limit: true` loads +cleanly — and Pydantic coerces it to `1`, which makes every scheduler-launched +run hit the LangGraph recursion ceiling at its first super-step (the same hole +the app-config guard already closes for Gateway runs). `max_concurrent_runs: true` +serializes the scheduler and `poll_interval_seconds: true` polls five times a +second. Mirrors the app-config boolean guard. +""" + +import pytest +from pydantic import ValidationError + +from deerflow.config.scheduler_config import SchedulerConfig + +SCHEDULER_INT_FIELDS = ( + "poll_interval_seconds", + "lease_seconds", + "max_concurrent_runs", + "queue_timeout_seconds", + "min_once_delay_seconds", + "recursion_limit", +) + + +@pytest.mark.parametrize("value", [True, False]) +@pytest.mark.parametrize("field", SCHEDULER_INT_FIELDS) +def test_scheduler_config_rejects_boolean_integers(field: str, value: bool) -> None: + with pytest.raises(ValidationError, match="must be an integer, not a boolean"): + SchedulerConfig(**{field: value}) + + +def test_scheduler_config_keeps_numeric_inputs() -> None: + config = SchedulerConfig( + poll_interval_seconds="30", + max_concurrent_runs=5, + recursion_limit=500, + ) + assert config.poll_interval_seconds == 30 + assert config.max_concurrent_runs == 5 + assert config.recursion_limit == 500