From 42800ceed6816d49c2e73347615e75bf36ca576e Mon Sep 17 00:00:00 2001 From: KVR Vishnu <46535652+VISHNU0906@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:26:23 +0530 Subject: [PATCH] fix(server): preserve numeric zero data_id in terminal projection _data_id_of used truthiness (x or y), so data_id/instance_id of 0 were treated as missing and fell through to the fallback or "". Select the first present non-None/non-empty id instead, and cover zero and fallback cases through GET /api/rollouts/terminal. Fixes #659 Signed-off-by: KVR Vishnu <46535652+VISHNU0906@users.noreply.github.com> --- agentlightning/server/routes/rollouts.py | 15 ++++++- tests/server/test_endpoints.py | 53 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/agentlightning/server/routes/rollouts.py b/agentlightning/server/routes/rollouts.py index f4598ef04..f6345a6b4 100644 --- a/agentlightning/server/routes/rollouts.py +++ b/agentlightning/server/routes/rollouts.py @@ -133,10 +133,23 @@ async def list_rollouts( return matches[:limit] +def _present_id(value: object) -> bool: + """True when value is a usable dataset/instance id (keeps numeric zero).""" + return value is not None and value != "" + + def _data_id_of(rollout: Rollout) -> str: + """Project data_id (or instance_id fallback) for the terminal listing. + + Explicit numeric ``0`` must survive; only missing / ``None`` / ``""`` fall back. + """ inp = rollout.input if isinstance(inp, dict): - return str(inp.get("data_id") or inp.get("instance_id") or "") + for key in ("data_id", "instance_id"): + value = inp.get(key) + if _present_id(value): + return str(value) + return "" return "" diff --git a/tests/server/test_endpoints.py b/tests/server/test_endpoints.py index e23fadfae..833c6aaf0 100644 --- a/tests/server/test_endpoints.py +++ b/tests/server/test_endpoints.py @@ -530,3 +530,56 @@ def test_terminal_rollouts_cursor_pagination(client: TestClient, auth_headers: d page3 = client.get("/api/rollouts/terminal", params={"after": 3}, headers=auth_headers) assert page3.json() == {"items": [], "next_after": 3, "total_terminal": 3} assert pending not in {it["rollout_id"] for it in body1["items"] + body2["items"]} + + +def _terminal_with_input(client: TestClient, headers: dict[str, str], rollout_input: dict) -> str: + """Create a rollout with the given input and drive it to succeeded.""" + created = client.post( + "/api/rollouts", + json=[{"input": rollout_input}], + headers=headers, + ) + assert created.status_code == 201 + rid = created.json()[0]["rollout_id"] + assert ( + client.patch(f"/api/rollouts/{rid}", json={"status": {"state": "running"}}, headers=headers).status_code == 200 + ) + assert ( + client.patch(f"/api/rollouts/{rid}", json={"status": {"state": "succeeded"}}, headers=headers).status_code + == 200 + ) + return rid + + +@pytest.mark.parametrize( + ("rollout_input", "expected_data_id"), + [ + ({"data_id": 0, "instance_id": "fallback"}, "0"), + ({"instance_id": 0}, "0"), + ({"data_id": 0, "instance_id": 0}, "0"), + ({"data_id": 7, "instance_id": "fallback"}, "7"), + ({"data_id": "item-1"}, "item-1"), + ({"data_id": "", "instance_id": "fallback"}, "fallback"), + ({"data_id": None, "instance_id": "fallback"}, "fallback"), + ({"instance_id": "only-instance"}, "only-instance"), + ({"prompt": "no-ids"}, ""), + ], +) +def test_terminal_rollouts_preserves_numeric_zero_data_id( + client: TestClient, + auth_headers: dict[str, str], + rollout_input: dict, + expected_data_id: str, +): + """GET /api/rollouts/terminal must keep explicit 0 and only fall back for missing/None/"".""" + rid = _terminal_with_input(client, auth_headers, rollout_input) + page = client.get("/api/rollouts/terminal", headers=auth_headers) + assert page.status_code == 200 + body = page.json() + assert body["total_terminal"] == 1 + assert body["items"][0] == { + "rollout_id": rid, + "state": "succeeded", + "data_id": expected_data_id, + "is_train": True, + }