test(dashboard): update component expectations to intentional post-FN-87xx behavior

Dashboard bare-run repair, component-drift cluster. All nine suites
were stale expectations behind cited intentional commits — no UI
regressions found: FN-8826's always-on WIP lifecycle badge
(TaskCard.oversight now asserts no overseer element instead of an empty
header), FN-8762's Todo Lists plugin extraction (MobileNavBar,
MainContent.graph-popout, useAppSettings repurposed to the
pluginDashboardViews contract, with a regression pin that the removed
todosEnabled field never returns), FN-8796's clock-arbitrated
mergeTaskSnapshot, FN-8797's prompt-only planning refresh, FN-8702's
767.98px phone-sheet boundary, FN-8764's primary-role split, and
FN-8947's added touch-target selector. Every update carries an FNXC
comment citing the causing commit. Verified 9 files / 293 tests green,
tsconfig.app.json typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-15 22:15:06 -07:00
parent 0842dcc996
commit 5c5ffb6f48
9 changed files with 127 additions and 41 deletions

View File

@@ -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", () => {

View File

@@ -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" }),

View File

@@ -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(
<MobileNavBar
{...props}
experimentalFeatures={{ todoView: true }}
pluginDashboardViews={[todosPluginView]}
/>,
);
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(
<MobileNavBar
{...createDefaultProps()}
experimentalFeatures={{ todoView: true }}
pluginDashboardViews={[todosPluginView]}
/>,
);
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(
<MobileNavBar
{...createDefaultProps()}
view="todos"
experimentalFeatures={{ todoView: true }}
view="plugin:fusion-plugin-todos:todos"
pluginDashboardViews={[todosPluginView]}
/>,
);

View File

@@ -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");

View File

@@ -102,6 +102,22 @@ function makeTask(overrides: Partial<Task> = {}): 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<Task> = {},
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(
<TaskCard
@@ -575,7 +591,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => {
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();
});
});

View File

@@ -84,7 +84,13 @@ describe("TaskDetailModal GitLab tracking", () => {
rerender(
<TaskDetailModal
initialTab="definition"
task={makeTask({ column: "todo", gitlabTracking: { item: { ...projectIssue, kind: "merge_request", iid: 5, url: "https://gitlab.com/acme/app/-/merge_requests/5", title: "MR" } } })}
/*
FNXC:TaskDetailStateStability 2026-08-15-22:25:
FN-8796 (1320265455) made mergeTaskSnapshot clock-arbitrated: an equal-clock sparse
prop no longer replaces populated detail fields, so this rerender must carry a newer
`updatedAt` to model a real server mutation (see makeUpdatedTask's contract note).
*/
task={makeTask({ column: "todo", updatedAt: "2026-01-01T00:00:01Z", gitlabTracking: { item: { ...projectIssue, kind: "merge_request", iid: 5, url: "https://gitlab.com/acme/app/-/merge_requests/5", title: "MR" } } })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}

View File

@@ -87,23 +87,30 @@ describe("TaskDetailContent hidden-popup gating (active=false)", () => {
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 () => {

View File

@@ -104,6 +104,23 @@ function mainContentProps(overrides: Partial<MainContentProps> = {}): 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<MainContentProps> = {}): 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<MainContentProps> = {}): 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"],

View File

@@ -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 () => {