data-flow.test.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { fireEvent, render, screen } from "@testing-library/react";
  2. import { afterEach, describe, expect, it, vi } from "vitest";
  3. import { DataFlowRenderer } from "@/components/inspector/DataFlowRenderer";
  4. import { Inspector } from "@/components/inspector/Inspector";
  5. import { getCardData } from "@/lib/api";
  6. import { finalResultItem, storyItem } from "@/lib/layout";
  7. import type { CardBusinessData } from "@/lib/types";
  8. import { makeStory, makeView } from "@/tests/fixtures";
  9. function makeCardData(overrides: Partial<CardBusinessData> = {}): CardBusinessData {
  10. return {
  11. cardKind: "data-decision",
  12. relationKind: "business-record",
  13. completeness: "partial",
  14. businessInputs: [{
  15. id: "input:cases",
  16. label: "候选案例",
  17. value: "案例 A、案例 B",
  18. source: { kind: "database", label: "取数结果", ref: "retrieval-agent:3786", fieldPath: "output.cases" },
  19. relation: "available-upstream",
  20. completeness: "complete",
  21. }],
  22. runtime: {
  23. summary: "实现 Agent 根据可用材料形成取舍记录。",
  24. units: [{ id: "event:3786", label: "解构 Case Agent", status: "completed", durationMs: 1250, summary: "返回两个候选案例。", input: "查找相关案例", output: { count: 2 }, eventRef: "event:3786", completeness: "complete" }],
  25. calculation: "按同一分支和时间顺序聚合,不使用文本相似度补链。",
  26. },
  27. businessOutputs: [{
  28. id: "output:decision",
  29. label: "明确取舍",
  30. value: "采用案例 A,舍弃案例 B。",
  31. source: { kind: "database", label: "数据取舍记录", ref: "data-decision:115" },
  32. relation: "persisted-output",
  33. completeness: "complete",
  34. }],
  35. displayUse: {
  36. businessModules: [{ id: "decision", title: "明确取舍", sourceIds: ["output:decision"] }],
  37. cardFields: [{ key: "headline", label: "取舍结论", sourceIds: ["output:decision"], transform: "直接使用结构化结论" }],
  38. unusedSourceIds: ["input:cases"],
  39. },
  40. notices: [{ level: "warning", message: "上游材料存在,但运行记录没有证明它被本次决策采用。" }],
  41. ...overrides,
  42. };
  43. }
  44. describe("card data flow", () => {
  45. afterEach(() => vi.unstubAllGlobals());
  46. it("keeps the old Inspector focused on business, changes and technical detail", () => {
  47. const item = storyItem(makeStory("candidate", "candidate-output", "result", { hasChanges: true }));
  48. const detail = { businessSections: [], changes: { sections: [], exactness: "exact" }, technical: {} };
  49. render(<Inspector item={item} tab="business" onTab={vi.fn()} onClose={vi.fn()} detail={detail} />);
  50. expect(screen.getAllByRole("tab").map((tab) => tab.textContent)).toEqual(["业务详情", "产出与改动", "技术详情"]);
  51. });
  52. it("renders the vertical lineage, explicit uncertainty and display mappings", () => {
  53. render(<DataFlowRenderer data={makeCardData()} />);
  54. expect(screen.getByRole("heading", { name: "数据关系与完整性" })).toBeInTheDocument();
  55. expect(screen.getByText("业务记录")).toBeInTheDocument();
  56. expect(screen.getByText("部分记录")).toBeInTheDocument();
  57. expect(screen.getByRole("heading", { name: "业务输入" })).toBeInTheDocument();
  58. expect(screen.getByRole("heading", { name: "运行过程 / I/O" })).toBeInTheDocument();
  59. expect(screen.getByRole("heading", { name: "业务输出" })).toBeInTheDocument();
  60. expect(screen.getByRole("heading", { name: "页面如何使用这些数据" })).toBeInTheDocument();
  61. expect(screen.getByText("可确认是上游信息,但无法确认是否被本次决策采用")).toBeInTheDocument();
  62. expect(screen.getAllByText("数据库")).toHaveLength(2);
  63. expect(screen.getByText("直接使用结构化结论")).toBeInTheDocument();
  64. expect(screen.getAllByText("F2")).toHaveLength(3);
  65. });
  66. it("uses keyboard-friendly disclosures for runtime units and long values", () => {
  67. const longText = "完整业务正文。".repeat(150);
  68. render(<DataFlowRenderer data={makeCardData({ businessOutputs: [{
  69. id: "output:decision",
  70. label: "完整正文",
  71. value: longText,
  72. source: { kind: "artifact", label: "候选快照" },
  73. relation: "persisted-output",
  74. completeness: "complete",
  75. }] })} />);
  76. const runtimeSummary = screen.getByText("解构 Case Agent").closest("summary");
  77. expect(runtimeSummary).toHaveAttribute("aria-expanded", "false");
  78. fireEvent.click(runtimeSummary!);
  79. expect(runtimeSummary).toHaveAttribute("aria-expanded", "true");
  80. expect(screen.getByText("输入")).toBeInTheDocument();
  81. const longSummary = screen.getByText("展开完整内容").closest("summary");
  82. expect(longSummary).toHaveAttribute("aria-expanded", "false");
  83. expect(screen.queryByText(longText)).not.toBeInTheDocument();
  84. fireEvent.click(longSummary!);
  85. expect(screen.getByText("收起完整内容").closest("summary")).toHaveAttribute("aria-expanded", "true");
  86. expect(screen.getByText(longText)).toBeInTheDocument();
  87. });
  88. it("shows dedicated loading, error and missing-reference states", () => {
  89. const { rerender } = render(<DataFlowRenderer loading />);
  90. expect(screen.getByLabelText("正在加载数据流")).toBeInTheDocument();
  91. rerender(<DataFlowRenderer error="API 503" />);
  92. expect(screen.getByText("数据流暂时不可用")).toBeInTheDocument();
  93. expect(screen.getByText("API 503")).toBeInTheDocument();
  94. rerender(<DataFlowRenderer missingRef />);
  95. expect(screen.getByText("没有稳定的数据定位")).toBeInTheDocument();
  96. });
  97. it("requests card data with the run and encoded detail reference", async () => {
  98. const payload = makeCardData();
  99. const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => payload });
  100. vi.stubGlobal("fetch", fetchMock);
  101. await expect(getCardData("443", "run:final-result")).resolves.toEqual(payload);
  102. expect(fetchMock).toHaveBeenCalledWith(
  103. "http://127.0.0.1:8788/api/script-builds/443/card-data/run%3Afinal-result",
  104. expect.objectContaining({ cache: "no-store" }),
  105. );
  106. });
  107. it("uses an independent final-result detail while preserving the artifact action", () => {
  108. const view = makeView();
  109. const item = finalResultItem(view);
  110. const onArtifact = vi.fn();
  111. expect(item.detailRef).toBe("run:final-result");
  112. render(<Inspector item={item} tab="business" onTab={vi.fn()} onClose={vi.fn()} onArtifact={onArtifact} detail={{ businessSections: [], technical: {} }} />);
  113. fireEvent.click(screen.getByRole("button", { name: "查看当前主脚本" }));
  114. expect(onArtifact).toHaveBeenCalledOnce();
  115. expect(view.finalResult.artifact.snapshotRef).toBe("artifact:base:current");
  116. });
  117. });