feat(FN-2875): add done-task elapsed time ceiling rounding

- Add formatElapsedDurationDone to render done task elapsed time with ceiling-based minute/hour/day rounding
- Keep in-progress time indicator logic on existing floor-based formatElapsedDuration behavior
- Route done-card time indicator rendering through the new done-specific formatter
- Expand TaskCard tests to cover done rounding semantics and preserve in-progress rounding expectations
This commit is contained in:
Fusion
2026-04-28 10:43:43 -07:00
committed by gsxdsm
parent ad0a1f489b
commit 5b11f33c3c
2 changed files with 91 additions and 6 deletions

View File

@@ -169,6 +169,20 @@ function formatElapsedDuration(elapsedMs: number): string {
return `${elapsedDays}d`;
}
export function formatElapsedDurationDone(elapsedMs: number): string {
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return "";
if (elapsedMs === 0) return "";
const elapsedMinutes = Math.ceil(elapsedMs / 60_000);
if (elapsedMinutes < 59) return `${elapsedMinutes}m`;
const elapsedHours = Math.ceil(elapsedMs / 3_600_000);
if (elapsedHours < 24) return `${elapsedHours}h`;
const elapsedDays = Math.ceil(elapsedMs / 86_400_000);
return `${elapsedDays}d`;
}
interface TaskCardProps {
task: Task;
@@ -692,12 +706,12 @@ function TaskCardComponent({
return null;
}
const elapsedLabel = formatElapsedDuration(instrumentedMs);
if (!elapsedLabel) {
return null;
}
if (task.column === "in-progress") {
const elapsedLabel = formatElapsedDuration(instrumentedMs);
if (!elapsedLabel) {
return null;
}
return {
label: elapsedLabel,
title: `Execution time ${elapsedLabel}`,
@@ -705,6 +719,11 @@ function TaskCardComponent({
};
}
const elapsedLabel = formatElapsedDurationDone(instrumentedMs);
if (!elapsedLabel) {
return null;
}
const completionMs = getDoneCompletionMs(task);
if (completionMs == null) {
return {

View File

@@ -1,6 +1,6 @@
import { afterEach, describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { TaskCard } from "../TaskCard";
import { TaskCard, formatElapsedDurationDone } from "../TaskCard";
import type { Task } from "@fusion/core";
// Mock lucide-react to avoid SVG rendering issues in test env
@@ -712,6 +712,72 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-time-indicator")).toBeNull();
});
describe("formatElapsedDuration rounding for done tasks", () => {
it.each([
[59_999, "1m"],
[60_000, "1m"],
[90_000, "2m"],
[3_540_000, "1h"],
[3_600_000, "1h"],
[86_400_000, "1d"],
])("formats %dms as %s for done tasks", (elapsedMs, expected) => {
expect(formatElapsedDurationDone(elapsedMs)).toBe(expected);
});
it("keeps in-progress rounding with floor semantics", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-25T12:01:30.000Z"));
const { container } = render(
<TaskCard
task={makeTask({
column: "in-progress",
workflowStepResults: [
{
workflowStepId: "step-1",
workflowStepName: "Plan",
phase: "pre-merge" as const,
status: "pending" as const,
startedAt: "2026-04-25T12:00:00.000Z",
},
],
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("1m");
});
it("renders done-card timer with ceiling rounding for fractional minutes", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "done",
createdAt: "2026-04-25T12:00:00.000Z",
columnMovedAt: "2026-04-25T12:04:30.000Z",
updatedAt: "2026-04-25T12:04:30.000Z",
workflowStepResults: [
{
workflowStepId: "step-1",
workflowStepName: "Plan",
phase: "pre-merge" as const,
status: "passed" as const,
startedAt: "2026-04-25T12:00:00.000Z",
completedAt: "2026-04-25T12:04:30.000Z",
},
],
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("5m");
});
});
it("live-ticks workflow runtime for in-progress steps", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-25T12:00:30.000Z"));