fix(dashboard-tests): satisfy FN-8491 plugin-MCP store contract across route suites; align planning create-task envelope

- routes-auth/automation/git/github/tasks: FN-8491 binds the plugin-MCP
  provider on every project-context resolution; mock stores now expose
  getProjectScopedPluginMcpServers so routes stop 500ing.
- routes-github: stub FN-8442 durable planning-claim plumbing whose internal
  getSession bypassed the namespace spy; the scoped-store routing contract
  is asserted unchanged.
- api-git: POST /planning/create-task now returns the FN-8442
  {task, alreadyCreated} envelope; mock matches.
- TaskDetailModal.tab-persistence: settle-then-requery before clicking the
  Session tab (detached-node race under CI load).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-23 23:51:29 -07:00
parent 74236211ea
commit fa6e4fef22
7 changed files with 80 additions and 3 deletions

View File

@@ -1081,7 +1081,15 @@ describe("Planning Mode API", () => {
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, createdTask, 201));
/*
FNXC:PlanningMode 2026-07-23-23:35:
FN-8442 (36b318096) changed POST /planning/create-task to respond with a
{ task, alreadyCreated } envelope (idempotent replay detection); the client
unwraps response.task. Mock the real server envelope, not a bare Task.
*/
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(true, { task: createdTask, alreadyCreated: false }, 201)
);
const result = await createTaskFromPlanning("plan-123");

View File

@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import type { Column, Task, TaskDetail } from "@fusion/core";
import {
@@ -232,8 +232,16 @@ describe("TaskDetailModal tab persistence", () => {
const view = renderDetail({ task });
await waitFor(() => expect(screen.getByRole("button", { name: "Session" })).toBeInTheDocument());
/*
FNXC:TaskDetailTabs 2026-07-24-00:00:
Settle pending CLI-session fetch commits and click a freshly-queried node: clicking
right after the tab appears raced the cliSession hydration re-render on loaded CI
shards (full-suite run 30070825088), dispatching on a detached node so the tab never
activated. Same detached-node class d7752931b fixed for PlanningModeModal Proceed.
*/
await act(async () => {});
fireEvent.click(screen.getByRole("button", { name: "Session" }));
expect(screen.getByRole("button", { name: "Session" })).toHaveClass("detail-tab-active");
await waitFor(() => expect(screen.getByRole("button", { name: "Session" })).toHaveClass("detail-tab-active"));
rerenderDetail(view.rerender, { task: makeTask({ id: "FN-NO-SESSION", column: "in-progress", prompt: "# Full task" }) });
await waitFor(() => expect(screen.getByRole("button", { name: "Plan" })).toHaveClass("detail-tab-active"));

View File

@@ -237,6 +237,16 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
addPrInfo: vi.fn().mockResolvedValue(undefined),
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
/*
FNXC:PluginMcpServers 2026-07-23-23:30:
FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
MCP provider on every getProjectContext call. A store that already exposes
getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
binder (which would otherwise call getPluginStore()); declare it here so the
route contracts under test stay isolated from plugin-loader bootstrapping.
Same alignment as remote-access-routes.test.ts (d7752931b).
*/
getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
listWorkflowSteps: vi.fn().mockResolvedValue([]),
createWorkflowStep: vi.fn(),
getWorkflowStep: vi.fn(),

View File

@@ -231,6 +231,16 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
/*
FNXC:PluginMcpServers 2026-07-23-23:40:
FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
MCP provider on every getProjectContext call. A store that already exposes
getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
binder (which would otherwise call getPluginStore()); declare it here so the
route contracts under test stay isolated from plugin-loader bootstrapping.
Same alignment as remote-access-routes.test.ts (d7752931b).
*/
getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
/*
FNXC:DashboardTests 2026-07-18-09:45:
Backup automation path calls resolveGlobalBackupRoot(store) which requires
getGlobalSettingsDir. Full-suite shard 4 failed FN-7537 manual backup parity

View File

@@ -207,6 +207,16 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
updatePrInfo: vi.fn().mockResolvedValue(undefined),
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
/*
FNXC:PluginMcpServers 2026-07-23-23:40:
FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
MCP provider on every getProjectContext call. A store that already exposes
getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
binder (which would otherwise call getPluginStore()); declare it here so the
route contracts under test stay isolated from plugin-loader bootstrapping.
Same alignment as remote-access-routes.test.ts (d7752931b).
*/
getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
listWorkflowSteps: vi.fn().mockResolvedValue([]),
createWorkflowStep: vi.fn(),
getWorkflowStep: vi.fn(),

View File

@@ -251,6 +251,16 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
removePrInfoByNumber: vi.fn().mockResolvedValue(undefined),
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
/*
FNXC:PluginMcpServers 2026-07-23-23:40:
FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
MCP provider on every getProjectContext call. A store that already exposes
getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
binder (which would otherwise call getPluginStore()); declare it here so the
route contracts under test stay isolated from plugin-loader bootstrapping.
Same alignment as remote-access-routes.test.ts (d7752931b).
*/
getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
listWorkflowSteps: vi.fn().mockResolvedValue([]),
createWorkflowStep: vi.fn(),
getWorkflowStep: vi.fn(),
@@ -1994,6 +2004,17 @@ describe("projectId store scoping regressions", () => {
keyDeliverables: [],
});
vi.spyOn(planningModule, "cleanupSession").mockImplementation(() => {});
/*
FNXC:PlanningMode 2026-07-23-23:55:
FN-8442 (36b318096) routes create-task through durable create-claim plumbing
(getDurablePlanningSession + updatePlanningCreateClaim). Those functions call the
REAL module-internal getSession (a namespace spy cannot intercept intra-module
calls), so the spied session above is invisible to them and they throw
SessionNotFoundError. This test's contract is scoped-store routing of createTask,
not claim persistence, so stub the claim plumbing to inert no-ops here.
*/
vi.spyOn(planningModule, "getDurablePlanningSession").mockResolvedValue(undefined);
vi.spyOn(planningModule, "updatePlanningCreateClaim").mockResolvedValue(undefined);
(scopedStore.createTask as ReturnType<typeof vi.fn>).mockResolvedValue({
...FAKE_TASK_DETAIL,

View File

@@ -236,6 +236,16 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
recordActivity: vi.fn().mockResolvedValue(undefined),
getFusionDir: vi.fn().mockReturnValue("/fake/root/.fusion"),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
/*
FNXC:PluginMcpServers 2026-07-23-23:40:
FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
MCP provider on every getProjectContext call. A store that already exposes
getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
binder (which would otherwise call getPluginStore()); declare it here so the
route contracts under test stay isolated from plugin-loader bootstrapping.
Same alignment as remote-access-routes.test.ts (d7752931b).
*/
getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
// FNXC:PostgresCutover 2026-07-05-16:20: routes borrow the AsyncDataLayer
// from the scoped store (e.g. triggerCommentWakeForAssignedAgent builds a
// backend AgentStore). null = legacy mode for this mock.