feat(FN-1529): implement search query propagation to remote node data

- Add search query propagation so remote nodes receive and use query filters
- Update api-node.ts to accept and forward search query parameters
- Add useRemoteNodeData query key dependency for proper re-fetching on filter changes
- Add api-node.test.ts with tests for search query propagation
- Add comprehensive tests for App component including remote data filtering
- Document search query propagation pitfall in .fusion/memory.md
This commit is contained in:
gsxdsm
2026-04-10 02:59:38 -07:00
parent a3d70e5190
commit dd77c79718
7 changed files with 503 additions and 11 deletions

View File

@@ -166,6 +166,7 @@ vi.mock("../../hooks/useNodes", () => ({
import { App } from "../../App";
import { fetchAuthStatus, fetchSettings, fetchGlobalSettings, fetchTaskDetail, updateSettings, runScript, fetchScripts } from "../../api";
import * as apiNodeModule from "../../hooks/useRemoteNodeData";
beforeEach(() => {
vi.clearAllMocks();
@@ -1735,3 +1736,159 @@ describe("App node mode switching", () => {
});
});
});
describe("App search query propagation to remote mode", () => {
// Mock useRemoteNodeData to capture searchQuery parameter
let capturedSearchQuery: string | undefined;
beforeEach(() => {
capturedSearchQuery = undefined;
// Get the mocked useRemoteNodeData and capture the searchQuery
vi.mocked(apiNodeModule.useRemoteNodeData).mockImplementation((nodeId, options) => {
capturedSearchQuery = options?.searchQuery;
return {
projects: [],
tasks: [],
health: null,
loading: false,
error: null,
refresh: vi.fn(),
};
});
});
afterEach(() => {
vi.clearAllMocks();
localStorage.removeItem("fusion-dashboard-current-node");
});
it("passes searchQuery to useRemoteNodeData when in remote mode", async () => {
// Set up mock with remote node
const { useNodes } = await import("../../hooks/useNodes");
vi.mocked(useNodes).mockReturnValue({
nodes: [
{
id: "node_remote_1",
name: "Remote Node 1",
type: "remote" as const,
url: "http://remote:4040",
status: "online" as const,
maxConcurrent: 2,
createdAt: "",
updatedAt: "",
},
],
loading: false,
error: null,
refresh: vi.fn(),
register: vi.fn(),
update: vi.fn(),
unregister: vi.fn(),
healthCheck: vi.fn(),
});
// Mock node context to return remote node
mockNodeContextValue.currentNode = {
id: "node_remote_1",
name: "Remote Node 1",
type: "remote",
url: "http://remote:4040",
status: "online",
maxConcurrent: 2,
createdAt: "",
updatedAt: "",
};
mockNodeContextValue.currentNodeId = "node_remote_1";
mockNodeContextValue.isRemote = true;
// Set project mode
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
await waitFor(() => {
expect(fetchSettings).toHaveBeenCalled();
});
// Wait for initial load to complete
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
});
// At this point, searchQuery should be passed to useRemoteNodeData
// capturedSearchQuery should be undefined (empty search initially)
expect(capturedSearchQuery).toBeUndefined();
});
it("updates searchQuery in useRemoteNodeData when header search changes", async () => {
// Set up mock with remote node
const { useNodes } = await import("../../hooks/useNodes");
vi.mocked(useNodes).mockReturnValue({
nodes: [
{
id: "node_remote_1",
name: "Remote Node 1",
type: "remote" as const,
url: "http://remote:4040",
status: "online" as const,
maxConcurrent: 2,
createdAt: "",
updatedAt: "",
},
],
loading: false,
error: null,
refresh: vi.fn(),
register: vi.fn(),
update: vi.fn(),
unregister: vi.fn(),
healthCheck: vi.fn(),
});
// Mock node context to return remote node
mockNodeContextValue.currentNode = {
id: "node_remote_1",
name: "Remote Node 1",
type: "remote",
url: "http://remote:4040",
status: "online",
maxConcurrent: 2,
createdAt: "",
updatedAt: "",
};
mockNodeContextValue.currentNodeId = "node_remote_1";
mockNodeContextValue.isRemote = true;
// Set project mode
localStorage.setItem("kb-dashboard-view-mode", "project");
render(<App />);
await waitFor(() => {
expect(fetchSettings).toHaveBeenCalled();
});
// Wait for initial load to complete
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
});
// Click the search toggle button to open search
const searchToggleBtn = screen.getByTestId("desktop-header-search-btn");
expect(searchToggleBtn).toBeInTheDocument();
fireEvent.click(searchToggleBtn);
// Now the search input should be visible
const searchInput = await screen.findByPlaceholderText("Search tasks...");
expect(searchInput).toBeInTheDocument();
// Type in the search input
fireEvent.change(searchInput, { target: { value: "test search" } });
// Wait for the search query to propagate
await waitFor(() => {
expect(capturedSearchQuery).toBe("test search");
});
});
});