diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx index 63a2a30e39..bd38542635 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx @@ -17,6 +17,7 @@ import { mockConfirm, mockUsePluginUiSlots, expectBaseRule, + expectSingleStatsRuntimeStatus, readDashboardStylesSource, setupTaskDetailModalHooks, } from "./TaskDetailModal.test-helpers"; @@ -83,6 +84,11 @@ function getRuleBlockFromSelectorList(css: string, selector: string): string { return ruleEnd === -1 ? "" : css.slice(ruleStart + 1, ruleEnd); } +/* +FNXC:TaskDetailStatsAssertions 2026-08-09-16:50: +FN-8906 requires Stats-tab runtime-status assertions to be scoped to the named Stats region because +the modal header lifecycle badge intentionally renders the same raw status string. +*/ describe("TaskDetailModal", () => { describe("source issue metadata", () => { beforeEach(() => { @@ -2440,7 +2446,7 @@ describe("TaskDetailModal", () => { expect(screen.getByText("Execution Details")).toBeInTheDocument(); expect(screen.getByText("Loading token statistics…")).toBeDefined(); expect(screen.getAllByText("Fast").length).toBeGreaterThan(0); - expect(screen.getByText("executing")).toBeInTheDocument(); + expectSingleStatsRuntimeStatus("executing"); }); it("shows spec content after fetchTaskDetail resolves", async () => { @@ -2529,7 +2535,7 @@ describe("TaskDetailModal", () => { expect(screen.getByText("Execution mode")).toBeInTheDocument(); expect(screen.getByText("Runtime status")).toBeInTheDocument(); expect(screen.getAllByText("Fast").length).toBeGreaterThan(0); - expect(screen.getByText("executing")).toBeInTheDocument(); + expectSingleStatsRuntimeStatus("executing"); expect(screen.getByText((1200).toLocaleString())).toBeInTheDocument(); expect(screen.getByText((450).toLocaleString())).toBeInTheDocument(); expect(screen.getByText((210).toLocaleString())).toBeInTheDocument(); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx index cec943e2ee..aa259c0b91 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx @@ -12,7 +12,7 @@ query ambiguous once the trigger stopped being a mobile-only affordance. import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { render, screen, fireEvent, act, waitFor, cleanup, within } from "@testing-library/react"; +import { render, screen, fireEvent, act, waitFor, cleanup } from "@testing-library/react"; // FNXC:Markdown 2026-06-23-03:30: Mock the heavy `mermaid` library so the shared // markdown pipeline's MermaidDiagram resolves without loading the real renderer. @@ -35,6 +35,7 @@ import { mockConfirm, mockUsePluginUiSlots, expectBaseRule, + expectSingleStatsRuntimeStatus, readDashboardStylesSource, resetTaskDetailFetchMock, setupTaskDetailModalHooks, @@ -45,22 +46,6 @@ import * as dashboardApi from "../../api"; import { FileBrowserProvider } from "../../context/FileBrowserContext"; import type { Task } from "@fusion/core"; -/* -FNXC:TaskDetailOptimisticOpening 2026-08-05-07:39: -A running task deliberately exposes its raw runtime status in two ownership regions: the modal -header's lifecycle badge and the Stats panel's Runtime status row. Optimistic-opening assertions -must scope the Stats claim to its named semantic region, then assert one row there and the expected -two owned values overall; this catches a duplicated Stats panel without treating legitimate header -context as a production rendering defect. -*/ -function expectSingleStatsRuntimeStatus(status: string) { - const statsPanel = screen.getByRole("region", { name: "Task execution statistics" }); - expect(within(statsPanel).getByText(status)).toBeInTheDocument(); - expect(within(statsPanel).getAllByText(status)).toHaveLength(1); - expect(screen.getByTestId("task-detail-status-badge")).toHaveTextContent(status); - expect(screen.getAllByText(status)).toHaveLength(2); -} - setupTaskDetailModalHooks(); describe("TaskDetailModal", () => { diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts b/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts index 73e94009d4..11ca300a16 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.test-helpers.ts @@ -3,7 +3,7 @@ import { loadAllAppCss } from "../../test/cssFixture"; import React from "react"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; -import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; +import { render, screen, fireEvent, act, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal"; import type { TaskDetail, Column, MergeResult, Task } from "@fusion/core"; @@ -16,6 +16,27 @@ const taskDetailSseSubscriptions = vi.hoisted(() => [] as Array<{ export { taskDetailSseSubscriptions }; +/* +FNXC:TaskDetailOptimisticOpening 2026-08-05-07:39: +A running task deliberately exposes its raw runtime status in two ownership regions: the modal +header's lifecycle badge and the Stats panel's Runtime status row. Optimistic-opening assertions +must scope the Stats claim to its named semantic region, then assert one row there and the expected +two owned values overall; this catches a duplicated Stats panel without treating legitimate header +context as a production rendering defect. + +FNXC:TaskDetailStatsAssertions 2026-08-09-16:50: +FN-8906 requires every TaskDetailModal test to use this shared helper: a non-empty raw runtime +status is owned by the header lifecycle badge and the Stats Runtime status row, so an unscoped +getByText(status) after opening Stats is ambiguous. +*/ +export function expectSingleStatsRuntimeStatus(status: string): void { + const statsPanel = screen.getByRole("region", { name: "Task execution statistics" }); + expect(within(statsPanel).getByText(status)).toBeInTheDocument(); + expect(within(statsPanel).getAllByText(status)).toHaveLength(1); + expect(screen.getByTestId("task-detail-status-badge")).toHaveTextContent(status); + expect(screen.getAllByText(status)).toHaveLength(2); +} + vi.mock("../../sse-bus", () => ({ subscribeSse: vi.fn((url: string, options: { events?: Record void> }) => { taskDetailSseSubscriptions.push({ url, options });