diff --git a/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts b/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts
index 7632bea78b..3c688a47ba 100644
--- a/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts
+++ b/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts
@@ -59,7 +59,14 @@ function findMobileBlockContaining(needle: string): string {
describe("mission + planning modal mobile CSS", () => {
it("MissionManager: mission icon button touch targets are 36px", () => {
const missionBlock = findMobileBlockContaining(".mission-manager-overlay");
- expect(missionBlock).toMatch(/\.mission-list__item-actions \.mission-icon-btn,[\s\S]*?\.mission-feature__actions \.mission-icon-btn\s*\{[\s\S]*?min-width:\s*36px;[\s\S]*?min-height:\s*36px;/s);
+ /*
+ FNXC:Missions 2026-08-15-22:40:
+ FN-8947 (b82f1a41f3) appended `.mission-fix-feature__actions .mission-icon-btn` to this
+ touch-target selector list, so `.mission-feature__actions .mission-icon-btn` is no longer
+ the final selector before `{`. Match the selectors up to the fix-feature entry that now
+ closes the list, keeping the same 36px touch-target invariant.
+ */
+ expect(missionBlock).toMatch(/\.mission-list__item-actions \.mission-icon-btn,[\s\S]*?\.mission-feature__actions \.mission-icon-btn,[\s\S]*?\.mission-fix-feature__actions \.mission-icon-btn\s*\{[\s\S]*?min-width:\s*36px;[\s\S]*?min-height:\s*36px;/s);
});
it("MissionManager: body prevents horizontal overflow", () => {
diff --git a/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx b/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx
index 2a909e07ff..8e85dd51a2 100644
--- a/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx
+++ b/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx
@@ -84,7 +84,12 @@ describe("Advanced Settings", () => {
expect((screen.getByLabelText("Name") as HTMLInputElement).value).toBe("Interviewed Agent");
expect((screen.getByLabelText("Title") as HTMLInputElement).value).toBe("Draft Title");
expect((screen.getByLabelText("Icon") as HTMLInputElement).value).toBe("🧠");
- expect((screen.getByLabelText("Role") as HTMLSelectElement).value).toBe("reviewer");
+ /*
+ FNXC:Agents 2026-08-15-22:45:
+ FN-8764 (eaadd153b1) split agent roles into a primary role plus additional workflow
+ roles, renaming this select's label from "Role" to "Primary role".
+ */
+ expect((screen.getByLabelText("Primary role") as HTMLSelectElement).value).toBe("reviewer");
});
expect(mockUpdateAgent).not.toHaveBeenCalled();
@@ -94,7 +99,9 @@ describe("Advanced Settings", () => {
"agent-001",
expect.objectContaining({
name: "Interviewed Agent",
- role: "reviewer",
+ // FNXC:Agents 2026-08-15-22:45: FN-8764 (eaadd153b1) saves the role list as `roles`
+ // (primary first) instead of a scalar `role`.
+ roles: ["reviewer"],
title: "Draft Title",
reportsTo: "agent-002",
runtimeConfig: expect.objectContaining({ model: "openai/gpt-4o" }),
diff --git a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx
index d9a29762b8..aec6c358c6 100644
--- a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx
+++ b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx
@@ -288,19 +288,32 @@ describe("MobileNavBar", () => {
expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-spacing-check-wide")).toBeDefined();
});
- it("keeps Todos in the mobile More sheet and routes to the todos view", () => {
+ /*
+ FNXC:Navigation 2026-08-15-22:15:
+ FN-8762 (5b2b31d2c9) extracted Todo Lists into the bundled `fusion-plugin-todos`
+ plugin: the host `todos` More-sheet item and `experimentalFeatures.todoView` gate
+ are gone, and Todos now surfaces via `pluginDashboardViews` with overflow placement.
+ These tests keep the same coverage (More-sheet only, routing, More-tab active) in
+ the plugin form.
+ */
+ const todosPluginView = {
+ pluginId: "fusion-plugin-todos",
+ view: { viewId: "todos", label: "Todos", componentPath: "./dashboard-view", icon: "CheckSquare", placement: "overflow" as const, order: 70 },
+ };
+
+ it("keeps Todos in the mobile More sheet and routes to the todos plugin view", () => {
const props = createDefaultProps();
render(
,
);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
- fireEvent.click(screen.getByTestId("mobile-more-item-todos"));
+ fireEvent.click(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-todos-todos"));
- expect(props.onChangeView).toHaveBeenCalledWith("todos");
+ expect(props.onChangeView).toHaveBeenCalledWith("plugin:fusion-plugin-todos:todos");
});
it("Mailbox is a primary tab and is not duplicated in the More sheet", () => {
@@ -316,22 +329,22 @@ describe("MobileNavBar", () => {
render(
,
);
- expect(screen.queryByTestId("mobile-nav-tab-todos")).toBeNull();
+ expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-todos-todos")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
- expect(screen.getByTestId("mobile-more-item-todos")).toBeInTheDocument();
+ expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-todos-todos")).toBeInTheDocument();
});
- it("marks the mobile More tab active for the todos view", () => {
+ it("marks the mobile More tab active for the todos plugin view", () => {
render(
,
);
diff --git a/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx b/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx
index 098ab5316c..2a66ef5628 100644
--- a/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx
@@ -209,8 +209,15 @@ describe("ScheduledTasksModal", () => {
it("keeps mobile Automations full-screen and hides resize handles by CSS contract", () => {
const source = readFileSync(resolve(__dirname, "../ScriptsModal.css"), "utf8");
+ /*
+ FNXC:GitManager 2026-08-15-22:35:
+ FN-8702 (1e67e87321) moved ScriptsModal.css's standalone phone-sheet blocks from
+ `max-width: 768px` to `max-width: 767.98px` so the takeover applies only BELOW the
+ 768px boundary (768px-wide tablets keep desktop geometry). The Automations
+ full-screen contract rides in that same block, so match the phone boundary.
+ */
const mobileBlock = source
- .match(/@media \(max-width: 768px\)\s*\{[\s\S]*?\n\}/g)
+ .match(/@media \(max-width: 767\.98px\)\s*\{[\s\S]*?\n\}/g)
?.find((block) => block.includes(".floating-window--automation")) ?? "";
expect(mobileBlock).toContain(".floating-window--automation");
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx
index 156fffebae..3d96cc2e6e 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx
@@ -102,6 +102,22 @@ function makeTask(overrides: Partial = {}): Task {
const noop = () => {};
+/*
+FNXC:PlannerOversight 2026-08-15-22:10:
+FN-8826 (1f5c44ac71) intentionally restored an always-on WIP lifecycle badge for
+empty-status cards in WIP lanes, so `card-header-badges` now legitimately mounts
+on the stale-snapshot fixtures below. The oversight invariant these tests guard is
+narrower: the header wrapper must never contain an overseer eye element while the
+effective oversight level is off/unresolved/inherited-default. Assert that instead
+of wrapper absence.
+*/
+function expectHeaderBadgesFreeOfOverseerEye() {
+ const header = screen.queryByTestId("card-header-badges");
+ if (header) {
+ expect(header.querySelector(".card-planner-overseer-state")).toBeNull();
+ }
+}
+
function renderCard(
overrides: Partial = {},
cardProps: {
@@ -272,14 +288,14 @@ describe("TaskCard workflow-effective oversight level (FN-7516 code-review fix)"
// unresolved on first render. The stale runtime snapshot must not leak an
// Eye badge or header wrapper before the configured-off response arrives.
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
await waitFor(() => {
expect(fetchWorkflowSettingValues).toHaveBeenCalledWith(`wf-stale-snapshot-off-${column}`, undefined);
});
await waitFor(() => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
});
});
@@ -425,13 +441,13 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
renderCard(staleSnapshot(column), { planningWorkflowId: " selected-workflow-off ", projectId: "project-8251" });
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
await waitFor(() => {
expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("selected-workflow-off", "project-8251");
});
await waitFor(() => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
});
});
@@ -447,13 +463,13 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
// The selected workflow's declaration default is not a meaningful oversight
// configuration, so a stale runtime snapshot cannot create an eye or shell.
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
await waitFor(() => {
expect(fetchWorkflowSettingValues).toHaveBeenCalledWith(`selected-inherited-default-${column}`, "project-8255");
});
await waitFor(() => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
});
});
@@ -475,12 +491,12 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
});
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
await waitFor(() => {
expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("aggregate-inherited-default", undefined);
});
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull();
});
@@ -495,7 +511,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalled());
await waitFor(() => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
expect(screen.queryByLabelText(/overseer/i)).toBeNull();
});
});
@@ -503,7 +519,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
it("fails closed without workflow identity but keeps a valid active task override authoritative", () => {
const { rerender } = renderCard(staleSnapshot("in-progress"));
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
rerender(
{
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledTimes(2));
await waitFor(() => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull();
});
});
@@ -596,12 +612,12 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
act(() => notifyWorkflowSettingValuesUpdated(workflowId, "project-cache-fix"));
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull();
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledTimes(2));
await waitFor(() => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
});
});
@@ -626,7 +642,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
resolveActive?.({ stored: { plannerOversightLevel: "steer" }, effective: { plannerOversightLevel: "steer" }, orphaned: [] });
});
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
});
it("hides the eye synchronously when an active selected workflow changes to an unresolved off workflow", async () => {
@@ -661,7 +677,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("mobile-inherited-default", undefined));
expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull();
- expect(screen.queryByTestId("card-header-badges")).toBeNull();
+ expectHeaderBadgesFreeOfOverseerEye();
expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull();
});
});
diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx
index 6d44d10aae..ec7eaa19c7 100644
--- a/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx
@@ -84,7 +84,13 @@ describe("TaskDetailModal GitLab tracking", () => {
rerender(
{
it("keeps Definition prompt refresh request-free while hidden and resumes one planning chain on reveal", async () => {
vi.useFakeTimers();
- const mockDetail = vi.mocked(dashboardApi.fetchTaskDetail);
- mockDetail.mockReset();
- mockDetail.mockResolvedValue(makeTask({ id: "FN-9001", column: "triage", status: "planning", prompt: "# Fresh plan" }));
+ /*
+ FNXC:TaskDetailPlan 2026-08-15-22:30:
+ FN-8797 (35810666be) made the Definition tab's 5s planning refresh prompt-only via
+ `fetchTaskPrompt` — the old `fetchTaskDetail` chain rolled queued cards back to Todo,
+ so the periodic read no longer touches the detail endpoint. The hidden/reveal gating
+ invariant is unchanged; only the polled request boundary moved.
+ */
+ const mockPrompt = vi.mocked(dashboardApi.fetchTaskPrompt);
+ mockPrompt.mockReset();
+ mockPrompt.mockResolvedValue({ id: "FN-9001", prompt: "# Fresh plan" });
const { rerenderWithActive } = renderContent(false, makeTask({ id: "FN-9001", column: "triage", status: "planning" }));
await act(async () => { await vi.advanceTimersByTimeAsync(16_000); });
- expect(mockDetail).not.toHaveBeenCalled();
+ expect(mockPrompt).not.toHaveBeenCalled();
rerenderWithActive(true);
await act(async () => { await vi.advanceTimersByTimeAsync(0); });
- expect(mockDetail).toHaveBeenCalledTimes(1);
+ expect(mockPrompt).toHaveBeenCalledTimes(1);
await act(async () => { await vi.advanceTimersByTimeAsync(5_000); });
- expect(mockDetail).toHaveBeenCalledTimes(2);
+ expect(mockPrompt).toHaveBeenCalledTimes(2);
rerenderWithActive(false);
await act(async () => { await vi.advanceTimersByTimeAsync(16_000); });
- expect(mockDetail).toHaveBeenCalledTimes(2);
+ expect(mockPrompt).toHaveBeenCalledTimes(2);
});
it("suspends the 5s verification polling while hidden and resumes it on reveal", async () => {
diff --git a/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx b/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx
index 8e2806b8dd..421c1b3e76 100644
--- a/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx
+++ b/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx
@@ -104,6 +104,23 @@ function mainContentProps(overrides: Partial = {}): MainConten
handleResumeProject: vi.fn(),
handleRemoveProject: vi.fn(),
nodes: [],
+ /*
+ FNXC:TodoPluginEnablement 2026-08-15-22:20:
+ FN-8762 (5b2b31d2c9) gated plugin task views on the project-scoped
+ `pluginDashboardViews` roster (MainContent.isEnabledPluginTaskView), so this
+ harness must enroll the plugin views it renders or MainContent treats them as
+ disabled. The same commit removed the host `todosEnabled`/`TodoView` props.
+ */
+ pluginDashboardViews: [
+ {
+ pluginId: "fusion-plugin-dependency-graph",
+ view: { viewId: "graph", label: "Graph", componentPath: "./dashboard-view", icon: "Workflow", placement: "primary", order: 1 },
+ },
+ {
+ pluginId: "example",
+ view: { viewId: "dashboard", label: "Example", componentPath: "./dashboard-view", icon: "Workflow", placement: "overflow", order: 2 },
+ },
+ ] as MainContentProps["pluginDashboardViews"],
graphPluginTaskView: "plugin:fusion-plugin-dependency-graph:graph",
graphWorkflowSelection: null,
setGraphWorkflowSelection: vi.fn(),
@@ -145,7 +162,6 @@ function mainContentProps(overrides: Partial = {}): MainConten
memoryEnabled: true,
goalsEnabled: true,
handleOpenMission: vi.fn(),
- todosEnabled: true,
openPlanningWithInitialPlanWithNav: vi.fn(),
ingestCreatedTasks: vi.fn(),
nodesEnabled: true,
@@ -211,7 +227,6 @@ function mainContentProps(overrides: Partial = {}): MainConten
ResearchView: LazyStub as MainContentProps["ResearchView"],
SecretsView: LazyStub as MainContentProps["SecretsView"],
SkillsView: LazyStub as MainContentProps["SkillsView"],
- TodoView: LazyStub as MainContentProps["TodoView"],
_AutomationsView: LazyStub as MainContentProps["_AutomationsView"],
_ImportTasksView: LazyStub as MainContentProps["_ImportTasksView"],
_SettingsView: LazyStub as MainContentProps["_SettingsView"],
diff --git a/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts b/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts
index bfacc365c7..e7bc8f9a04 100644
--- a/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts
+++ b/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts
@@ -523,7 +523,14 @@ describe("useAppSettings", () => {
});
});
- it("derives todosEnabled from experimentalFeatures.todoView", async () => {
+ /*
+ FNXC:Navigation 2026-08-15-22:15:
+ FN-8762 (5b2b31d2c9) extracted Todo Lists into the bundled `fusion-plugin-todos`
+ plugin and removed the host `todosEnabled` derivation from useAppSettings; the
+ `experimentalFeatures.todoView` gate no longer exists. Regression-pin that the
+ removed field never reappears on the hook result.
+ */
+ it("no longer derives a host todosEnabled flag (Todos is the fusion-plugin-todos plugin, FN-8762)", async () => {
mockFetchSettings.mockResolvedValueOnce({
autoMerge: false,
globalPause: false,
@@ -539,8 +546,9 @@ describe("useAppSettings", () => {
const { result } = renderHook(() => useAppSettings("proj_123"));
await waitFor(() => {
- expect(result.current.todosEnabled).toBe(true);
+ expect(result.current.taskStuckTimeoutMs).toBe(600000);
});
+ expect("todosEnabled" in result.current).toBe(false);
});
it("derives goalsEnabled from experimentalFeatures.goalsView", async () => {