fix(infra): roll session warning durations across unit boundaries (#100096)

* fix(infra): formatDuration rolls over near-boundary values to the next unit

The raw float was checked against the boundary before rounding, so values
like 59.5s, 59m30s, and 23h59m30s rounded up to "60 seconds", "60 minutes",
and "24 hours" in session maintenance warning messages instead of "1 minute",
"1 hour", and "1 day". Round first, then check the unit boundary.

Closes #99978

* fix(infra): fix formatDuration progressive rollover in session maintenance warning

Round seconds first, then promote to the next unit only when the
rounded lower unit reaches its overflow threshold (60s->min, 60m->hr,
24h->day). The previous approach compared raw milliseconds against the
unit boundary before rounding, causing half-unit values like 30s, 30m,
and 12h to promote one unit too early.

Closes #99978

* test(infra): expose formatDuration via testing export for direct verification

* ci: retrigger checks

* ci: retrigger checks

* test(infra): keep duration formatter private

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Narahari Raghava
2026-07-04 21:02:30 -05:00
committed by GitHub
parent 48d12dcc8d
commit 7d3cfa85dc
2 changed files with 44 additions and 11 deletions

View File

@@ -199,4 +199,37 @@ describe("deliverSessionMaintenanceWarning", () => {
{ sessionKey: params.sessionKey },
]);
});
it.each([
[59_500, "60 seconds", "1 minute"],
[3_570_000, "60 minutes", "1 hour"],
[86_370_000, "24 hours", "1 day"],
])(
"formatDuration rolls over %dms to next unit instead of %s",
async (pruneAfterMs, _buggyOutput, expected) => {
mocks.deliverOutboundPayloads.mockRejectedValueOnce(new Error("force system event"));
const params = createParams({
warning: { pruneAfterMs, wouldPrune: true, wouldCap: false, maxEntries: 100 } as never,
});
await deliverSessionMaintenanceWarning(params);
expect(firstSystemEventCall()?.[0]).toContain(`older than ${expected}`);
},
);
it.each([
[30_000, "30 seconds"],
[1_800_000, "30 minutes"],
[43_200_000, "12 hours"],
])("formatDuration keeps %dms in its own unit as %s", async (pruneAfterMs, expected) => {
mocks.deliverOutboundPayloads.mockRejectedValueOnce(new Error("force system event"));
const params = createParams({
warning: { pruneAfterMs, wouldPrune: true, wouldCap: false, maxEntries: 100 } as never,
});
await deliverSessionMaintenanceWarning(params);
expect(firstSystemEventCall()?.[0]).toContain(`older than ${expected}`);
});
});

View File

@@ -54,20 +54,20 @@ function buildWarningContext(params: WarningParams): string {
}
function formatDuration(ms: number): string {
if (ms >= 86_400_000) {
const days = Math.round(ms / 86_400_000);
return `${days} day${days === 1 ? "" : "s"}`;
const secs = Math.round(ms / 1000);
if (secs < 60) {
return `${secs} second${secs === 1 ? "" : "s"}`;
}
if (ms >= 3_600_000) {
const hours = Math.round(ms / 3_600_000);
return `${hours} hour${hours === 1 ? "" : "s"}`;
}
if (ms >= 60_000) {
const mins = Math.round(ms / 60_000);
const mins = Math.round(secs / 60);
if (mins < 60) {
return `${mins} minute${mins === 1 ? "" : "s"}`;
}
const secs = Math.round(ms / 1000);
return `${secs} second${secs === 1 ? "" : "s"}`;
const hours = Math.round(mins / 60);
if (hours < 24) {
return `${hours} hour${hours === 1 ? "" : "s"}`;
}
const days = Math.round(hours / 24);
return `${days} day${days === 1 ? "" : "s"}`;
}
function buildWarningText(warning: SessionMaintenanceWarning): string {