import { act, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { setAgentGuiI18nTestLocale } from "../../../i18n/testUtils"; import type { AgentTaskSubAgentVM } from "../contracts/agentTaskItemVM"; import { AgentSubAgentCard } from "./AgentSubAgentCards"; describe("AgentSubAgentCard", () => { afterEach(() => { vi.useRealTimers(); setAgentGuiI18nTestLocale("zh-CN"); }); it("keeps ticking while running even when no new sub-agent activity arrives", async () => { setAgentGuiI18nTestLocale("en"); vi.useFakeTimers(); vi.setSystemTime(1_000); render( ); expect(screen.getByText("Starting…")).toBeInTheDocument(); await act(async () => { await vi.advanceTimersByTimeAsync(2_000); }); expect(screen.getByText(/2\.0s · Running/)).toBeInTheDocument(); await act(async () => { await vi.advanceTimersByTimeAsync(9_000); }); expect(screen.getByText(/11s · Running/)).toBeInTheDocument(); }); it("does not tick while queued for a concurrency slot, even as time passes", async () => { setAgentGuiI18nTestLocale("en"); vi.useFakeTimers(); vi.setSystemTime(1_000); render( ); expect( screen.getByText("Queued — waiting for an agent slot…") ).toBeInTheDocument(); await act(async () => { await vi.advanceTimersByTimeAsync(11_000); }); // A queued sub-agent has not been dispatched yet: startedAtUnixMs is the // spawn call's own (dispatch) time, not a real run-start time, so no // elapsed clock must appear or tick while queued=true. expect(screen.queryByText(/\d+(\.\d+)?s ·/)).not.toBeInTheDocument(); expect( screen.getByText("Queued — waiting for an agent slot…") ).toBeInTheDocument(); }); it("starts ticking only once dispatch clears the queue", async () => { setAgentGuiI18nTestLocale("en"); vi.useFakeTimers(); vi.setSystemTime(1_000); const { rerender } = render( ); await act(async () => { await vi.advanceTimersByTimeAsync(9_000); }); expect(screen.queryByText(/\d+(\.\d+)?s ·/)).not.toBeInTheDocument(); // The sub-agent clears the queue at t=10_000 and actually starts running. vi.setSystemTime(10_000); rerender( ); await act(async () => { await vi.advanceTimersByTimeAsync(2_000); }); expect(screen.getByText(/2\.0s · Running/)).toBeInTheDocument(); }); it("freezes terminal elapsed time instead of following Date.now", async () => { setAgentGuiI18nTestLocale("en"); vi.useFakeTimers(); vi.setSystemTime(10_000); render( ); expect(screen.getByText(/5\.0s · Completed/)).toBeInTheDocument(); await act(async () => { vi.setSystemTime(120_000); await vi.advanceTimersByTimeAsync(5_000); }); expect(screen.getByText(/5\.0s · Completed/)).toBeInTheDocument(); expect(screen.queryByText(/1m 59s/)).not.toBeInTheDocument(); }); it("shows completed instead of starting when terminal output is absent", async () => { setAgentGuiI18nTestLocale("en"); render( ); fireEvent.click( screen.getByRole("button", { name: /Sub-agent Repo smell analyst Completed/ }) ); await act(async () => { await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); }); expect(screen.getByText("Completed")).toBeInTheDocument(); expect(screen.queryByText("Starting…")).not.toBeInTheDocument(); }); it.each(["running", "completed"] as const)( "renders complete Markdown and routes its link while %s", async (status) => { setAgentGuiI18nTestLocale("en"); const onLinkClick = vi.fn(); render( ); if (status === "completed") { fireEvent.click( screen.getByRole("button", { name: /Sub-agent Repo smell analyst Completed/ }) ); } fireEvent.click(await screen.findByRole("link", { name: "report" })); expect(onLinkClick).toHaveBeenCalledWith( "https://example.com/report.pdf" ); expect(screen.queryByText("…report.pdf)")).not.toBeInTheDocument(); } ); it("shows failed instead of starting when failure detail is absent", async () => { setAgentGuiI18nTestLocale("en"); render( ); fireEvent.click( screen.getByRole("button", { name: /Sub-agent Repo smell analyst Failed/ }) ); await act(async () => { await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); }); expect( screen.getByText("Failed", { selector: ".workspace-agents-status-panel__detail-subagent-activity--in-terminal" }) ).toBeInTheDocument(); expect(screen.queryByText("Starting…")).not.toBeInTheDocument(); }); it("renders a completed Markdown result without duplicating its progress summary", async () => { setAgentGuiI18nTestLocale("en"); const onLinkClick = vi.fn(); render( ); fireEvent.click( screen.getByRole("button", { name: /Sub-agent Repo smell analyst Completed/ }) ); expect( await screen.findByRole("heading", { name: "Full result" }) ).toBeInTheDocument(); fireEvent.click(screen.getByRole("link", { name: "report" })); expect(onLinkClick).toHaveBeenCalledWith("https://example.com/report.pdf"); expect( screen.queryByText("Full result", { selector: "div" }) ).not.toBeInTheDocument(); }); it("keeps partial assistant Markdown alongside a terminal failure", async () => { setAgentGuiI18nTestLocale("en"); render( ); fireEvent.click( screen.getByRole("button", { name: /Sub-agent Repo smell analyst Failed/ }) ); expect(await screen.findByText("report")).toBeInTheDocument(); expect(screen.getByText("Provider connection failed")).toBeInTheDocument(); }); it("updates the same child from streaming Markdown to its completed result", async () => { setAgentGuiI18nTestLocale("en"); const { rerender } = render( ); expect(await screen.findByText("Streaming opening")).toBeInTheDocument(); rerender( ); expect(await screen.findByText("completed result")).toBeInTheDocument(); }); }); function subAgent( overrides: Partial = {} ): AgentTaskSubAgentVM { return { childSessionId: "child-session-1", parentToolCallId: "spawn-1", status: "running", name: "Repo smell analyst", task: "inspect the repository", laneIndex: 1, laneCount: 1, latestActivity: null, latestActivityKind: null, activityLog: [], activityOmittedCount: 0, failureDetail: null, startedAtUnixMs: 1_000, latestActivityAtUnixMs: 1_000, terminalAtUnixMs: null, childSessions: [], ...overrides }; }