import { fireEvent, render, screen, within } from "@testing-library/react"; import { ReactFlowProvider } from "@xyflow/react"; import { describe, expect, it, vi } from "vitest"; import { DirectToolGroupCard } from "@/components/flow/DirectToolGroupCard"; import { RetrievalAgentCard } from "@/components/flow/RetrievalAgentCard"; import { buildHorizontalLayout, retrievalAgentItem, retrievalDirectItem } from "@/lib/layout"; import { makeBranch, makeRetrievalStage, makeView } from "@/tests/fixtures"; const base = { onOpen: vi.fn(), onToggleRound: vi.fn(), onToggleBranch: vi.fn(), onToggleRetrievalAgent: vi.fn() }; const nodeProps = { selected: false, zIndex: 1, isConnectable: false, positionAbsoluteX: 0, positionAbsoluteY: 0, dragging: false, draggable: true, selectable: true, deletable: false }; const wrap = (node: React.ReactNode) => render({node}); describe("V8 retrieval stage", () => { it("shows an immediate Agent retrieval preview on keyboard focus", () => { const branch = makeBranch(1, 1); const item = retrievalAgentItem(branch.retrievalStage.agentRuns[0], 1, 1); const { container } = wrap(); fireEvent.focus(container.querySelector(".retrievalAgentCard")!); const preview = screen.getByRole("dialog", { name: /取数预览/ }); expect(within(preview).getByText("命中了什么")).toBeInTheDocument(); expect(within(preview).getByText("初筛得到")).toBeInTheDocument(); }); it("loads and presents real tool results only after the preview opens", async () => { const branch = makeBranch(1, 1); const item = retrievalDirectItem(branch.retrievalStage.directToolGroups[0], 1, 1); if (item.itemType !== "retrieval-direct") throw new Error("expected retrieval-direct item"); item.group.calls = [{ id: "event:4047", eventId: 4047, businessLabel: "领域信息", status: "success", resultSummary: "已读取", detailRef: "event:4047" }]; item.group.callCount = 1; item.group.successCount = 1; const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ detailKind: "retrieval-event", outcome: { state: "hit", count: 1, message: "读取成功,共返回 1 条记录。" }, items: [{ id: "fact-1", title: "领域事实 1", subtitle: "核实了布里丹寓言的史实背景。", meta: [], sections: [] }], technical: {} }), }); vi.stubGlobal("fetch", fetchMock); const { container } = wrap(); expect(fetchMock).not.toHaveBeenCalled(); fireEvent.focus(container.querySelector(".directToolCard")!); const preview = screen.getByRole("dialog", { name: /取数预览/ }); expect(await within(preview).findByText("领域事实 1")).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledTimes(1); vi.unstubAllGlobals(); }); it("presents direct tools and delegated agents as peer retrieval operations", () => { const branch = makeBranch(1, 1); const direct = retrievalDirectItem(branch.retrievalStage.directToolGroups[0], 1, 1); const agent = retrievalAgentItem(branch.retrievalStage.agentRuns[0], 1, 1); wrap(<>); expect(screen.getByText("工具取数")).toBeInTheDocument(); expect(screen.getByText("Agent 取数")).toBeInTheDocument(); expect(screen.getByText("初筛整理")).toBeInTheDocument(); expect(screen.queryByText("数据取舍")).not.toBeInTheDocument(); }); it("keeps repeated attempts inside one agent and opens an individual query", () => { const branch = makeBranch(1, 1); const run = { ...branch.retrievalStage.agentRuns[0], attempts: branch.retrievalStage.agentRuns[0].attempts.concat(Array.from({ length: 10 }, (_, index) => ({ ...branch.retrievalStage.agentRuns[0].attempts[0], id: `event:extra-${index}`, eventId: 900 + index, queryLabel: `额外查询 ${index + 1}`, detailRef: `event:${900 + index}` }))) }; run.querySummary = { ...run.querySummary, total: 13 }; const item = retrievalAgentItem(run, 1, 1); const onOpen = vi.fn(); wrap(); expect(screen.getByText((_, element) => element?.tagName === "P" && element.textContent?.startsWith("13 次") === true)).toBeInTheDocument(); expect(screen.getAllByRole("listitem")).toHaveLength(13); fireEvent.click(screen.getByText("额外查询 1")); expect(onOpen).toHaveBeenCalledWith(expect.objectContaining({ itemType: "query-attempt", detailRef: "event:900" }), "business"); }); it("stacks both parallel and sequential retrieval operations vertically", () => { const parallelView = makeView(1, 1, 1); parallelView.rounds[0].branches[0].retrievalStage = makeRetrievalStage(1, 1, 2, 1, "parallel"); const options = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), expandedRetrievalAgents: new Set(), ...base }; const parallel = buildHorizontalLayout(parallelView, options).nodes.filter((node) => node.type === "directToolGroup" || node.type === "retrievalAgent"); expect(new Set(parallel.map((node) => node.position.x)).size).toBe(1); expect(new Set(parallel.map((node) => node.position.y)).size).toBe(parallel.length); const sequentialView = makeView(1, 1, 1); const sequential = buildHorizontalLayout(sequentialView, options).nodes.filter((node) => node.type === "directToolGroup" || node.type === "retrievalAgent"); expect(new Set(sequential.map((node) => node.position.x)).size).toBe(1); expect(new Set(sequential.map((node) => node.position.y)).size).toBe(sequential.length); }); it("keeps unknown-time operations in one neutral column inside the retrieval frame", () => { const view = makeView(1, 1, 1); const stage = view.rounds[0].branches[0].retrievalStage; stage.observedMode = "unknown"; stage.waves = []; stage.directToolGroups.forEach((item) => { item.waveIndex = undefined; item.startedAt = undefined; item.endedAt = undefined; }); stage.agentRuns.forEach((item) => { item.waveIndex = undefined; item.startedAt = undefined; item.endedAt = undefined; }); const options = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), expandedRetrievalAgents: new Set(), ...base }; const layout = buildHorizontalLayout(view, options); const operations = layout.nodes.filter((node) => node.type === "directToolGroup" || node.type === "retrievalAgent"); const frame = layout.nodes.find((node) => node.type === "retrievalStageFrame"); expect(new Set(operations.map((node) => node.position.x)).size).toBe(1); expect(Math.max(...operations.map((node) => node.position.x + Number(node.style?.width || 340)))).toBeLessThanOrEqual(Number(frame?.style?.width || 0)); }); it("marks expanded query lists so semantic zoom cannot hide a successful expansion", () => { const branch = makeBranch(1, 1); const item = retrievalAgentItem(branch.retrievalStage.agentRuns[0], 1, 1); const { container } = wrap(); expect(container.querySelector(".retrievalAgentCard")).toHaveClass("isQueryExpanded"); expect(container.querySelector(".queryAttemptList")).toBeInTheDocument(); }); it("grows the branch frame when query rows are expanded", () => { const view = makeView(1, 1, 1); const agentId = view.rounds[0].branches[0].retrievalStage.agentRuns[0].id; const baseOptions = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), ...base }; const collapsed = buildHorizontalLayout(view, { ...baseOptions, expandedRetrievalAgents: new Set() }); const expanded = buildHorizontalLayout(view, { ...baseOptions, expandedRetrievalAgents: new Set([agentId]) }); const height = (nodes: typeof collapsed.nodes) => Number(nodes.find((node) => node.type === "branchFrame")?.style?.height || 0); expect(height(expanded.nodes)).toBeGreaterThan(height(collapsed.nodes)); }); });