-
Notifications
You must be signed in to change notification settings - Fork 35
fix(result): normalize result artifact timestamps #928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """Timestamp formatting helpers for persisted artifacts.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from datetime import UTC, datetime | ||
|
|
||
|
|
||
| def artifact_timestamp(value: datetime | None) -> str | None: | ||
| """Return a timezone-aware ISO 8601 timestamp for JSON artifacts. | ||
|
|
||
| Legacy rollout paths sometimes pass naive ``datetime`` values. Persisting | ||
| them with ``str(datetime)`` produced a space-separated timestamp without an | ||
| offset, so downstream consumers could not compare artifacts reliably. Treat | ||
| naive values as UTC at the artifact boundary and emit a compact ``Z`` suffix. | ||
| """ | ||
| if value is None: | ||
| return None | ||
| if value.tzinfo is None or value.tzinfo.utcoffset(value) is None: | ||
| value = value.replace(tzinfo=UTC) | ||
| else: | ||
| value = value.astimezone(UTC) | ||
| return value.isoformat().replace("+00:00", "Z") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
| import contextlib | ||
| import json | ||
| import logging | ||
| from datetime import datetime | ||
| from datetime import datetime, timedelta, timezone | ||
| from unittest.mock import AsyncMock, MagicMock, patch | ||
|
|
||
| import pytest | ||
|
|
@@ -136,6 +136,65 @@ def test_token_total_round_trips_into_agent_result(self, tmp_path): | |
| assert data["final_metrics"]["total_completion_tokens"] == 5993 | ||
| assert "total_tokens" not in data | ||
|
|
||
| def test_result_timestamps_are_timezone_aware_iso8601(self, tmp_path): | ||
| """Guards issue #528: result.json timestamps are ISO 8601 UTC strings.""" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For these new regression tests, the docstrings cite only issue #528; Useful? React with 👍 / 👎. |
||
| from benchflow.rollout import _build_rollout_result | ||
|
|
||
| rollout_dir = tmp_path / "trial" | ||
| rollout_dir.mkdir() | ||
| _build_rollout_result( | ||
| rollout_dir, | ||
| task_name="t1", | ||
| rollout_name="trial-1", | ||
| agent="test", | ||
| agent_name="openhands", | ||
| model="m", | ||
| n_tool_calls=0, | ||
| prompts=[], | ||
| error=None, | ||
| verifier_error=None, | ||
| trajectory=[], | ||
| partial_trajectory=False, | ||
| rewards={"reward": 1.0}, | ||
| started_at=datetime(2026, 3, 24, 10, 0), | ||
| timing={}, | ||
| ) | ||
|
|
||
| data = json.loads((rollout_dir / "result.json").read_text()) | ||
| assert data["started_at"] == "2026-03-24T10:00:00Z" | ||
| assert data["finished_at"].endswith("Z") | ||
| assert " " not in data["finished_at"] | ||
| assert datetime.fromisoformat(data["finished_at"].replace("Z", "+00:00")) | ||
|
|
||
| def test_result_timestamps_normalize_offsets_to_utc(self, tmp_path): | ||
| """Guards issue #528: aware result timestamps serialize in UTC.""" | ||
| from benchflow.rollout import _build_rollout_result | ||
|
|
||
| rollout_dir = tmp_path / "trial" | ||
| rollout_dir.mkdir() | ||
| _build_rollout_result( | ||
| rollout_dir, | ||
| task_name="t1", | ||
| rollout_name="trial-1", | ||
| agent="test", | ||
| agent_name="openhands", | ||
| model="m", | ||
| n_tool_calls=0, | ||
| prompts=[], | ||
| error=None, | ||
| verifier_error=None, | ||
| trajectory=[], | ||
| partial_trajectory=False, | ||
| rewards={"reward": 1.0}, | ||
| started_at=datetime( | ||
| 2026, 3, 24, 3, 0, tzinfo=timezone(timedelta(hours=-7)) | ||
| ), | ||
| timing={}, | ||
| ) | ||
|
|
||
| data = json.loads((rollout_dir / "result.json").read_text()) | ||
| assert data["started_at"] == "2026-03-24T10:00:00Z" | ||
|
|
||
|
|
||
| class TestSdkVerify: | ||
| @pytest.fixture | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When BenchFlow runs on a host whose local timezone is not UTC, native rollouts still create
started_atwith_init_rollout()'s local naivedatetime.now()(src/benchflow/rollout/_setup.py:272). This branch then simply attaches UTC to that wall time, soresult.json/config.jsonclaim e.g. 10:00 local is10:00Zinstead of converting to the actual UTC instant, corrupting artifact ordering and cross-run comparisons outside UTC environments.Useful? React with 👍 / 👎.