From 75aad972a7a61f498ac772448918d06e833ef605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Fri, 3 Apr 2026 14:29:02 +0100 Subject: [PATCH] fix: increment CI checks elapsed time for in-progress runs in PR panel (#529) --- .../components/github/pr-checks-section.tsx | 18 ++- apps/web/e2e/helpers/api-client.ts | 2 + apps/web/e2e/tests/pr/pr-checks-timer.spec.ts | 128 ++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 apps/web/e2e/tests/pr/pr-checks-timer.spec.ts diff --git a/apps/web/components/github/pr-checks-section.tsx b/apps/web/components/github/pr-checks-section.tsx index 87cc49b308..73ac912d12 100644 --- a/apps/web/components/github/pr-checks-section.tsx +++ b/apps/web/components/github/pr-checks-section.tsx @@ -1,3 +1,4 @@ +import { useMemo, useState, useEffect } from "react"; import { IconCheck, IconX, IconClock } from "@tabler/icons-react"; import type { CheckRun } from "@/lib/types/github"; import { CollapsibleSection, AddToContextButton, formatDuration, formatElapsed } from "./pr-shared"; @@ -79,6 +80,15 @@ export function ChecksSection({ checks: CheckRun[]; onAddAsContext: (message: string) => void; }) { + const hasRunning = useMemo(() => checks.some((c) => c.started_at && !c.completed_at), [checks]); + + const [, setTick] = useState(0); + useEffect(() => { + if (!hasRunning) return; + const id = setInterval(() => setTick((t) => t + 1), 1_000); + return () => clearInterval(id); + }, [hasRunning]); + const summary = checks.length > 0 ? ` \u2014 ${formatSectionSummary(checks)}` : ""; const hasFailed = checks.some(isFailedCheck); @@ -98,6 +108,7 @@ export function ChecksSection({ return (
@@ -115,7 +126,12 @@ export function ChecksSection({ )} {label && {label}} {duration && ( - {duration} + + {duration} + )} {isFailedCheck(check) && (
diff --git a/apps/web/e2e/helpers/api-client.ts b/apps/web/e2e/helpers/api-client.ts index ca2bc4b96c..7f28c518a0 100644 --- a/apps/web/e2e/helpers/api-client.ts +++ b/apps/web/e2e/helpers/api-client.ts @@ -56,6 +56,8 @@ export type MockCheckRun = { status: string; conclusion?: string; html_url?: string; + started_at?: string; + completed_at?: string; }; function setIf(body: Record, key: string, value: unknown) { diff --git a/apps/web/e2e/tests/pr/pr-checks-timer.spec.ts b/apps/web/e2e/tests/pr/pr-checks-timer.spec.ts new file mode 100644 index 0000000000..865fedaa77 --- /dev/null +++ b/apps/web/e2e/tests/pr/pr-checks-timer.spec.ts @@ -0,0 +1,128 @@ +import { test, expect } from "../../fixtures/test-base"; +import { KanbanPage } from "../../pages/kanban-page"; +import { SessionPage } from "../../pages/session-page"; + +test.describe("PR checks running timer", () => { + /** + * Verifies that the elapsed time for in-progress CI checks increments + * live (not frozen) in the PR detail panel. + * + * Setup: + * Inbox -> Working (auto_start, on_turn_complete -> Done) -> Done + * Task with PR #201, check run "CI" in_progress with started_at in the past + */ + test("in-progress check duration increments over time", async ({ + testPage, + apiClient, + seedData, + }) => { + test.setTimeout(120_000); + + // --- Seed workflow --- + const workflow = await apiClient.createWorkflow(seedData.workspaceId, "Checks Timer Workflow"); + const inboxStep = await apiClient.createWorkflowStep(workflow.id, "Inbox", 0); + const workingStep = await apiClient.createWorkflowStep(workflow.id, "Working", 1); + const doneStep = await apiClient.createWorkflowStep(workflow.id, "Done", 2); + + await apiClient.updateWorkflowStep(workingStep.id, { + prompt: 'e2e:message("done")\n{{task_prompt}}', + events: { + on_enter: [{ type: "auto_start_agent" }], + on_turn_complete: [{ type: "move_to_step", config: { step_id: doneStep.id } }], + }, + }); + + await apiClient.saveUserSettings({ + workspace_id: seedData.workspaceId, + workflow_filter_id: workflow.id, + enable_preview_on_click: false, + }); + + // --- Seed mock GitHub data --- + await apiClient.mockGitHubReset(); + await apiClient.mockGitHubSetUser("test-user"); + + await apiClient.mockGitHubAddPRs([ + { + number: 201, + title: "Timer test PR", + state: "open", + head_branch: "feat/timer", + head_sha: "abc123", + base_branch: "main", + author_login: "test-user", + repo_owner: "testorg", + repo_name: "testrepo", + }, + ]); + + // Add an in-progress check with started_at 2 minutes ago + const twoMinAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString(); + await apiClient.mockGitHubAddCheckRuns("testorg", "testrepo", "abc123", [ + { + name: "CI", + status: "in_progress", + started_at: twoMinAgo, + }, + ]); + + // --- Create task --- + const task = await apiClient.createTask(seedData.workspaceId, "Timer Check Task", { + workflow_id: workflow.id, + workflow_step_id: inboxStep.id, + agent_profile_id: seedData.agentProfileId, + repository_ids: [seedData.repositoryId], + }); + + const kanban = new KanbanPage(testPage); + await kanban.goto(); + + // Move task to Working -> auto_start -> Done + await apiClient.moveTask(task.id, workflow.id, workingStep.id); + + // Associate PR with task + await apiClient.mockGitHubAssociateTaskPR({ + task_id: task.id, + owner: "testorg", + repo: "testrepo", + pr_number: 201, + pr_url: "https://github.com/testorg/testrepo/pull/201", + pr_title: "Timer test PR", + head_branch: "feat/timer", + base_branch: "main", + author_login: "test-user", + }); + + // Wait for task to reach Done + await expect(kanban.taskCardInColumn("Timer Check Task", doneStep.id)).toBeVisible({ + timeout: 45_000, + }); + + // --- Open task --- + await kanban.taskCardInColumn("Timer Check Task", doneStep.id).click(); + await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 }); + + const session = new SessionPage(testPage); + await session.waitForLoad(); + await session.idleInput().waitFor({ state: "visible", timeout: 30_000 }); + + // Open PR detail panel + await expect(session.prTopbarButton()).toBeVisible({ timeout: 15_000 }); + await session.prTopbarButton().click(); + await expect(session.prDetailPanel()).toBeVisible({ timeout: 10_000 }); + + // Find the running check's duration element + const durationEl = testPage.getByTestId("check-duration-CI"); + await expect(durationEl).toBeVisible({ timeout: 10_000 }); + + // Capture the initial text and verify it contains "running" + const initialText = await durationEl.textContent(); + expect(initialText).toContain("running"); + + // Wait ~3 seconds and verify the time has changed (incremented) + await testPage.waitForTimeout(3_000); + const updatedText = await durationEl.textContent(); + expect(updatedText).toContain("running"); + expect(updatedText).not.toBe(initialText); + }); +});