feat(FN-3265): wire Insights create-task to real task creation with mobile

Merges three features: wires the Insights view's "create task" action to real task creation (FN-3265) with tests, adds mobile-responsive collapse behavior in AgentsView when selecting live runs (FN-3271), and expands regression test coverage for worker budget handling (FN-3263). Changes span the das

Fusion-Task-Id: FN-3265
This commit is contained in:
Fusion
2026-05-03 10:02:17 -07:00
committed by gsxdsm
parent 987ced4e10
commit 7feb29502c
6 changed files with 224 additions and 19 deletions

View File

@@ -470,6 +470,24 @@ function AppInner() {
closeTaskDetail: modalManager.closeDetailTask,
});
const handleInsightTaskCreate = useCallback(
async ({ insightId, title, description }: { insightId: string; title: string; description: string }) => {
await createTask({
title,
description,
column: "triage",
source: {
sourceType: "dashboard_ui",
sourceMetadata: {
origin: "insights",
insightId,
},
},
});
},
[createTask],
);
// Task handlers
const {
handleBoardQuickCreate,
@@ -782,6 +800,7 @@ function AppInner() {
projectId={currentProject?.id}
addToast={addToast}
onClose={() => handleChangeTaskView("board")}
onCreateTask={handleInsightTaskCreate}
/>
</Suspense>
</PageErrorBoundary>

View File

@@ -30,7 +30,7 @@ interface InsightsViewProps {
projectId?: string;
addToast: (message: string, type?: ToastType) => void;
onClose?: () => void;
onCreateTask?: (title: string, description: string) => void;
onCreateTask?: (payload: { insightId: string; title: string; description: string }) => Promise<void>;
}
const CATEGORY_ICONS: Record<InsightCategory, React.ComponentType<{ size?: number; className?: string }>> = {
@@ -142,13 +142,25 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
try {
setStatusMessage(`Creating task from "${title}"...`);
setStatusType("info");
const taskData = await createTaskFromInsight(id);
if (taskData && onCreateTask) {
onCreateTask(taskData.title, taskData.description);
if (!onCreateTask) {
throw new Error("Task creation is unavailable in this view");
}
const taskData = await createTaskFromInsight(id);
if (!taskData) {
throw new Error("Failed to prepare task payload from insight");
}
await onCreateTask({
insightId: id,
title: taskData.title,
description: taskData.description,
});
setStatusMessage(`Task created from "${title}"`);
setStatusType("success");
addToast(`Task created: ${taskData?.title ?? title}`, "success");
addToast(`Task created: ${taskData.title}`, "success");
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to create task";
setStatusMessage(message);

View File

@@ -63,9 +63,11 @@ vi.mock("../../api", async (importOriginal) => {
});
});
const mockCreateTask = vi.fn();
const mockUseTasks = vi.fn(() => ({
tasks: [],
createTask: vi.fn(),
createTask: mockCreateTask,
moveTask: vi.fn(),
deleteTask: vi.fn(),
mergeTask: vi.fn(),
@@ -83,6 +85,27 @@ vi.mock("../../hooks/useTasks", () => ({
}));
// Mock useRemoteNodeData
const mockUseInsights = vi.fn(() => ({
sections: [],
loading: false,
error: null,
latestRun: null,
isRunInFlight: false,
runError: null,
refresh: vi.fn(),
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: vi.fn(),
dismissStates: new Map(),
createTaskStates: new Map(),
totalCount: 0,
dismissedCount: 0,
}));
vi.mock("../../hooks/useInsights", () => ({
useInsights: (..._args: unknown[]) => mockUseInsights(),
}));
vi.mock("../../hooks/useRemoteNodeData", () => ({
useRemoteNodeData: vi.fn(() => ({
projects: [],
@@ -464,10 +487,11 @@ beforeEach(() => {
vi.clearAllMocks();
mockSubscribeSse.mockReset();
mockSubscribeSse.mockReturnValue(vi.fn());
mockCreateTask.mockReset();
mockUseTasks.mockReset();
mockUseTasks.mockImplementation(() => ({
tasks: [],
createTask: vi.fn(),
createTask: mockCreateTask,
moveTask: vi.fn(),
deleteTask: vi.fn(),
mergeTask: vi.fn(),
@@ -516,6 +540,23 @@ beforeEach(() => {
mockGetSkippedSteps.mockReturnValue([]);
mockGetStepData.mockReset();
mockGetStepData.mockReturnValue(null);
mockUseInsights.mockReset();
mockUseInsights.mockImplementation(() => ({
sections: [],
loading: false,
error: null,
latestRun: null,
isRunInFlight: false,
runError: null,
refresh: vi.fn(),
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: vi.fn(),
dismissStates: new Map(),
createTaskStates: new Map(),
totalCount: 0,
dismissedCount: 0,
}));
});
describe("App backend-unreachable first-run flow", () => {
@@ -1707,6 +1748,80 @@ describe("App view switching", () => {
expect(document.querySelector(".agents-view")).toBeNull();
});
it("creates a real triage task from insights using dashboard task creation flow", async () => {
mockUseInsights.mockImplementation(() => ({
sections: [
{
category: "features",
label: "Features",
items: [
{
id: "INS-1",
projectId: DEFAULT_PROJECT_ID,
title: "Insight title",
content: "Insight content",
category: "features",
status: "generated",
fingerprint: "fp-ins-1",
provenance: { trigger: "manual" },
lastRunId: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
],
isLoading: false,
error: null,
},
],
loading: false,
error: null,
latestRun: null,
isRunInFlight: false,
runError: null,
refresh: vi.fn(),
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: vi.fn().mockResolvedValue({
title: "Task from insight",
description: "Use this insight as a task description",
}),
dismissStates: new Map(),
createTaskStates: new Map(),
totalCount: 1,
dismissedCount: 0,
}));
render(<App />);
await waitFor(() => {
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeTruthy();
});
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
fireEvent.click(screen.getByTestId("view-overflow-insights"));
await waitFor(() => {
expect(screen.getByTestId("create-task-INS-1")).toBeTruthy();
});
fireEvent.click(screen.getByTestId("create-task-INS-1"));
await waitFor(() => {
expect(mockCreateTask).toHaveBeenCalledWith({
title: "Task from insight",
description: "Use this insight as a task description",
column: "triage",
source: {
sourceType: "dashboard_ui",
sourceMetadata: {
origin: "insights",
insightId: "INS-1",
},
},
});
});
});
it("persists insights view preference to localStorage", async () => {
localStorage.removeItem(taskViewStorageKey());

View File

@@ -68,22 +68,15 @@ vi.mock("lucide-react", () => ({
),
}));
// Mock the createTask API
vi.mock("../../api", () => ({
createTask: vi.fn(),
}));
import { useInsights } from "../../hooks/useInsights";
import { createTask } from "../../api";
const mockUseInsights = vi.mocked(useInsights);
const mockCreateTask = vi.mocked(createTask);
describe("InsightsView", () => {
const defaultProps = {
addToast: vi.fn(),
onClose: vi.fn(),
onCreateTask: vi.fn(),
onCreateTask: vi.fn().mockResolvedValue(undefined),
};
const mockSections = [
@@ -722,6 +715,7 @@ describe("InsightsView", () => {
it("should trigger create task on insight action click", async () => {
const createTaskFn = vi.fn().mockResolvedValue({ title: "New Task", description: "Task description" });
const onCreateTask = vi.fn().mockResolvedValue(undefined);
const sectionsWithInsight = [
{
category: "features" as const,
@@ -764,7 +758,7 @@ describe("InsightsView", () => {
dismissedCount: 0,
});
render(<InsightsView {...defaultProps} />);
render(<InsightsView {...defaultProps} onCreateTask={onCreateTask} />);
const createButton = screen.getByTestId("create-task-INS-1");
await act(async () => {
@@ -772,7 +766,71 @@ describe("InsightsView", () => {
});
expect(createTaskFn).toHaveBeenCalledWith("INS-1");
expect(defaultProps.onCreateTask).toHaveBeenCalledWith("New Task", "Task description");
expect(onCreateTask).toHaveBeenCalledWith({
insightId: "INS-1",
title: "New Task",
description: "Task description",
});
await waitFor(() => {
expect(defaultProps.addToast).toHaveBeenCalledWith("Task created: New Task", "success");
});
});
it("shows error and skips success toast when app-level task creation fails", async () => {
const createTaskFn = vi.fn().mockResolvedValue({ title: "New Task", description: "Task description" });
const onCreateTask = vi.fn().mockRejectedValue(new Error("Task creation is unavailable in this view"));
const sectionsWithInsight = [
{
category: "features" as const,
label: "Features",
items: [
{
id: "INS-1",
projectId: "test",
title: "Test Insight",
content: "Content",
category: "features" as const,
status: "generated" as const,
fingerprint: "fp1",
provenance: { trigger: "manual" as const },
lastRunId: null,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
],
isLoading: false,
error: null,
},
...mockSections.slice(1),
];
mockUseInsights.mockReturnValue({
sections: sectionsWithInsight,
loading: false,
error: null,
latestRun: null,
isRunInFlight: false,
runError: null,
refresh: vi.fn(),
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: createTaskFn,
dismissStates: new Map(),
createTaskStates: new Map(),
totalCount: 1,
dismissedCount: 0,
});
render(<InsightsView {...defaultProps} onCreateTask={onCreateTask} />);
await act(async () => {
fireEvent.click(screen.getByTestId("create-task-INS-1"));
});
await waitFor(() => {
expect(defaultProps.addToast).toHaveBeenCalledWith("Task creation is unavailable in this view", "error");
});
expect(defaultProps.addToast).not.toHaveBeenCalledWith("Task created: New Task", "success");
});
it("should show toast on run success", async () => {

View File

@@ -2,12 +2,12 @@
* Insights REST API Routes
*
* Provides CRUD endpoints for project insights and insight generation runs.
* Also includes action endpoints for running insight generation and creating tasks from insights.
* Also includes action endpoints for running insight generation and preparing task payload drafts from insights.
*
* Endpoints:
* - Insights: GET /, GET /:id, PATCH /:id, DELETE /:id
* - Runs: GET /runs, POST /runs, GET /runs/:id
* - Actions: POST /run (trigger manual run), POST /:id/dismiss, POST /:id/create-task
* - Actions: POST /run (trigger manual run), POST /:id/dismiss, POST /:id/create-task (returns suggested task title/description draft)
*/
import { Router } from "express";