FN-8646: fix dashboard task enrichment validation
Repair deterministic dashboard test regressions and reject whitespace-only enrichment targets. - Normalize task IDs before enabling and submitting research enrichment. - Update research, task-detail CSS, and chat visibility test fixtures. - Add a patch changeset for the dashboard behavior fix. Files changed: .changeset/fn-8646-dashboard-test-repairs.md | 7 +++++++ .../app/components/ResearchTaskActionModal.tsx | 12 ++++++++++-- .../app/components/__tests__/ResearchView.test.tsx | 20 ++++++++++++++++---- .../components/__tests__/TaskDetailModal.css.test.ts | 11 +++++++++-- .../dashboard/app/hooks/__tests__/useChat.test.ts | 9 ++++++++- 5 files changed, 50 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-8646 Fusion-Task-Lineage: 033f4fa4-b096-4129-b82b-8fa7078dd6b2 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8646-dashboard-test-repairs.md
Normal file
7
.changeset/fn-8646-dashboard-test-repairs.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Prevent blank task IDs from enabling Research finding enrichment.
|
||||||
|
category: fix
|
||||||
|
dev: Trims the target task ID before enabling and submitting the enrich action.
|
||||||
@@ -133,6 +133,14 @@ export function ResearchTaskActionModal({ open, mode, run, finding, projectId, o
|
|||||||
};
|
};
|
||||||
}, [open, mode, projectId, isArchivedColumn]);
|
}, [open, mode, projectId, isArchivedColumn]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ResearchEnrich 2026-08-01-02:35:
|
||||||
|
Enriching requires a real task identifier. Whitespace made the action appear available and sent
|
||||||
|
a non-actionable identifier downstream, so validate the normalized value and submit that same
|
||||||
|
value rather than preserving an input-only representation.
|
||||||
|
*/
|
||||||
|
const normalizedTaskId = taskId.trim();
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -195,11 +203,11 @@ export function ResearchTaskActionModal({ open, mode, run, finding, projectId, o
|
|||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={saving || (mode === "enrich" && !taskId)}
|
disabled={saving || (mode === "enrich" && !normalizedTaskId)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
void onConfirm({
|
void onConfirm({
|
||||||
taskId: mode === "enrich" ? taskId : undefined,
|
taskId: mode === "enrich" ? normalizedTaskId : undefined,
|
||||||
title: mode === "create" ? title.trim() : undefined,
|
title: mode === "create" ? title.trim() : undefined,
|
||||||
description: mode === "create" ? description.trim() : undefined,
|
description: mode === "create" ? description.trim() : undefined,
|
||||||
priority: mode === "create" ? priority : undefined,
|
priority: mode === "create" ? priority : undefined,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
|
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
|
||||||
import { Header } from "../Header";
|
import { Header } from "../Header";
|
||||||
import { ResearchView } from "../ResearchView";
|
import { ResearchView } from "../ResearchView";
|
||||||
@@ -291,7 +292,14 @@ describe("ResearchView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ResearchEnrich 2026-08-01-02:35:
|
||||||
|
The enrich control is a real editable combobox, not a broken product path. Drive it with
|
||||||
|
per-character user input so this regression test validates the same state updates as a user;
|
||||||
|
synthetic change events previously masked that interaction contract.
|
||||||
|
*/
|
||||||
it("triggers enrich-task action from finding modal", async () => {
|
it("triggers enrich-task action from finding modal", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
const attachRunToTask = vi.fn().mockResolvedValue({});
|
const attachRunToTask = vi.fn().mockResolvedValue({});
|
||||||
mockUseResearch.mockReturnValue({
|
mockUseResearch.mockReturnValue({
|
||||||
...baseHookValue,
|
...baseHookValue,
|
||||||
@@ -314,8 +322,8 @@ describe("ResearchView", () => {
|
|||||||
|
|
||||||
const enrichDialog = await screen.findByRole("dialog");
|
const enrichDialog = await screen.findByRole("dialog");
|
||||||
const targetInput = within(enrichDialog).getByRole("combobox", { name: "Target task" });
|
const targetInput = within(enrichDialog).getByRole("combobox", { name: "Target task" });
|
||||||
fireEvent.change(targetInput, { target: { value: "FN-1" } });
|
await user.type(targetInput, "FN-1");
|
||||||
fireEvent.click(within(enrichDialog).getByRole("button", { name: "Enrich Task" }));
|
await user.click(within(enrichDialog).getByRole("button", { name: "Enrich Task" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(attachRunToTask).toHaveBeenCalledWith("RR-1", "FN-1", "finding-1", false);
|
expect(attachRunToTask).toHaveBeenCalledWith("RR-1", "FN-1", "finding-1", false);
|
||||||
@@ -533,7 +541,8 @@ describe("ResearchView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps enrich action disabled until a task id is provided", async () => {
|
it("keeps enrich action disabled for empty and whitespace-only task ids, then enables for a typed id", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockUseResearch.mockReturnValue({
|
mockUseResearch.mockReturnValue({
|
||||||
...baseHookValue,
|
...baseHookValue,
|
||||||
runs: [{ id: "RR-1", title: "t", query: "q", status: "completed" }],
|
runs: [{ id: "RR-1", title: "t", query: "q", status: "completed" }],
|
||||||
@@ -557,7 +566,10 @@ describe("ResearchView", () => {
|
|||||||
expect(enrichButton).toBeDisabled();
|
expect(enrichButton).toBeDisabled();
|
||||||
|
|
||||||
const targetInput = within(dialog).getByRole("combobox", { name: "Target task" });
|
const targetInput = within(dialog).getByRole("combobox", { name: "Target task" });
|
||||||
fireEvent.change(targetInput, { target: { value: "FN-1" } });
|
await user.type(targetInput, " ");
|
||||||
|
expect(enrichButton).toBeDisabled();
|
||||||
|
await user.clear(targetInput);
|
||||||
|
await user.type(targetInput, "FN-1");
|
||||||
await waitFor(() => expect(enrichButton).not.toBeDisabled());
|
await waitFor(() => expect(enrichButton).not.toBeDisabled());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,20 @@ describe("TaskDetailModal CSS contract", () => {
|
|||||||
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
|
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskDetailPadding 2026-08-01-03:20:
|
||||||
|
FN-8634 moved the canonical tab inset from the scrolling `.detail-body` to its
|
||||||
|
`.detail-body-content` child so scrollbar tracks remain outside the content inset.
|
||||||
|
FN-7408 must assert that visual contract at the owning element while chat and
|
||||||
|
planner variants remain forbidden from introducing competing outer padding.
|
||||||
|
*/
|
||||||
it("FN-7408 keeps task-detail tab body padding canonical across Activity, planner Chat, and Plan surfaces", async () => {
|
it("FN-7408 keeps task-detail tab body padding canonical across Activity, planner Chat, and Plan surfaces", async () => {
|
||||||
const css = await loadAllAppCssBaseOnly();
|
const css = await loadAllAppCssBaseOnly();
|
||||||
const detailBodyBlock = getCssRuleBlock(css, ".detail-body");
|
const detailBodyContentBlock = getCssRuleBlock(css, ".detail-body-content");
|
||||||
const rawBodyBlock = getCssRuleBlock(css, ".detail-body--agent-log");
|
const rawBodyBlock = getCssRuleBlock(css, ".detail-body--agent-log");
|
||||||
const planBlock = getCssRuleBlock(css, ".detail-section--plan-prompt");
|
const planBlock = getCssRuleBlock(css, ".detail-section--plan-prompt");
|
||||||
|
|
||||||
expect(detailBodyBlock).toContain("padding: calc(var(--space-lg) + var(--space-xs));");
|
expect(detailBodyContentBlock).toContain("padding: calc(var(--space-lg) + var(--space-xs));");
|
||||||
expectNoOuterPaddingOverride(css, ".detail-body--chat");
|
expectNoOuterPaddingOverride(css, ".detail-body--chat");
|
||||||
expectNoOuterPaddingOverride(css, ".detail-body--planner-chat");
|
expectNoOuterPaddingOverride(css, ".detail-body--planner-chat");
|
||||||
expectNoOuterPaddingOverride(css, ".task-detail-content--chat-expanded .detail-body--chat");
|
expectNoOuterPaddingOverride(css, ".task-detail-content--chat-expanded .detail-body--chat");
|
||||||
|
|||||||
@@ -1827,7 +1827,14 @@ describe("useChat", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
|
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
|
||||||
mockFetchChatSession.mockResolvedValueOnce({ session: generatingSession });
|
/*
|
||||||
|
FNXC:ChatVisibilityResume 2026-08-01-02:35:
|
||||||
|
The visible-edge reconciliation now performs its own authoritative session read after
|
||||||
|
selection. Keep both reads generating: a one-shot mock made the second read fall back to an
|
||||||
|
unrelated idle fixture, which falsely modeled a completed user-visible generation and hid the
|
||||||
|
prior thread this test is meant to protect.
|
||||||
|
*/
|
||||||
|
mockFetchChatSession.mockResolvedValue({ session: generatingSession });
|
||||||
mockFetchChatMessages
|
mockFetchChatMessages
|
||||||
.mockResolvedValueOnce({ messages: [] })
|
.mockResolvedValueOnce({ messages: [] })
|
||||||
.mockResolvedValueOnce({ messages: priorThreadNewestFirst });
|
.mockResolvedValueOnce({ messages: priorThreadNewestFirst });
|
||||||
|
|||||||
Reference in New Issue
Block a user