diff --git a/packages/dashboard/app/components/__tests__/ListView.test.tsx b/packages/dashboard/app/components/__tests__/ListView.test.tsx
index 4734775c74..6ef5944415 100644
--- a/packages/dashboard/app/components/__tests__/ListView.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ListView.test.tsx
@@ -420,6 +420,16 @@ describe("ListView", () => {
localStorage.clear();
showAllColumnsByDefault();
ensureMatchMedia();
+ /*
+ FNXC:ViewportMode 2026-07-24-02:30:
+ FN-8557 (973c978f9) made isMobileViewport treat `window.innerWidth <= 768`
+ as a mobile signal alongside matchMedia. mockMobileViewport stamps
+ innerWidth=375 via defineProperty and its spy restore only resets
+ matchMedia, so the leaked width flipped every later test into the mobile
+ card layout (no
rows). Reset innerWidth to a desktop width here so the
+ per-test viewport helpers stay authoritative.
+ */
+ Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: false,
media: query,
@@ -4221,6 +4231,8 @@ describe("ListView - Bulk Selection", () => {
for (const key of Object.keys(listViewSseHandlers)) delete listViewSseHandlers[key];
localStorage.clear();
ensureMatchMedia();
+ // FNXC:ViewportMode 2026-07-24-02:30: FN-8557 innerWidth leak reset (see the main ListView beforeEach comment).
+ Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
matches: false,
media: query,
diff --git a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
index b7687621bf..de4b88544e 100644
--- a/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
+++ b/packages/dashboard/app/components/__tests__/settings-mobile.test.tsx
@@ -163,6 +163,15 @@ function setDocumentHidden(hidden: boolean): void {
}
function mockSettingsViewport(matches: boolean): void {
+ /*
+ FNXC:ViewportMode 2026-07-24-02:20:
+ FN-8557 (973c978f9) made isMobileViewport treat `window.innerWidth <= 768` as a
+ mobile signal alongside matchMedia. Individual mobile tests here stamp
+ innerWidth=375 via defineProperty without restoring it, which leaked mobile
+ mode into later desktop assertions. The viewport mock now owns innerWidth in
+ both directions so each test's declared viewport is authoritative.
+ */
+ Object.defineProperty(window, "innerWidth", { configurable: true, value: matches ? 375 : 1280 });
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
diff --git a/packages/dashboard/app/hooks/__tests__/useModalResizePersist.test.tsx b/packages/dashboard/app/hooks/__tests__/useModalResizePersist.test.tsx
index 902b06eb28..1e79e77ea9 100644
--- a/packages/dashboard/app/hooks/__tests__/useModalResizePersist.test.tsx
+++ b/packages/dashboard/app/hooks/__tests__/useModalResizePersist.test.tsx
@@ -181,7 +181,15 @@ describe("useModalResizePersist", () => {
});
it("clears inline size and does not inject a grip on mobile", () => {
- setViewport(700);
+ /*
+ FNXC:ViewportMode 2026-07-24-02:25:
+ FN-8557 (973c978f9) classifies a narrow touch viewport with a tablet-class
+ physical screen (min edge > 480px) as tablet, which keeps the FN-6377 touch
+ grip. jsdom reports `"ontouchstart" in window === true`, so the old 700x800
+ fixture became a tablet. Use a phone-class screen (375px min edge) so this
+ test still exercises the true mobile contract: no grip, inline size cleared.
+ */
+ setViewport(375);
localStorage.setItem(STORAGE_KEY, JSON.stringify({ width: 610, height: 480 }));
render();
diff --git a/packages/dashboard/src/__tests__/debug-execfile.test.ts b/packages/dashboard/src/__tests__/debug-execfile.test.ts
index 54e689a056..ff346e7fbc 100644
--- a/packages/dashboard/src/__tests__/debug-execfile.test.ts
+++ b/packages/dashboard/src/__tests__/debug-execfile.test.ts
@@ -20,6 +20,14 @@ class MockStore extends EventEmitter {
addTask(task: Task): void { this.tasks.set(task.id, task); }
getMissionStore() { return new EventEmitter(); }
async listTasks(): Promise { return []; }
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers = vi.fn().mockResolvedValue([]);
}
function createTask(overrides: Partial = {}): Task {
diff --git a/packages/dashboard/src/__tests__/scripts-routes.routes.test.ts b/packages/dashboard/src/__tests__/scripts-routes.routes.test.ts
index 31ae3b3d39..841323ac2a 100644
--- a/packages/dashboard/src/__tests__/scripts-routes.routes.test.ts
+++ b/packages/dashboard/src/__tests__/scripts-routes.routes.test.ts
@@ -68,6 +68,14 @@ function createMockStore(overrides: Partial = {}): TaskStore {
updateWorkflowStep: vi.fn(),
deleteWorkflowStep: vi.fn(),
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
...overrides,
} as unknown as TaskStore;
}
diff --git a/packages/dashboard/src/__tests__/shared-branch-group-entry-points.test.ts b/packages/dashboard/src/__tests__/shared-branch-group-entry-points.test.ts
index 8610052a6f..8905950237 100644
--- a/packages/dashboard/src/__tests__/shared-branch-group-entry-points.test.ts
+++ b/packages/dashboard/src/__tests__/shared-branch-group-entry-points.test.ts
@@ -237,6 +237,14 @@ function createMockStore(): TaskStore {
getMissionTask: vi.fn(),
deleteMissionTask: vi.fn(),
}),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
};
return store as unknown as TaskStore;
diff --git a/packages/dashboard/src/__tests__/stranded-refinements-routes.test.ts b/packages/dashboard/src/__tests__/stranded-refinements-routes.test.ts
index 596271b390..0f5df22d88 100644
--- a/packages/dashboard/src/__tests__/stranded-refinements-routes.test.ts
+++ b/packages/dashboard/src/__tests__/stranded-refinements-routes.test.ts
@@ -20,6 +20,14 @@ function createMockStore(overrides: Partial = {}): TaskStore {
logEntry: vi.fn().mockResolvedValue(undefined),
on: vi.fn(),
off: vi.fn(),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
...overrides,
} as unknown as TaskStore;
}
diff --git a/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts b/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts
index ddf9cf421d..b5b48de6d2 100644
--- a/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts
+++ b/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts
@@ -34,6 +34,14 @@ class MockStore extends EventEmitter {
getWorkflowDefinition = vi.fn(async () => undefined);
getWorkflowSettingValues = vi.fn((workflowId: string, projectId: string) => this.workflowValues.get(`${workflowId}::${projectId}`) ?? {});
getWorkflowSettingsProjectId = vi.fn(() => "default");
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers = vi.fn().mockResolvedValue([]);
setSelection(taskId: string, workflowId: string): void {
this.workflowSelections.set(taskId, { workflowId, stepIds: [] });
diff --git a/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts b/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts
index 99477ebe4b..6a8d13fe13 100644
--- a/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts
+++ b/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts
@@ -99,6 +99,14 @@ function createMockStore(fusionDir: string) {
getAllDocuments: vi.fn().mockResolvedValue([]),
listWorkflowSteps: vi.fn().mockResolvedValue([]),
getMissionStore: vi.fn(),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
} as any;
}
diff --git a/packages/dashboard/src/routes/__tests__/agent-onboarding-routes.test.ts b/packages/dashboard/src/routes/__tests__/agent-onboarding-routes.test.ts
index 6db146877a..97a2a2c327 100644
--- a/packages/dashboard/src/routes/__tests__/agent-onboarding-routes.test.ts
+++ b/packages/dashboard/src/routes/__tests__/agent-onboarding-routes.test.ts
@@ -60,6 +60,14 @@ function createMockStore(): TaskStore {
updateWorkflowStep: vi.fn(),
deleteWorkflowStep: vi.fn(),
getMissionStore: vi.fn(),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
} as unknown as TaskStore;
}
diff --git a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge.test.ts b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge.test.ts
index 89eeb0071e..17b7050bef 100644
--- a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge.test.ts
+++ b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge.test.ts
@@ -11,6 +11,14 @@ describe("task workflow merge route", () => {
const store: TaskStore = {
getRootDir: vi.fn(() => process.cwd()),
mergeTask: vi.fn(),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
} as unknown as TaskStore;
const onMerge = vi.fn(async (id: string) => ({
diff --git a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts
index 9d5ff062ec..cc28c79ff6 100644
--- a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts
+++ b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts
@@ -58,6 +58,14 @@ const createHarness = (taskState: any, events: RunAuditEvent[]) => {
}
return filtered;
}),
+ /*
+ FNXC:PluginMcpServers 2026-07-24-02:05:
+ FN-8491 (3cd023fa4) made resolveProjectContext bind a project-scoped plugin
+ MCP provider on every getProjectContext call; a store exposing
+ getProjectScopedPluginMcpServers is treated as runtime-owned and skips the
+ binder (which would otherwise 500 on getPluginStore()).
+ */
+ getProjectScopedPluginMcpServers: vi.fn().mockResolvedValue([]),
} as unknown as TaskStore;
const app = express();