diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 7b573ef032c..cb981180726 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -18,6 +18,7 @@ import { getLastProjectId } from "../lib/recent-projects"; import { IssueProperties } from "./IssueProperties"; import { applyOptimisticIssueFieldUpdate } from "../lib/optimistic-issue-comments"; import { queryKeys } from "../lib/queryKeys"; +import { ApiError } from "../api/client"; const mockAgentsApi = vi.hoisted(() => ({ list: vi.fn(), @@ -48,6 +49,10 @@ const mockIssuesApi = vi.hoisted(() => ({ unarchiveFromInbox: vi.fn(), })); +const mockRoutinesApi = vi.hoisted(() => ({ + get: vi.fn(), +})); + const mockAuthApi = vi.hoisted(() => ({ getSession: vi.fn(), })); @@ -90,6 +95,10 @@ vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi, })); +vi.mock("../api/routines", () => ({ + routinesApi: mockRoutinesApi, +})); + vi.mock("../api/auth", () => ({ authApi: mockAuthApi, })); @@ -628,6 +637,7 @@ describe("IssueProperties", () => { document.body.appendChild(container); mockAgentsApi.list.mockResolvedValue([]); mockAgentsApi.adapterModels.mockResolvedValue([]); + mockRoutinesApi.get.mockReset(); mockProjectsApi.list.mockResolvedValue([]); mockExecutionWorkspacesApi.list.mockResolvedValue([]); mockExecutionWorkspacesApi.controlRuntimeCommands.mockReset(); @@ -1141,6 +1151,77 @@ describe("IssueProperties", () => { act(() => root.unmount()); }); + it("links a routine-created task back to its source routine under Relationships", async () => { + mockRoutinesApi.get.mockResolvedValue({ id: "routine-1", title: "Nightly triage" }); + const root = renderProperties(container, { + issue: createIssue({ originKind: "routine_execution", originId: "routine-1" }), + childIssues: [], + onUpdate: vi.fn(), + inline: true, + }); + + await waitForAssertion(() => { + const row = container + .querySelector('[data-property-label="Routine"]') + ?.closest('[data-property-row="true"]'); + const link = row?.querySelector("a"); + expect(link?.getAttribute("href")).toBe("/routines/routine-1"); + expect(link?.textContent).toContain("Nightly triage"); + }); + expect(mockRoutinesApi.get).toHaveBeenCalledWith("routine-1"); + + act(() => root.unmount()); + }); + + it("omits the Routine row while the source routine is still loading", async () => { + mockRoutinesApi.get.mockReturnValue(new Promise(() => {})); + const root = renderProperties(container, { + issue: createIssue({ originKind: "routine_execution", originId: "routine-1" }), + childIssues: [], + onUpdate: vi.fn(), + inline: true, + }); + await flush(); + + expect(mockRoutinesApi.get).toHaveBeenCalledWith("routine-1"); + expect(container.querySelector('[data-property-label="Routine"]')).toBeNull(); + + act(() => root.unmount()); + }); + + it("omits the Routine row when the source routine no longer exists", async () => { + mockRoutinesApi.get.mockRejectedValue(new ApiError("Not found", 404, null)); + const root = renderProperties(container, { + issue: createIssue({ originKind: "routine_execution", originId: "routine-gone" }), + childIssues: [], + onUpdate: vi.fn(), + inline: true, + }); + + await waitForAssertion(() => { + expect(mockRoutinesApi.get).toHaveBeenCalledWith("routine-gone"); + }); + await flush(); + expect(container.querySelector('[data-property-label="Routine"]')).toBeNull(); + + act(() => root.unmount()); + }); + + it("omits the Routine row for tasks not created by a routine", async () => { + const root = renderProperties(container, { + issue: createIssue(), + childIssues: [], + onUpdate: vi.fn(), + inline: true, + }); + await flush(); + + expect(container.querySelector('[data-property-label="Routine"]')).toBeNull(); + expect(mockRoutinesApi.get).not.toHaveBeenCalled(); + + act(() => root.unmount()); + }); + it("groups the assignee picker and gates a live-run reassign behind an interrupt confirm", async () => { const minimalAgent = (id: string, name: string) => ({ diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx index 736987aaa1a..924c7c8eb05 100644 --- a/ui/src/components/issue-properties/IssueProperties.tsx +++ b/ui/src/components/issue-properties/IssueProperties.tsx @@ -125,6 +125,7 @@ import { import { issueReviewPolicyBadge } from "../../lib/review-policy"; import { IssueCasesPanel } from "../IssueCasesPanel"; import { ExpandRelationListButton, RemovableIssueReferencePill } from "./relation-controls"; +import { RoutineOriginRow } from "./routine-origin-row"; import { Badge } from "@/components/ui/badge"; import { TaskDetailReferencesPanel, @@ -2447,6 +2448,10 @@ export function IssueProperties({ {parentContent} + {issue.originKind === "routine_execution" && issue.originId ? ( + + ) : null} + {streamlinedPropertiesEnabled ? ( routinesApi.get(routineId), + }); + + const title = routine?.title?.trim(); + if (!title) return null; + + return ( + + + + {title} + + + ); +}