fix(runs): trim event_types filter entries before querying (#7758)

* fix(runs): trim event_types filter entries before querying

* style(runs): compute the trimmed event type once per entry

Addresses the review nit on #7758: use the walrus pattern already present
in this file (lines 657 and 1658) instead of stripping each entry twice.

No behavior change.

---------

Co-authored-by: gouyu-hou <gouyu-hou@users.noreply.github.com>
Co-authored-by: gouyu-hou <213296172+gouyu-hou@users.noreply.github.com>
This commit is contained in:
Xiao Hou 2026-11-01 23:27:00 +08:00 committed by GitHub
parent 51f732304a
commit 830c8ff6f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 1 deletions

View File

@ -1920,12 +1920,18 @@ async def list_run_events(
user_id = await _run_scope_user_id(request, thread_id)
event_store = get_run_event_store(request)
if event_types:
types = event_types.split(",")
# Split on commas and normalise each entry: the value goes straight into
# a SQL ``IN``, so a padded entry ("message, tool") would silently match
# nothing and return a 200 with a truncated audit trail. Blank entries
# carry no intent and are dropped; a filter that reduces to nothing is
# the same as no filter, matching the empty-string case above.
types = [name for entry in event_types.split(",") if (name := entry.strip())]
if len(types) > MAX_EVENT_TYPE_FILTERS:
raise HTTPException(
status_code=422,
detail=f"event_types accepts at most {MAX_EVENT_TYPE_FILTERS} values",
)
types = types or None
else:
types = None
events = await event_store.list_events(

View File

@ -253,3 +253,56 @@ async def test_list_run_events_bounds_event_type_filter_cardinality(count: int)
else:
assert await list_run_events(**kwargs) == []
assert len(calls["event_types"]) == MAX_EVENT_TYPE_FILTERS
async def _event_type_filter_reaches_store(event_types: str | None) -> object:
"""Call the route with *event_types* and return what the store received."""
from app.gateway.routers.thread_runs import list_run_events
calls: dict = {}
class FakeStore:
async def list_events(self, thread_id, run_id, *, event_types=None, task_id=None, limit=500, after_seq=None, user_id=None):
calls["event_types"] = event_types
return []
class FakeState:
run_event_store = FakeStore()
class FakeApp:
state = FakeState()
class FakeRequest:
app = FakeApp()
_deerflow_test_bypass_auth = True
cookies: dict = {}
await list_run_events(
thread_id="t1",
run_id="r1",
request=FakeRequest(),
event_types=event_types,
task_id=None,
limit=500,
after_seq=None,
)
return calls["event_types"]
@pytest.mark.parametrize("raw", ["message, tool", " message ,tool ", "message,\ttool\n"])
@pytest.mark.anyio
async def test_event_type_filter_entries_are_trimmed_before_reaching_the_store(raw: str) -> None:
"""A padded filter must not silently match nothing.
The entries are handed to a SQL ``IN``, so ``" tool"`` (leading space, the
natural result of ``?event_types=message, tool``) matched no row and the
endpoint returned 200 with a truncated audit trail.
"""
assert await _event_type_filter_reaches_store(raw) == ["message", "tool"]
@pytest.mark.parametrize("raw, expected", [("message,,tool", ["message", "tool"]), (" , ", None), (",", None), ("", None)])
@pytest.mark.anyio
async def test_event_type_filter_drops_blank_entries(raw: str, expected) -> None:
assert await _event_type_filter_reaches_store(raw) == expected