cards-v8.test.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import { fireEvent, render, screen } from "@testing-library/react";
  2. import { ReactFlowProvider } from "@xyflow/react";
  3. import { describe, expect, it, vi } from "vitest";
  4. import { CandidateCard } from "@/components/flow/CandidateCard";
  5. import { RoundSummaryCard } from "@/components/flow/RoundSummaryCard";
  6. import { StoryCard } from "@/components/flow/StoryCard";
  7. import { RunResultCard } from "@/components/flow/RunResultCard";
  8. import { AgentDecisionCard } from "@/components/decision/AgentDecisionCard";
  9. import { MobileTimeline } from "@/components/workbench/MobileTimeline";
  10. import { branchSummaryItem, finalResultItem, multipathDecisionItem, multipathReviewItem, roundSummaryItem, storyItem } from "@/lib/layout";
  11. import { makeBranch, makeRound, makeStory, makeView } from "@/tests/fixtures";
  12. const base = { onOpen: vi.fn(), onToggleRound: vi.fn(), onToggleBranch: vi.fn() };
  13. const nodeProps = { selected: false, zIndex: 1, isConnectable: false, positionAbsoluteX: 0, positionAbsoluteY: 0, dragging: false, draggable: true, selectable: true, deletable: false };
  14. const wrap = (node: React.ReactNode) => render(<ReactFlowProvider>{node}</ReactFlowProvider>);
  15. describe("V8 business cards", () => {
  16. it("labels the creation objective action as details while keeping decision actions explicit", () => {
  17. const objective = storyItem(makeView().objective);
  18. const { unmount } = wrap(<AgentDecisionCard id="objective" type="agentDecision" {...nodeProps} data={{ ...base, item: objective }} />);
  19. expect(screen.getByRole("button", { name: "查看详情" })).toBeInTheDocument();
  20. expect(screen.queryByRole("button", { name: "查看决策" })).not.toBeInTheDocument();
  21. unmount();
  22. const dataDecision = storyItem(makeStory("data", "data-decision"), 1, 1);
  23. wrap(<AgentDecisionCard id="data" type="agentDecision" {...nodeProps} data={{ ...base, item: dataDecision }} />);
  24. expect(screen.getByRole("button", { name: "查看决策" })).toBeInTheDocument();
  25. });
  26. it("shows implementer data decisions as data decisions", () => {
  27. const item = storyItem(makeStory("data", "data-decision"), 1, 1);
  28. wrap(<AgentDecisionCard id="data" type="agentDecision" {...nodeProps} data={{ ...base, item }} />);
  29. expect(screen.getByText("数据取舍")).toBeInTheDocument();
  30. expect(screen.getByText("实现 Agent")).toBeInTheDocument();
  31. });
  32. it("distinguishes domain-information output from candidate script", () => {
  33. const branch = makeBranch(1, 4);
  34. wrap(<CandidateCard id="branch" type="candidateCard" {...nodeProps} data={{ ...base, item: branchSummaryItem(branch, 1), expanded: false }} />);
  35. expect(screen.getByText(/领域信息/)).toBeInTheDocument();
  36. expect(screen.getByText("领域事实")).toBeInTheDocument();
  37. expect(screen.getByRole("button", { name: /展开方案过程/ })).toBeInTheDocument();
  38. expect(screen.getByRole("button", { name: /查看方案汇总/ })).toBeInTheDocument();
  39. expect(screen.queryByRole("button", { name: "查看完整候选脚本" })).not.toBeInTheDocument();
  40. });
  41. it("opens the same artifact viewer from collapsed and expanded candidate output", () => {
  42. const onOpen = vi.fn();
  43. const branch = makeBranch(1, 1);
  44. const summaryItem = branchSummaryItem(branch, 1);
  45. const { unmount } = wrap(<CandidateCard id="branch" type="candidateCard" {...nodeProps} data={{ ...base, onOpen, item: summaryItem, expanded: false }} />);
  46. fireEvent.click(screen.getByRole("button", { name: "查看完整候选脚本" }));
  47. expect(onOpen).toHaveBeenCalledWith(summaryItem, "artifact");
  48. unmount();
  49. onOpen.mockClear();
  50. const creativeOutput = makeStory("output", "candidate-output", "decision", { artifactRef: "artifact:branch:1" });
  51. const outputItem = storyItem(creativeOutput, 1, 1);
  52. wrap(<AgentDecisionCard id="output" type="agentDecision" {...nodeProps} data={{ ...base, onOpen, onPrompt: vi.fn(), item: outputItem }} />);
  53. fireEvent.click(screen.getByRole("button", { name: "查看完整候选脚本" }));
  54. expect(onOpen).toHaveBeenCalledWith(outputItem, "artifact");
  55. expect(screen.queryByRole("button", { name: "查看实现任务与提示词" })).not.toBeInTheDocument();
  56. });
  57. it("keeps the candidate artifact action on an expanded mobile creative decision", () => {
  58. const view = makeView(1, 1, 1);
  59. const branch = view.rounds[0].branches[0];
  60. branch.output.node = makeStory("mobile-output", "candidate-output", "decision", { artifactRef: "artifact:branch:1" });
  61. const onOpen = vi.fn();
  62. render(<MobileTimeline
  63. view={view}
  64. expandedRounds={new Set([1])}
  65. expandedBranches={new Set(["1:1"])}
  66. expandedRetrievalAgents={new Set()}
  67. onToggleRound={vi.fn()}
  68. onToggleBranch={vi.fn()}
  69. onToggleRetrievalAgent={vi.fn()}
  70. onOpen={onOpen}
  71. onCanvas={vi.fn()}
  72. />);
  73. const artifactButtons = screen.getAllByRole("button", { name: "查看完整候选脚本" });
  74. expect(artifactButtons).toHaveLength(2);
  75. fireEvent.click(artifactButtons[1]);
  76. expect(onOpen).toHaveBeenCalledWith(expect.objectContaining({ id: "mobile-output" }), "artifact");
  77. expect(screen.queryByText("查看实现任务与提示词")).not.toBeInTheDocument();
  78. });
  79. it("keeps runtime association in technical detail instead of the business card", () => {
  80. const story = makeStory("evaluation", "round-evaluation");
  81. story.sourceNotice = "runtime-associated";
  82. story.detailRef = "event:987";
  83. wrap(<StoryCard id="evaluation" type="storyCard" {...nodeProps} data={{ ...base, item: storyItem(story, 1) }} />);
  84. expect(screen.queryByText("运行记录关联")).not.toBeInTheDocument();
  85. expect(screen.queryByText("event:987")).not.toBeInTheDocument();
  86. });
  87. it("keeps objective and step summaries visible on the mobile timeline", () => {
  88. const view = makeView(1, 1, 1);
  89. view.objective.card = {
  90. primary: { label: "核心目标", value: "形成清晰可执行的创作方向" },
  91. secondary: [
  92. { key: "inputs", label: "决策前读取", value: "选题 · 账号段落模式 · 账号人设" },
  93. { key: "shape", label: "目标结构", value: "2 个创作目标 · 1 个领域评估维度" },
  94. ],
  95. };
  96. view.objective.decision!.card = view.objective.card;
  97. view.rounds[0].plan.node.card = {
  98. primary: { label: "实现方式", value: "分工 · 2 路实现" },
  99. secondary: [
  100. { key: "paths", label: "各路任务", value: "方案 1:补充步骤;方案 2:增强信息" },
  101. { key: "reason", label: "规划理由", value: "两个任务没有强依赖" },
  102. ],
  103. };
  104. view.rounds[0].plan.node.decision!.card = view.rounds[0].plan.node.card;
  105. render(<MobileTimeline
  106. view={view}
  107. expandedRounds={new Set([1])}
  108. expandedBranches={new Set()}
  109. expandedRetrievalAgents={new Set()}
  110. onToggleRound={vi.fn()}
  111. onToggleBranch={vi.fn()}
  112. onToggleRetrievalAgent={vi.fn()}
  113. onOpen={vi.fn()}
  114. onCanvas={vi.fn()}
  115. />);
  116. expect(screen.getByText("选题 · 账号段落模式 · 账号人设")).toBeInTheDocument();
  117. expect(screen.getByText("2 个创作目标 · 1 个领域评估维度")).toBeInTheDocument();
  118. expect(screen.getByText("方案 1:补充步骤;方案 2:增强信息")).toBeInTheDocument();
  119. expect(screen.getByText("两个任务没有强依赖")).toBeInTheDocument();
  120. expect(screen.getAllByText("创作目标")).toHaveLength(1);
  121. expect(screen.getAllByText("本轮目标")).toHaveLength(1);
  122. expect(screen.getAllByText("实现规划")).toHaveLength(1);
  123. });
  124. it("keeps candidate review and main decision as two distinct cards", () => {
  125. const batch = makeRound(1, 2, 1).convergenceBatches[0];
  126. const { unmount } = wrap(<AgentDecisionCard id="review" type="agentDecision" {...nodeProps} data={{ ...base, item: multipathReviewItem(batch.review!, 1) }} />);
  127. expect(screen.getByText("多方案评审 Agent")).toBeInTheDocument();
  128. expect(screen.getAllByText("评审建议").length).toBeGreaterThanOrEqual(1);
  129. unmount();
  130. wrap(<AgentDecisionCard id="decision" type="agentDecision" {...nodeProps} data={{ ...base, item: multipathDecisionItem(batch.decision!, 1) }} />);
  131. expect(screen.getByText("主 Agent")).toBeInTheDocument();
  132. expect(screen.getAllByText("最终决定").length).toBeGreaterThanOrEqual(1);
  133. expect(screen.getByText("方案 1、方案 2")).toBeInTheDocument();
  134. });
  135. it("describes a stopped open branch as incomplete process instead of missing data", () => {
  136. const branch = makeBranch(1, 1);
  137. branch.status = "open";
  138. branch.outcome.status = "open";
  139. branch.outcome.label = "构建停止时尚未处置";
  140. branch.outcome.sourceNotice = "process-incomplete";
  141. wrap(<CandidateCard id="branch" type="candidateCard" {...nodeProps} data={{ ...base, item: branchSummaryItem(branch, 1), expanded: false }} />);
  142. expect(screen.getByText("尚未处置")).toBeInTheDocument();
  143. expect(screen.getByText("流程未完成")).toBeInTheDocument();
  144. expect(screen.queryByText("记录缺失")).not.toBeInTheDocument();
  145. });
  146. it("keeps round process and detail actions visible", () => {
  147. const onOpen = vi.fn();
  148. const round = makeRound(1, 3, 2);
  149. const item = roundSummaryItem(round);
  150. wrap(<RoundSummaryCard id="round" type="roundSummary" {...nodeProps} data={{ ...base, onOpen, item }} />);
  151. expect(screen.getByRole("button", { name: /展开本轮/ })).toBeInTheDocument();
  152. expect(screen.getByRole("button", { name: /查看详情/ })).toBeInTheDocument();
  153. fireEvent.click(screen.getByRole("button", { name: "查看本轮产出脚本表" }));
  154. expect(onOpen).toHaveBeenCalledWith(item, "artifact");
  155. expect(screen.getByText("本轮目标 1")).toBeInTheDocument();
  156. expect(screen.getByText("3 项具体任务 · 完整内容见详情")).toBeInTheDocument();
  157. expect(screen.getByText("分工 · 3 个实现方案")).toBeInTheDocument();
  158. expect(screen.getByText("2 个决策批次 · 涉及方案 1、2、3")).toBeInTheDocument();
  159. expect(screen.getByText("形成第 1 轮内容产出")).toBeInTheDocument();
  160. });
  161. it("opens the round artifact from the expanded round result", () => {
  162. const onOpen = vi.fn();
  163. const result = makeStory("round:1:result", "round-result", "result", { artifactRef: "artifact:round:1" });
  164. const item = storyItem(result, 1);
  165. wrap(<StoryCard id="round-result" type="storyCard" {...nodeProps} data={{ ...base, onOpen, item }} />);
  166. fireEvent.click(screen.getByRole("button", { name: "查看本轮产出脚本表" }));
  167. expect(onOpen).toHaveBeenCalledWith(item, "artifact");
  168. });
  169. it("labels a completed round without an overall evaluation explicitly", () => {
  170. const result = makeStory("round:1:result", "round-result", "result", { status: "not-evaluated" });
  171. wrap(<StoryCard id="round-result" type="storyCard" {...nodeProps} data={{ ...base, item: storyItem(result, 1) }} />);
  172. expect(screen.getByText("未整体评审")).toBeInTheDocument();
  173. expect(screen.queryByText("尚无结论")).not.toBeInTheDocument();
  174. });
  175. it("uses the run-aware decision summary instead of a generic missing-record label", () => {
  176. const round = makeRound(1, 2, 0);
  177. round.summary.decision.summary = "构建停止前尚未形成多路决策";
  178. wrap(<RoundSummaryCard id="round" type="roundSummary" {...nodeProps} data={{ ...base, item: roundSummaryItem(round) }} />);
  179. expect(screen.getByText("构建停止前尚未形成多路决策")).toBeInTheDocument();
  180. expect(screen.queryByText("未记录主 Agent 多路决策")).not.toBeInTheDocument();
  181. });
  182. it("opens the dedicated artifact viewer from the final result", () => {
  183. const onOpen = vi.fn();
  184. const view = makeView();
  185. view.finalResult.summary = "脚本构建终结:已经完成结构、内容和表达检查。\n1. 脚本结构概述:四个段落已经形成完整闭环。";
  186. const item = finalResultItem(view);
  187. wrap(<RunResultCard id="final" type="finalResult" {...nodeProps} data={{ ...base, onOpen, item }} />);
  188. expect(screen.getByText("当前主脚本已经形成")).toBeInTheDocument();
  189. expect(screen.getByText("构建摘要")).toBeInTheDocument();
  190. expect(screen.getByText("3", { selector: ".artifactStats b" })).toBeInTheDocument();
  191. fireEvent.click(screen.getByRole("button", { name: "查看结果详情" }));
  192. expect(onOpen).toHaveBeenCalledWith(item, "business");
  193. onOpen.mockClear();
  194. fireEvent.click(screen.getByRole("button", { name: "查看完整主脚本" }));
  195. expect(onOpen).toHaveBeenCalledWith(item, "artifact");
  196. onOpen.mockClear();
  197. fireEvent.keyDown(screen.getByRole("button", { name: "查看完整主脚本" }), { key: "Enter" });
  198. expect(onOpen).not.toHaveBeenCalledWith(item, "business");
  199. });
  200. });