feat(FN-3952): expose full mesh state aggregation and fix graph selection b

Exposed full mesh state aggregation via mesh routes with corresponding tests, refined the dependency graph to decouple node selection from opening task details, and documented the new aggregation API.

Fusion-Task-Id: FN-3952
This commit is contained in:
Fusion
2026-05-10 14:37:30 -07:00
committed by gsxdsm
parent bcb4107a1a
commit 5f4fd222da
9 changed files with 185 additions and 59 deletions

View File

@@ -105,8 +105,8 @@ export function GraphTaskNode({
data-current-step={isActive && hasValidCurrentStep ? String(task.currentStep) : undefined}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={(event) => {
onClick?.(event);
onClick={onClick}
onDoubleClick={(event) => {
if (event.defaultPrevented) {
return;
}

View File

@@ -87,7 +87,7 @@ describe("DependencyGraph highlighting", () => {
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
});
it("applies edge dimming/highlighting and preserves click-to-detail", () => {
it("applies edge dimming/highlighting and preserves double-click-to-detail", () => {
const onOpenDetail = vi.fn();
render(<DependencyGraph tasks={tasks} onOpenDetail={onOpenDetail} />);
@@ -99,7 +99,7 @@ describe("DependencyGraph highlighting", () => {
expect(edgeAB?.className.baseVal || edgeAB?.className).toContain("graph-edge--highlighted");
expect(edgeCB?.className.baseVal || edgeCB?.className).toContain("graph-edge--highlighted");
fireEvent.click(screen.getByTestId("task-C"));
fireEvent.doubleClick(screen.getByTestId("graph-task-node-C"));
expect(onOpenDetail).toHaveBeenCalledTimes(1);
expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "C" }));
});

View File

@@ -117,18 +117,33 @@ describe("DependencyGraph", () => {
expect(fitToGraph).toHaveBeenCalled();
});
it("clicking a card triggers onOpenDetail exactly once", () => {
it("single-clicking a node selects it without opening detail", () => {
const onOpenDetail = vi.fn();
render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenDetail={onOpenDetail} />);
fireEvent.click(screen.getByTestId("task-A"));
const node = screen.getByTestId("graph-task-node-A");
fireEvent.click(node);
expect(node.className).toContain("graph-node--draggable");
expect(onOpenDetail).not.toHaveBeenCalled();
});
it("double-clicking a node opens detail", () => {
const onOpenDetail = vi.fn();
render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenDetail={onOpenDetail} />);
fireEvent.doubleClick(screen.getByTestId("graph-task-node-A"));
expect(onOpenDetail).toHaveBeenCalledTimes(1);
expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "A" }));
});
it("falls back to onOpenTaskDetail when onOpenDetail is not provided", () => {
it("double-click falls back to onOpenTaskDetail when onOpenDetail is not provided", () => {
const onOpenTaskDetail = vi.fn();
render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenTaskDetail={onOpenTaskDetail} />);
fireEvent.click(screen.getByTestId("task-A"));
fireEvent.doubleClick(screen.getByTestId("graph-task-node-A"));
expect(onOpenTaskDetail).toHaveBeenCalledWith("A");
});

View File

@@ -220,18 +220,17 @@ describe("GraphTaskNode", () => {
expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
});
it("clicking card opens task detail exactly once", () => {
it("double-clicking card opens task detail exactly once", () => {
const props = createProps(createTask());
const { container } = render(<GraphTaskNode {...props} />);
const card = container.querySelector(".card");
expect(card).toBeTruthy();
fireEvent.click(card!);
const node = screen.getByTestId("graph-task-node-FN-TEST");
fireEvent.doubleClick(node);
expect(props.onOpenDetail).toHaveBeenCalledTimes(1);
expect(props.onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-TEST" }));
});
it("clicking active indicator surface opens task detail", () => {
it("single click on active indicator surface does not open task detail", () => {
const props = createProps(createTask({ column: "in-progress", status: "executing" }));
const { container } = render(<GraphTaskNode {...props} />);
@@ -239,8 +238,7 @@ describe("GraphTaskNode", () => {
expect(indicator).toBeTruthy();
fireEvent.click(indicator!);
expect(props.onOpenDetail).toHaveBeenCalledTimes(1);
expect(props.onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-TEST" }));
expect(props.onOpenDetail).not.toHaveBeenCalled();
});
it("applies highlighted class only when requested", () => {

View File

@@ -67,7 +67,7 @@ describe("dependency graph plugin host integration contract", () => {
}),
);
fireEvent.click(screen.getByTestId("task-FN-HOST"));
fireEvent.doubleClick(screen.getByTestId("graph-task-node-FN-HOST"));
expect(openTaskDetail).toHaveBeenCalledTimes(1);
expect(openTaskDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-HOST" }));
});

View File

@@ -72,11 +72,11 @@ describe("dependency graph interactions", () => {
expect(result.current.pan.y).toBeCloseTo(150, 3);
});
it("clicking a node opens task detail", () => {
it("double-clicking a node opens task detail", () => {
const onOpenDetail = vi.fn();
render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenDetail={onOpenDetail} />);
fireEvent.click(screen.getByTestId("task-A"));
fireEvent.doubleClick(screen.getByTestId("graph-task-node-A"));
expect(onOpenDetail).toHaveBeenCalledTimes(1);
expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "A" }));
expect(screen.getAllByText(/Executing/).length).toBeGreaterThan(0);