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]);
|
||||
|
||||
/*
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -195,11 +203,11 @@ export function ResearchTaskActionModal({ open, mode, run, finding, projectId, o
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
disabled={saving || (mode === "enrich" && !taskId)}
|
||||
disabled={saving || (mode === "enrich" && !normalizedTaskId)}
|
||||
onClick={() => {
|
||||
setSaving(true);
|
||||
void onConfirm({
|
||||
taskId: mode === "enrich" ? taskId : undefined,
|
||||
taskId: mode === "enrich" ? normalizedTaskId : undefined,
|
||||
title: mode === "create" ? title.trim() : undefined,
|
||||
description: mode === "create" ? description.trim() : undefined,
|
||||
priority: mode === "create" ? priority : undefined,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
|
||||
import { Header } from "../Header";
|
||||
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 () => {
|
||||
const user = userEvent.setup();
|
||||
const attachRunToTask = vi.fn().mockResolvedValue({});
|
||||
mockUseResearch.mockReturnValue({
|
||||
...baseHookValue,
|
||||
@@ -314,8 +322,8 @@ describe("ResearchView", () => {
|
||||
|
||||
const enrichDialog = await screen.findByRole("dialog");
|
||||
const targetInput = within(enrichDialog).getByRole("combobox", { name: "Target task" });
|
||||
fireEvent.change(targetInput, { target: { value: "FN-1" } });
|
||||
fireEvent.click(within(enrichDialog).getByRole("button", { name: "Enrich Task" }));
|
||||
await user.type(targetInput, "FN-1");
|
||||
await user.click(within(enrichDialog).getByRole("button", { name: "Enrich Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
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({
|
||||
...baseHookValue,
|
||||
runs: [{ id: "RR-1", title: "t", query: "q", status: "completed" }],
|
||||
@@ -557,7 +566,10 @@ describe("ResearchView", () => {
|
||||
expect(enrichButton).toBeDisabled();
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
|
||||
@@ -28,13 +28,20 @@ describe("TaskDetailModal CSS contract", () => {
|
||||
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 () => {
|
||||
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 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--planner-chat");
|
||||
expectNoOuterPaddingOverride(css, ".task-detail-content--chat-expanded .detail-body--chat");
|
||||
|
||||
@@ -1827,7 +1827,14 @@ describe("useChat", () => {
|
||||
},
|
||||
};
|
||||
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
|
||||
.mockResolvedValueOnce({ messages: [] })
|
||||
.mockResolvedValueOnce({ messages: priorThreadNewestFirst });
|
||||
|
||||
Reference in New Issue
Block a user