From c076085eee868caaaba2cff6832ccfd23fc1749e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:40:31 +0000 Subject: [PATCH] fix: use integer truncation in FormatAge to prevent rounding past display boundaries Math.Round with C# default banker's rounding (MidpointRounding.ToEven) can produce counterintuitive results at display-threshold boundaries: - 59.5 minutes -> Math.Round(59.5) = 60 -> displayed as '60m ago' instead of '59m ago' (or the correct transition to '1h ago') - 47.5 hours -> Math.Round(47.5) = 48 -> displayed as '48h ago' instead of '47h ago' (near the 48h/days boundary) Using integer truncation ((int)delta.TotalX) matches the idiomatic convention for age display: show the floor of the elapsed time, which is consistent, predictable, and never exceeds the guard condition. Adds three regression tests covering: - 59.5-minute boundary (was '60m ago', now '59m ago') - 47.5-hour boundary (was '48h ago', now '47h ago') - Exactly 60 seconds (correctly '1m ago') Test status: Shared.Tests 589 passed, 20 skipped; Tray.Tests 122 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Shared/Models.cs | 6 +++--- tests/OpenClaw.Shared.Tests/ModelsTests.cs | 25 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/OpenClaw.Shared/Models.cs b/src/OpenClaw.Shared/Models.cs index 15b1afb..44de5db 100644 --- a/src/OpenClaw.Shared/Models.cs +++ b/src/OpenClaw.Shared/Models.cs @@ -441,9 +441,9 @@ internal static class ModelFormatting { var delta = DateTime.UtcNow - timestampUtc; if (delta.TotalSeconds < 60) return "just now"; - if (delta.TotalMinutes < 60) return $"{(int)Math.Round(delta.TotalMinutes)}m ago"; - if (delta.TotalHours < 48) return $"{(int)Math.Round(delta.TotalHours)}h ago"; - return $"{(int)Math.Round(delta.TotalDays)}d ago"; + if (delta.TotalMinutes < 60) return $"{(int)delta.TotalMinutes}m ago"; + if (delta.TotalHours < 48) return $"{(int)delta.TotalHours}h ago"; + return $"{(int)delta.TotalDays}d ago"; } /// diff --git a/tests/OpenClaw.Shared.Tests/ModelsTests.cs b/tests/OpenClaw.Shared.Tests/ModelsTests.cs index 3b5c68b..03b300f 100644 --- a/tests/OpenClaw.Shared.Tests/ModelsTests.cs +++ b/tests/OpenClaw.Shared.Tests/ModelsTests.cs @@ -762,6 +762,31 @@ public class SessionInfoAgeTextTests }; Assert.Equal("10m ago", session.AgeText); } + + [Fact] + public void AgeText_NearMinuteBoundary_DoesNotRoundUpTo60m() + { + // 59.5 minutes: Math.Round would produce 60 with banker's rounding; + // truncation correctly yields 59m ago. + var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddSeconds(-3570) }; // 59.5 min + Assert.Equal("59m ago", session.AgeText); + } + + [Fact] + public void AgeText_NearHourBoundary_DoesNotRoundUpTo48h() + { + // 47.5 hours: Math.Round would produce 48 with banker's rounding; + // truncation correctly yields 47h ago. + var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddSeconds(-(int)(47.5 * 3600)) }; + Assert.Equal("47h ago", session.AgeText); + } + + [Fact] + public void AgeText_ExactlyOneMinute_ShowsMinutesAgo() + { + var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddSeconds(-60) }; + Assert.Equal("1m ago", session.AgeText); + } } public class SessionInfoRichDisplayTextTests