import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { makeTask, noop, noopDelete, noopMerge, noopMove, noopOpenDetail, mockConfirm, mockUsePluginUiSlots, expectBaseRule, readDashboardStylesSource, setupTaskDetailModalHooks, } from "./TaskDetailModal.test-helpers"; import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal"; setupTaskDetailModalHooks(); describe("TaskDetailModal", () => { describe("Agent Log model resolution", () => { // AgentLogViewer only renders the model header when entries.length > 0, // so we mock useAgentLogs to return at least one entry. const mockLogEntry = { timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }; async function setupModelTest(settingsOverrides: Record = {}) { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, ...settingsOverrides, } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [mockLogEntry], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); return render( , ); } async function setupModelTestWithTask(taskOverrides: Partial, settingsOverrides: Record = {}) { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, ...settingsOverrides, } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [mockLogEntry], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); return render( , ); } async function openAgentLogAndExpandModelDetails(container: HTMLElement) { fireEvent.click(screen.getByText("Logs")); fireEvent.click(screen.getByText("Agent Log")); await waitFor(() => { const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header).toBeTruthy(); }); const expandButton = screen.getByTestId("agent-log-model-expand") as HTMLButtonElement; if (expandButton.getAttribute("aria-expanded") !== "true") { fireEvent.click(expandButton); } return container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement; } it("shows resolved executor from settings when task has no explicit executor override", async () => { const { container } = await setupModelTest({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", }); const header = await openAgentLogAndExpandModelDetails(container); // Validator should also fall back to the default expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); }); it("shows the project default override before the global default", async () => { const { container } = await setupModelTest({ defaultProviderOverride: "openai", defaultModelIdOverride: "gpt-4o", defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", }); const header = await openAgentLogAndExpandModelDetails(container); const matches = header.textContent!.match(/openai\/gpt-4o/g); expect(matches).toHaveLength(3); expect(header.textContent).not.toContain("anthropic/claude-sonnet-4-5"); }); it("shows resolved validator from project validator settings when task has no validator override", async () => { const { container } = await setupModelTest({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o", }); const header = await openAgentLogAndExpandModelDetails(container); // Executor falls back to default expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); // Validator uses the validator-specific setting expect(header.textContent).toContain("openai/gpt-4o"); }); it("falls back to default settings for validator when no validator-specific setting exists", async () => { const { container } = await setupModelTest({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", // No validatorProvider or validatorModelId }); const header = await openAgentLogAndExpandModelDetails(container); // Count occurrences - should appear three times (once for executor, once for validator, once for planning) const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g); expect(matches).toHaveLength(3); }); it("shows task executor override even when settings provide a default", async () => { const { container } = await setupModelTestWithTask( { modelProvider: "openai", modelId: "gpt-4o" }, { defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" }, ); const header = await openAgentLogAndExpandModelDetails(container); // Default model should not appear for executor expect(header.textContent).toContain("openai/gpt-4o"); // Validator falls back to default expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); }); it("shows task validator override even when settings provide a validator default", async () => { const { container } = await setupModelTestWithTask( { validatorModelProvider: "google", validatorModelId: "gemini-pro" }, { defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" }, ); const header = await openAgentLogAndExpandModelDetails(container); // Executor falls back to default expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); // Settings validator should not appear (task override wins) expect(header.textContent).not.toContain("openai/gpt-4o"); }); it("shows 'Using default' for both when no models can be resolved", async () => { const { container } = await setupModelTest({ // No defaultProvider/defaultModelId }); const header = await openAgentLogAndExpandModelDetails(container); expect(header.textContent).toContain("Using default"); // Should show "Using default" for executor, validator, and planning const defaultBadges = header.querySelectorAll(".model-badge-default"); expect(defaultBadges).toHaveLength(3); }); it("shows 'Using default' for both when settings fetch fails", async () => { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockRejectedValueOnce(new Error("Network error")); vi.mocked(useAgentLogs).mockReturnValue({ entries: [mockLogEntry], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); // Wait for the failed fetch to settle const header = await openAgentLogAndExpandModelDetails(container); expect(header.textContent).toContain("Using default"); const defaultBadges = header.querySelectorAll(".model-badge-default"); expect(defaultBadges).toHaveLength(3); }); it("shows partial override: task executor with settings-based validator", async () => { const { container } = await setupModelTestWithTask( { modelProvider: "google", modelId: "gemini-pro", // No validator override — should use settings validator }, { defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o", }, ); const header = await openAgentLogAndExpandModelDetails(container); // Executor uses task override expect(header.textContent).toContain("google/gemini-pro"); // Validator uses settings-specific validator expect(header.textContent).toContain("openai/gpt-4o"); }); // Planning model resolution tests describe("Planning model resolution", () => { it("shows planning model from runtime triage log marker", async () => { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [ { timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }, { timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Triage using model: google/gemini-pro", type: "text" as const, agent: "triage" }, ], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); const header = await openAgentLogAndExpandModelDetails(container); // Planning should show the runtime triage marker, not settings default expect(header.textContent).toContain("Planning:"); expect(header.textContent).toContain("google/gemini-pro"); // Executor/Validator should still show settings default expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); }); it("shows planning model from settings planningProvider when no runtime marker", async () => { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", planningProvider: "openai", planningModelId: "gpt-4o", } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [mockLogEntry], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); const header = await openAgentLogAndExpandModelDetails(container); // Planning should use planningProvider/planningModelId from settings expect(header.textContent).toContain("Planning:"); expect(header.textContent).toContain("openai/gpt-4o"); // Executor/Validator should show default expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); // Planning should NOT show the default expect(header.textContent).toContain("openai/gpt-4o"); }); it("falls back to default settings for planning when no planning-specific setting exists", async () => { const { container } = await setupModelTest({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", }); const header = await openAgentLogAndExpandModelDetails(container); expect(header.textContent).toContain("Planning:"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); // Planning falls back to default - same as executor/validator const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g); expect(matches).toHaveLength(3); // executor, validator, planning }); it("shows 'Using default' for planning when no models can be resolved", async () => { const { container } = await setupModelTest({ // No defaultProvider/defaultModelId }); const header = await openAgentLogAndExpandModelDetails(container); expect(header.textContent).toContain("Planning:"); const defaultBadges = header.querySelectorAll(".model-badge-default"); // 3 default badges: executor, validator, planning expect(defaultBadges).toHaveLength(3); }); it("per-task planning model override takes precedence over settings", async () => { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", planningProvider: "openai", planningModelId: "gpt-4o", } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [mockLogEntry], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); const header = await openAgentLogAndExpandModelDetails(container); // Per-task override should take precedence over settings expect(header.textContent).toContain("Planning:"); expect(header.textContent).toContain("google/gemini-2.5-pro"); // Should NOT show the settings planning model expect(header.textContent).not.toContain("openai/gpt-4o"); }); it("runtime triage marker takes precedence over planningProvider settings", async () => { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", planningProvider: "openai", planningModelId: "gpt-4o", } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [ { timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }, { timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Triage using model: google/gemini-pro", type: "text" as const, agent: "triage" }, ], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); const header = await openAgentLogAndExpandModelDetails(container); // Runtime marker should win over planning settings expect(header.textContent).toContain("google/gemini-pro"); // Should NOT show the planning settings model expect(header.textContent).not.toContain("openai/gpt-4o"); }); }); }); it("shows executor/reviewer models from runtime agent-log markers", async () => { const { fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [ { timestamp: "2026-01-01T00:00:01Z", taskId: "FN-099", text: "Executor using model: openai/gpt-4o", type: "text" as const, agent: "executor" }, { timestamp: "2026-01-01T00:00:02Z", taskId: "FN-099", text: "Reviewer using model: google/gemini-2.5-pro", type: "text" as const, agent: "reviewer" }, ], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); fireEvent.click(screen.getByText("Logs")); fireEvent.click(screen.getByText("Agent Log")); await waitFor(() => { const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header).toBeTruthy(); }); const expandButton = screen.getByTestId("agent-log-model-expand") as HTMLButtonElement; if (expandButton.getAttribute("aria-expanded") !== "true") { fireEvent.click(expandButton); } const header = container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement; expect(header.textContent).toContain("openai/gpt-4o"); expect(header.textContent).toContain("google/gemini-2.5-pro"); }); it("falls back to assigned-agent runtime model when no runtime marker exists", async () => { const { fetchSettings, fetchAgent } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); vi.mocked(fetchSettings).mockResolvedValueOnce({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {}, defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", } as any); vi.mocked(fetchAgent).mockResolvedValueOnce({ id: "agent-1", name: "Agent One", role: "executor", state: "active", runtimeConfig: { model: "openai/gpt-4.1" }, } as any); vi.mocked(useAgentLogs).mockReturnValue({ entries: [{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }], loading: false, clear: vi.fn(), loadMore: vi.fn(async () => {}), hasMore: false, total: null, loadingMore: false, }); const { container } = render( , ); fireEvent.click(screen.getByText("Logs")); fireEvent.click(screen.getByText("Agent Log")); await waitFor(() => { const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header).toBeTruthy(); }); const expandButton = screen.getByTestId("agent-log-model-expand") as HTMLButtonElement; if (expandButton.getAttribute("aria-expanded") !== "true") { fireEvent.click(expandButton); } const header = container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement; expect(header.textContent).toContain("openai/gpt-4.1"); }); describe("step progress", () => { it("renders step progress section when steps exist", () => { const { container } = render( , ); expect(container.querySelector(".detail-step-progress")).toBeTruthy(); expect(screen.getByText("Progress")).toBeTruthy(); }); it("shows '(no steps defined)' when steps array is empty", () => { const { container } = render( , ); expect(container.querySelector(".detail-step-progress")).toBeTruthy(); expect(screen.getByText("(no steps defined)")).toBeTruthy(); }); it("renders correct number of segments matching step count", () => { const { container } = render( , ); const segments = container.querySelectorAll(".step-progress-segment"); expect(segments).toHaveLength(3); }); it("segments have correct status modifier classes", () => { const { container } = render( , ); const segments = container.querySelectorAll(".step-progress-segment"); expect(segments[0].classList.contains("step-progress-segment--done")).toBe(true); expect(segments[1].classList.contains("step-progress-segment--in-progress")).toBe(true); expect(segments[2].classList.contains("step-progress-segment--pending")).toBe(true); expect(segments[3].classList.contains("step-progress-segment--skipped")).toBe(true); }); it("segments have correct inline background colors based on status", () => { const { container } = render( , ); const segments = container.querySelectorAll(".step-progress-segment"); expect((segments[0] as HTMLElement).style.backgroundColor).toBe("var(--color-success)"); expect((segments[1] as HTMLElement).style.backgroundColor).toBe("var(--todo)"); expect((segments[2] as HTMLElement).style.backgroundColor).toBe("var(--border)"); expect((segments[3] as HTMLElement).style.backgroundColor).toBe("var(--text-dim)"); }); it("displays singular completion label for one-step tasks", () => { render( , ); expect(screen.getByText("1/1 step")).toBeTruthy(); expect(screen.queryByText("1/1 steps")).toBeNull(); }); it("displays correct completion count", () => { render( , ); expect(screen.getByText("2/4 steps")).toBeTruthy(); expect(screen.queryByText("2/4 step")).toBeNull(); }); it("has data-tooltip attribute with step name and status on each segment", () => { const { container } = render( , ); const segments = container.querySelectorAll(".step-progress-segment"); expect(segments[0].getAttribute("data-tooltip")).toBe("Initialize project (done)"); expect(segments[1].getAttribute("data-tooltip")).toBe("Add tests (in-progress)"); }); it("step progress only renders in Definition tab, not in Agent Log subview", () => { const { container } = render( , ); // Should be visible in Definition tab expect(container.querySelector(".detail-step-progress")).toBeTruthy(); // Switch to Logs tab, then Agent Log subview fireEvent.click(screen.getByText("Logs")); fireEvent.click(screen.getByText("Agent Log")); // Should not be visible in Agent Log subview expect(container.querySelector(".detail-step-progress")).toBeNull(); }); it("step progress is hidden in Comments tab", () => { const { container } = render( , ); // Switch to Comments tab fireEvent.click(screen.getByText("Comments")); // Should not be visible in Comments tab expect(container.querySelector(".detail-step-progress")).toBeNull(); }); }); describe("Commits tab visibility", () => { it.each<[string, Parameters[0]]>([ ["with mergeDetails.commitSha", { column: "done", mergeDetails: { commitSha: "abc1234567890", filesChanged: 3, insertions: 10, deletions: 2 } }], ["with mergeDetails but no commitSha", { column: "done", mergeDetails: { filesChanged: 3 } }], ["without mergeDetails", { column: "done" }], ])("never shows a separate Commits tab for done tasks (%s) — changes are in the Changes tab", (_label, taskOverrides) => { const { container } = render( , ); expect(screen.queryByText("Commits")).toBeNull(); const tabTexts = Array.from(container.querySelectorAll(".detail-tab")).map((t) => t.textContent); expect(tabTexts).toContain("Changes"); }); }); describe("comment state propagation (FN-845)", () => { it("passes onTaskUpdated to TaskComments when provided", async () => { const { addSteeringComment } = await import("../../api"); const onTaskUpdated = vi.fn(); const updatedTask = makeTask({ comments: [{ id: "c1", text: "New comment", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }], }); vi.mocked(addSteeringComment).mockResolvedValueOnce(updatedTask); render( , ); // Switch to Comments tab fireEvent.click(screen.getByText("Comments")); // Add a comment fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "New comment" } }); fireEvent.click(screen.getByText("Add Comment")); await waitFor(() => { expect(addSteeringComment).toHaveBeenCalledWith("FN-099", "New comment", undefined); expect(onTaskUpdated).toHaveBeenCalledWith(updatedTask); }); }); it("comment mutations still work when onTaskUpdated is not provided", async () => { const { addSteeringComment } = await import("../../api"); const addToast = vi.fn(); vi.mocked(addSteeringComment).mockResolvedValueOnce(makeTask({ comments: [{ id: "c1", text: "Hello", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }], })); render( , ); // Switch to Comments tab fireEvent.click(screen.getByText("Comments")); // Add a comment — should succeed without error even without onTaskUpdated fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "Hello" } }); fireEvent.click(screen.getByText("Add Comment")); await waitFor(() => { expect(addSteeringComment).toHaveBeenCalledWith("FN-099", "Hello", undefined); expect(addToast).toHaveBeenCalledWith("Comment added", "success"); }); }); }); describe("Workflow step ordering in edit mode (FN-836)", () => { it("sends ordered enabledWorkflowSteps when saving with reordered steps", async () => { const { updateTask, fetchWorkflowSteps } = await import("../../api"); const mockUpdate = vi.mocked(updateTask); mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task); vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([ { id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", mode: "prompt" as const, enabled: true, createdAt: "", updatedAt: "" }, { id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", mode: "prompt" as const, enabled: true, createdAt: "", updatedAt: "" }, ]); const { container } = render( , ); // Enter edit mode fireEvent.click(container.querySelector(".modal-edit-btn")!); // Wait for workflow steps to load and reorder controls to appear await waitFor(() => { expect(screen.getByTestId("workflow-step-order")).toBeTruthy(); }); // Move WS-002 up (swap with WS-001) fireEvent.click(screen.getByTestId("workflow-step-move-up-WS-002")); // Save fireEvent.click(screen.getByText("Save")); await waitFor(() => { expect(mockUpdate).toHaveBeenCalledWith("FN-001", expect.objectContaining({ enabledWorkflowSteps: ["WS-002", "WS-001"], }), undefined); }); }); }); describe("Workflow tab", () => { it.each<[string, Parameters[0]]>([ ["empty enabledWorkflowSteps", { enabledWorkflowSteps: [] }], ["undefined enabledWorkflowSteps", { enabledWorkflowSteps: undefined, workflowStepResults: undefined }], ["non-empty enabledWorkflowSteps", { enabledWorkflowSteps: ["WS-001"] }], ["previous workflow results", { enabledWorkflowSteps: [], workflowStepResults: [{ workflowStepId: "WS-001", workflowStepName: "QA Check", status: "passed" }] }], ])("Workflow tab is always rendered (%s)", (_label, taskOverrides) => { render( , ); expect(screen.getByText("Workflow")).toBeTruthy(); }); it("switches to Workflow tab and calls fetchWorkflowResults", async () => { const { fetchWorkflowResults } = await import("../../api"); const mockFetch = vi.mocked(fetchWorkflowResults); const mockResults: import("@fusion/core").WorkflowStepResult[] = [ { workflowStepId: "WS-001", workflowStepName: "QA Check", status: "passed", output: "All tests passed.", startedAt: "2026-04-04T10:00:00Z", completedAt: "2026-04-04T10:02:00Z", }, ]; mockFetch.mockResolvedValueOnce(mockResults); render( , ); fireEvent.click(screen.getByText("Workflow")); await waitFor(() => { expect(mockFetch).toHaveBeenCalledWith("FN-099", undefined); }); // Should render the workflow results after async tab load completes expect(await screen.findByText("QA Check", {}, { timeout: 15_000 })).toBeTruthy(); }); it("shows loading state when workflow results are being fetched", async () => { const { fetchWorkflowResults } = await import("../../api"); const mockFetch = vi.mocked(fetchWorkflowResults); // Never resolve to keep loading state mockFetch.mockResolvedValueOnce(new Promise(() => {}) as any); render( , ); fireEvent.click(screen.getByText("Workflow")); await waitFor(() => { expect(screen.getByTestId("workflow-results-loading")).toBeTruthy(); }); }); it("shows error toast when fetchWorkflowResults fails", async () => { const { fetchWorkflowResults } = await import("../../api"); const mockFetch = vi.mocked(fetchWorkflowResults); mockFetch.mockRejectedValueOnce(new Error("Server error")); const addToast = vi.fn(); render( , ); fireEvent.click(screen.getByText("Workflow")); await waitFor(() => { expect(addToast).toHaveBeenCalledWith( "Failed to load workflow results: Server error", "error", ); }); }); it("renders configured workflow steps state when results are empty", async () => { const { fetchWorkflowResults } = await import("../../api"); const mockFetch = vi.mocked(fetchWorkflowResults); mockFetch.mockResolvedValueOnce([]); render( , ); fireEvent.click(screen.getByText("Workflow")); await waitFor(() => { expect(screen.getByTestId("workflow-configured-steps")).toBeTruthy(); expect(screen.getByTestId("workflow-configured-step-WS-001")).toHaveTextContent("WS-001"); }); }); it("renders multiple workflow step results with status badges", async () => { const { fetchWorkflowResults } = await import("../../api"); const mockFetch = vi.mocked(fetchWorkflowResults); const mockResults: import("@fusion/core").WorkflowStepResult[] = [ { workflowStepId: "WS-001", workflowStepName: "QA Check", status: "passed", output: "All tests passed.", startedAt: "2026-04-04T10:00:00Z", completedAt: "2026-04-04T10:02:00Z", }, { workflowStepId: "WS-002", workflowStepName: "Security Audit", status: "failed", output: "Found 2 issues.", startedAt: "2026-04-04T10:02:05Z", completedAt: "2026-04-04T10:03:00Z", }, ]; mockFetch.mockResolvedValueOnce(mockResults); render( , ); fireEvent.click(screen.getByText("Workflow")); await waitFor(() => { expect(screen.getByText("QA Check")).toBeTruthy(); expect(screen.getByText("Security Audit")).toBeTruthy(); expect(screen.getByTestId("workflow-result-badge-WS-001")).toHaveTextContent("Passed"); expect(screen.getByTestId("workflow-result-badge-WS-002")).toHaveTextContent("Failed"); }); }); it("hides Definition content when Workflow tab is active", async () => { const { container } = render( , ); // Definition content visible initially expect(container.querySelector(".markdown-body")).toBeTruthy(); // Switch to Workflow tab fireEvent.click(screen.getByText("Workflow")); // Definition content should be hidden await waitFor(() => { expect(container.querySelector(".markdown-body")).toBeNull(); }); }); }); });