retrieval-v8.test.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { fireEvent, render, screen, within } from "@testing-library/react";
  2. import { ReactFlowProvider } from "@xyflow/react";
  3. import { describe, expect, it, vi } from "vitest";
  4. import { DirectToolGroupCard } from "@/components/flow/DirectToolGroupCard";
  5. import { RetrievalAgentCard } from "@/components/flow/RetrievalAgentCard";
  6. import { buildHorizontalLayout, retrievalAgentItem, retrievalDirectItem } from "@/lib/layout";
  7. import { makeBranch, makeRetrievalStage, makeView } from "@/tests/fixtures";
  8. const base = { onOpen: vi.fn(), onToggleRound: vi.fn(), onToggleBranch: vi.fn(), onToggleRetrievalAgent: vi.fn() };
  9. const nodeProps = { selected: false, zIndex: 1, isConnectable: false, positionAbsoluteX: 0, positionAbsoluteY: 0, dragging: false, draggable: true, selectable: true, deletable: false };
  10. const wrap = (node: React.ReactNode) => render(<ReactFlowProvider>{node}</ReactFlowProvider>);
  11. describe("V8 retrieval stage", () => {
  12. it("shows an immediate Agent retrieval preview on keyboard focus", () => {
  13. const branch = makeBranch(1, 1);
  14. const item = retrievalAgentItem(branch.retrievalStage.agentRuns[0], 1, 1);
  15. const { container } = wrap(<RetrievalAgentCard id={item.id} type="retrievalAgent" {...nodeProps} data={{ ...base, buildId: "443", item, expanded: false }} />);
  16. fireEvent.focus(container.querySelector(".retrievalAgentCard")!);
  17. const preview = screen.getByRole("dialog", { name: /取数预览/ });
  18. expect(within(preview).getByText("命中了什么")).toBeInTheDocument();
  19. expect(within(preview).getByText("初筛得到")).toBeInTheDocument();
  20. });
  21. it("loads and presents real tool results only after the preview opens", async () => {
  22. const branch = makeBranch(1, 1);
  23. const item = retrievalDirectItem(branch.retrievalStage.directToolGroups[0], 1, 1);
  24. if (item.itemType !== "retrieval-direct") throw new Error("expected retrieval-direct item");
  25. item.group.calls = [{ id: "event:4047", eventId: 4047, businessLabel: "领域信息", status: "success", resultSummary: "已读取", detailRef: "event:4047" }];
  26. item.group.callCount = 1;
  27. item.group.successCount = 1;
  28. const fetchMock = vi.fn().mockResolvedValue({
  29. ok: true,
  30. json: async () => ({ detailKind: "retrieval-event", outcome: { state: "hit", count: 1, message: "读取成功,共返回 1 条记录。" }, items: [{ id: "fact-1", title: "领域事实 1", subtitle: "核实了布里丹寓言的史实背景。", meta: [], sections: [] }], technical: {} }),
  31. });
  32. vi.stubGlobal("fetch", fetchMock);
  33. const { container } = wrap(<DirectToolGroupCard id={item.id} type="directToolGroup" {...nodeProps} data={{ ...base, buildId: "443", item }} />);
  34. expect(fetchMock).not.toHaveBeenCalled();
  35. fireEvent.focus(container.querySelector(".directToolCard")!);
  36. const preview = screen.getByRole("dialog", { name: /取数预览/ });
  37. expect(await within(preview).findByText("领域事实 1")).toBeInTheDocument();
  38. expect(fetchMock).toHaveBeenCalledTimes(1);
  39. vi.unstubAllGlobals();
  40. });
  41. it("presents direct tools and delegated agents as peer retrieval operations", () => {
  42. const branch = makeBranch(1, 1);
  43. const direct = retrievalDirectItem(branch.retrievalStage.directToolGroups[0], 1, 1);
  44. const agent = retrievalAgentItem(branch.retrievalStage.agentRuns[0], 1, 1);
  45. wrap(<><DirectToolGroupCard id={direct.id} type="directToolGroup" {...nodeProps} data={{ ...base, item: direct }} /><RetrievalAgentCard id={agent.id} type="retrievalAgent" {...nodeProps} data={{ ...base, item: agent, expanded: false }} /></>);
  46. expect(screen.getByText("工具取数")).toBeInTheDocument();
  47. expect(screen.getByText("Agent 取数")).toBeInTheDocument();
  48. expect(screen.getByText("初筛整理")).toBeInTheDocument();
  49. expect(screen.queryByText("数据取舍")).not.toBeInTheDocument();
  50. });
  51. it("keeps repeated attempts inside one agent and opens an individual query", () => {
  52. const branch = makeBranch(1, 1);
  53. 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}` }))) };
  54. run.querySummary = { ...run.querySummary, total: 13 };
  55. const item = retrievalAgentItem(run, 1, 1);
  56. const onOpen = vi.fn();
  57. wrap(<RetrievalAgentCard id={item.id} type="retrievalAgent" {...nodeProps} data={{ ...base, onOpen, item, expanded: true }} />);
  58. expect(screen.getByText((_, element) => element?.tagName === "P" && element.textContent?.startsWith("13 次") === true)).toBeInTheDocument();
  59. expect(screen.getAllByRole("listitem")).toHaveLength(13);
  60. fireEvent.click(screen.getByText("额外查询 1"));
  61. expect(onOpen).toHaveBeenCalledWith(expect.objectContaining({ itemType: "query-attempt", detailRef: "event:900" }), "business");
  62. });
  63. it("stacks both parallel and sequential retrieval operations vertically", () => {
  64. const parallelView = makeView(1, 1, 1);
  65. parallelView.rounds[0].branches[0].retrievalStage = makeRetrievalStage(1, 1, 2, 1, "parallel");
  66. const options = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), expandedRetrievalAgents: new Set<string>(), ...base };
  67. const parallel = buildHorizontalLayout(parallelView, options).nodes.filter((node) => node.type === "directToolGroup" || node.type === "retrievalAgent");
  68. expect(new Set(parallel.map((node) => node.position.x)).size).toBe(1);
  69. expect(new Set(parallel.map((node) => node.position.y)).size).toBe(parallel.length);
  70. const sequentialView = makeView(1, 1, 1);
  71. const sequential = buildHorizontalLayout(sequentialView, options).nodes.filter((node) => node.type === "directToolGroup" || node.type === "retrievalAgent");
  72. expect(new Set(sequential.map((node) => node.position.x)).size).toBe(1);
  73. expect(new Set(sequential.map((node) => node.position.y)).size).toBe(sequential.length);
  74. });
  75. it("keeps unknown-time operations in one neutral column inside the retrieval frame", () => {
  76. const view = makeView(1, 1, 1);
  77. const stage = view.rounds[0].branches[0].retrievalStage;
  78. stage.observedMode = "unknown";
  79. stage.waves = [];
  80. stage.directToolGroups.forEach((item) => { item.waveIndex = undefined; item.startedAt = undefined; item.endedAt = undefined; });
  81. stage.agentRuns.forEach((item) => { item.waveIndex = undefined; item.startedAt = undefined; item.endedAt = undefined; });
  82. const options = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), expandedRetrievalAgents: new Set<string>(), ...base };
  83. const layout = buildHorizontalLayout(view, options);
  84. const operations = layout.nodes.filter((node) => node.type === "directToolGroup" || node.type === "retrievalAgent");
  85. const frame = layout.nodes.find((node) => node.type === "retrievalStageFrame");
  86. expect(new Set(operations.map((node) => node.position.x)).size).toBe(1);
  87. expect(Math.max(...operations.map((node) => node.position.x + Number(node.style?.width || 340)))).toBeLessThanOrEqual(Number(frame?.style?.width || 0));
  88. });
  89. it("marks expanded query lists so semantic zoom cannot hide a successful expansion", () => {
  90. const branch = makeBranch(1, 1);
  91. const item = retrievalAgentItem(branch.retrievalStage.agentRuns[0], 1, 1);
  92. const { container } = wrap(<RetrievalAgentCard id={item.id} type="retrievalAgent" {...nodeProps} data={{ ...base, item, expanded: true }} />);
  93. expect(container.querySelector(".retrievalAgentCard")).toHaveClass("isQueryExpanded");
  94. expect(container.querySelector(".queryAttemptList")).toBeInTheDocument();
  95. });
  96. it("grows the branch frame when query rows are expanded", () => {
  97. const view = makeView(1, 1, 1);
  98. const agentId = view.rounds[0].branches[0].retrievalStage.agentRuns[0].id;
  99. const baseOptions = { expandedRounds: new Set([1]), expandedBranches: new Set(["1:1"]), ...base };
  100. const collapsed = buildHorizontalLayout(view, { ...baseOptions, expandedRetrievalAgents: new Set<string>() });
  101. const expanded = buildHorizontalLayout(view, { ...baseOptions, expandedRetrievalAgents: new Set([agentId]) });
  102. const height = (nodes: typeof collapsed.nodes) => Number(nodes.find((node) => node.type === "branchFrame")?.style?.height || 0);
  103. expect(height(expanded.nodes)).toBeGreaterThan(height(collapsed.nodes));
  104. });
  105. });