runtime-v8.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { expect, test } from "@playwright/test";
  2. import { makeView } from "../fixtures";
  3. const view = makeView(2, 3, 2, "success");
  4. const artifactDetail = {
  5. businessSections: [{ id: "summary", title: "主脚本规模", content: "1 个段落、1 个元素、1 条关联" }],
  6. technical: { source: "read-only-test" },
  7. changes: { summary: "当前主脚本内容", sections: [], artifactRef: "artifact:base:current", exactness: "current-only" },
  8. scriptTable: {
  9. snapshotRef: "artifact:base:current",
  10. exactness: "current-only",
  11. sourceOrigin: "database",
  12. counts: { paragraphs: 1, elements: 1, links: 1 },
  13. maxDepth: 0,
  14. paragraphs: [{
  15. id: 1, rowId: 1, index: 1, level: 1, depth: 0, name: "机制拆解", contentRange: ["正文: 第二段"],
  16. sections: {
  17. theme: { label: "主题", description: "解释份额换订单", descriptionParts: [{ type: "text", text: "解释份额换订单" }], dimensions: [{ type: "主维度", name: "核心竞争力", value: "份额换订单" }] },
  18. form: { label: "形式", description: "案例拆解", descriptionParts: [{ type: "text", text: "案例拆解" }], dimensions: [] },
  19. function: { label: "作用", description: "承接上文", descriptionParts: [{ type: "text", text: "承接上文" }], dimensions: [] },
  20. feeling: { label: "感受", description: "建立可信度", descriptionParts: [{ type: "text", text: "建立可信度" }], dimensions: [] },
  21. },
  22. fullDescription: "通过份额换订单解释机制。",
  23. fullDescriptionParts: [{ type: "text", text: "通过" }, { type: "element", id: 10, name: "份额换订单", resolved: true, text: "[元素:10]" }, { type: "text", text: "解释机制。" }],
  24. }],
  25. elements: [{ id: 10, rowId: 10, name: "份额换订单", primaryDimension: "核心竞争力", secondaryDimension: "商业模式", paragraphs: [{ id: 1, index: 1, name: "机制拆解" }] }],
  26. },
  27. };
  28. test.beforeEach(async ({ page }) => {
  29. await page.route("**/api/**", async (route) => {
  30. const path = new URL(route.request().url()).pathname;
  31. if (path === "/api/script-builds") {
  32. await route.fulfill({ json: [{ id: view.header.id, status: "success", currentRound: 2, createdAt: view.header.createdAt }] });
  33. return;
  34. }
  35. if (path.endsWith("/execution-view")) {
  36. await route.fulfill({ json: view });
  37. return;
  38. }
  39. if (path.includes("/artifacts/")) {
  40. await route.fulfill({ json: artifactDetail });
  41. return;
  42. }
  43. if (path.includes("/decision-prompts/")) {
  44. await route.fulfill({ json: {
  45. promptRef: "prompt:main",
  46. actor: { role: "main", label: "主 Agent" },
  47. systemPrompt: {
  48. content: "当前主 Agent 规则正文。",
  49. source: "current-db",
  50. version: 8,
  51. accuracy: "current-not-run-snapshot",
  52. truncated: false,
  53. },
  54. notices: [{ code: "PROMPT_NOT_SNAPSHOTTED", message: "系统没有保存这次运行所用提示词的历史快照;下方展示当前提示词。" }],
  55. } });
  56. return;
  57. }
  58. if (path.includes("/inspector-view/")) {
  59. await route.fulfill({ json: {
  60. schemaVersion: "inspector-source-v2",
  61. detailRef: "event:test",
  62. cardKind: "test",
  63. completeness: "complete",
  64. businessProjection: {
  65. businessSections: [{ id: "summary", title: "业务记录", content: "这是按需读取的完整业务记录。" }],
  66. },
  67. modules: [{
  68. id: "summary",
  69. title: "业务记录",
  70. businessPath: "/businessSections/0",
  71. presentation: "text",
  72. rows: [{
  73. id: "summary:content",
  74. businessSelector: "/businessSections/0/content",
  75. bindingIds: ["summary:source:1"],
  76. }],
  77. bindings: [{
  78. id: "summary:source:1",
  79. sourceId: "event:test",
  80. role: "output",
  81. selector: { kind: "json-pointer", path: "/output/content" },
  82. transform: { kind: "direct" },
  83. evidence: { availability: "produced-by-run", adoption: "not-applicable", confidence: "exact" },
  84. resolution: "resolved",
  85. }],
  86. runtimeRefs: ["event:test"],
  87. defaultOpen: true,
  88. }],
  89. sources: {
  90. "event:test": {
  91. id: "event:test",
  92. kind: "runtime-event",
  93. label: "测试 Event",
  94. locator: { eventId: "test" },
  95. completeness: "complete",
  96. resultState: "present",
  97. truncated: false,
  98. selectedValues: [{ bindingId: "summary:source:1", value: "这是按需读取的完整业务记录。" }],
  99. rawRecord: { output: { content: "这是按需读取的完整业务记录。" } },
  100. },
  101. },
  102. notices: [],
  103. } });
  104. return;
  105. }
  106. await route.fulfill({
  107. json: {
  108. businessSections: [{ id: "summary", title: "业务记录", content: "这是按需读取的完整业务记录。" }],
  109. technical: { source: "read-only-test" },
  110. },
  111. });
  112. });
  113. await page.goto("/");
  114. });
  115. test("V8 在桌面与平板呈现发散、取数、取舍再收敛", async ({ page }, testInfo) => {
  116. test.skip(testInfo.project.name.includes("mobile"), "移动端使用轮次列表");
  117. await expect(page.getByRole("heading", { name: "脚本构建" })).toBeVisible();
  118. await expect(page.locator(".react-flow__minimap")).toHaveCount(0);
  119. await page.getByRole("button", { name: /展开本轮/ }).first().click();
  120. await expect(page.getByText("主 Agent · 规划", { exact: true })).toBeVisible();
  121. await expect(page.getByText("实现 Agent · 多路方案", { exact: true })).toBeVisible();
  122. await expect(page.getByText("多方案评审 Agent", { exact: true }).first()).toBeAttached();
  123. await expect(page.getByText("主 Agent · 收敛", { exact: true })).toBeAttached();
  124. await expect(page.getByText("整体评审 Agent", { exact: true }).first()).toBeAttached();
  125. await expect(page.getByTestId("rf__node-multipath-review-1-1").getByText("多方案评审", { exact: true })).toBeAttached();
  126. await expect(page.getByTestId("rf__node-multipath-1-1").getByText("第 1 批决策", { exact: true })).toBeAttached();
  127. await expect(page.getByTestId("rf__node-multipath-1-2").getByText("第 2 批决策", { exact: true })).toBeAttached();
  128. await page.getByRole("button", { name: /展开方案过程/ }).first().click();
  129. const canvas = page.locator(".flowShell");
  130. await expect(canvas.getByText("取数阶段", { exact: true }).first()).toBeAttached();
  131. await expect(canvas.getByText("工具取数", { exact: true }).first()).toBeAttached();
  132. await expect(canvas.getByText("Agent 取数", { exact: true }).first()).toBeAttached();
  133. await expect(canvas.getByText("数据取舍", { exact: true }).first()).toBeAttached();
  134. const details = page.getByRole("button", { name: /查看详情/ }).first();
  135. await expect(details).toBeVisible();
  136. await details.click();
  137. await expect(page.getByRole("dialog", { name: /三列来源详情/ })).toBeVisible();
  138. await expect(page.getByRole("columnheader", { name: "业务详情" })).toBeVisible();
  139. await expect(page.getByRole("columnheader", { name: "数据依据" })).toBeVisible();
  140. await expect(page.getByRole("columnheader", { name: "原始记录与运行" })).toBeVisible();
  141. });
  142. test("收敛卡在全量、紧凑和概览缩放下都不越出代理区域", async ({ page }, testInfo) => {
  143. test.skip(testInfo.project.name.includes("mobile"), "移动端使用轮次列表");
  144. await page.getByRole("button", { name: /展开本轮/ }).first().click();
  145. const assertContained = async (cardSelector: string, regionId: string) => {
  146. const region = page.locator(`[data-id="${regionId}"]`);
  147. const cards = page.locator(cardSelector);
  148. await expect(region).toBeAttached();
  149. await expect(cards.first()).toBeAttached();
  150. const regionBox = await region.boundingBox();
  151. expect(regionBox).not.toBeNull();
  152. for (let index = 0; index < await cards.count(); index += 1) {
  153. const box = await cards.nth(index).boundingBox();
  154. expect(box).not.toBeNull();
  155. expect(box!.y).toBeGreaterThanOrEqual(regionBox!.y - 1);
  156. expect(box!.y + box!.height).toBeLessThanOrEqual(regionBox!.y + regionBox!.height + 1);
  157. }
  158. };
  159. const assertConvergenceLayout = async () => {
  160. await assertContained('[data-id^="multipath-review-"]', "region:round:1:multipath-review");
  161. await assertContained('[data-id^="multipath-1-"]', "region:round:1:convergence");
  162. };
  163. await assertConvergenceLayout();
  164. await page.locator(".react-flow__controls-zoomout").click();
  165. await page.locator(".react-flow__controls-zoomout").click();
  166. await expect(page.locator(".flowShell")).toHaveAttribute("data-detail-level", "compact");
  167. await assertConvergenceLayout();
  168. await page.locator(".react-flow__controls-zoomout").click();
  169. await page.locator(".react-flow__controls-zoomout").click();
  170. await page.locator(".react-flow__controls-zoomout").click();
  171. await expect(page.locator(".flowShell")).toHaveAttribute("data-detail-level", "overview");
  172. await assertConvergenceLayout();
  173. await expect(page.locator(".react-flow__node-agentRegion")).toHaveCount(6);
  174. });
  175. test("Agent 规则抽屉明确区分当前规则与历史运行快照", async ({ page }, testInfo) => {
  176. test.skip(testInfo.project.name.includes("mobile"), "移动端由组件测试覆盖规则抽屉");
  177. const objective = page.getByTestId("rf__node-objective");
  178. await objective.getByRole("button", { name: "查看详情" }).click();
  179. const inspector = page.getByRole("dialog", { name: /三列来源详情$/ });
  180. await expect(inspector).toBeVisible();
  181. const trigger = inspector.getByRole("button", { name: "查看主 Agent 提示词" });
  182. await expect(trigger).toBeVisible();
  183. await trigger.click();
  184. const dialog = page.getByRole("dialog", { name: "Agent 任务与提示词" });
  185. await expect(dialog).toBeVisible();
  186. await expect(dialog.getByRole("heading", { name: "当前提示词" })).toBeVisible();
  187. await expect(dialog.getByText(/没有保存这次运行所用提示词的历史快照/)).toBeVisible();
  188. await expect(dialog.getByText("当前数据库版本 · v8")).toBeVisible();
  189. await page.keyboard.press("Escape");
  190. await expect(dialog).toHaveCount(0);
  191. await expect(inspector).toBeVisible();
  192. await expect(trigger).toBeFocused();
  193. });
  194. test("V8 移动端按轮次阅读并能展开取数树", async ({ page }, testInfo) => {
  195. test.skip(!testInfo.project.name.includes("mobile"), "仅验证移动端信息架构");
  196. await expect(page.getByRole("heading", { name: "这次构建经历了什么" })).toBeVisible();
  197. await page.getByRole("button", { name: "展开第 1 轮" }).click();
  198. await expect(page.getByText("候选方案 1", { exact: true })).toBeVisible();
  199. await page.getByRole("button", { name: "展开方案过程" }).first().click();
  200. await expect(page.getByText("取数阶段", { exact: true }).first()).toBeVisible();
  201. await expect(page.getByText("Agent 取数", { exact: true }).first()).toBeVisible();
  202. await expect(page.getByText("数据取舍", { exact: true }).first()).toBeVisible();
  203. const mobile = page.locator(".mobileTimeline");
  204. await expect(mobile.getByText("多方案评审 Agent · 评审判断", { exact: true }).first()).toBeVisible();
  205. await expect(mobile.getByText("评审建议", { exact: true }).first()).toBeVisible();
  206. await expect(mobile.getByText("主 Agent · 取舍决定", { exact: true }).first()).toBeVisible();
  207. await expect(mobile.getByText("最终决定", { exact: true }).first()).toBeVisible();
  208. await expect(mobile.getByText("整体评审 Agent · 评审判断", { exact: true }).first()).toBeVisible();
  209. });
  210. test("完整主脚本在桌面、平板和手机都使用独立可滚动查看器", async ({ page }) => {
  211. const trigger = page.getByRole("button", { name: "查看完整主脚本" });
  212. await expect(trigger).toHaveCount(1);
  213. await trigger.focus();
  214. await trigger.evaluate((element: HTMLElement) => element.click());
  215. const dialog = page.getByRole("dialog");
  216. await expect(dialog).toBeVisible();
  217. await expect(dialog.getByText("1 个段落 · 1 个元素 · 1 条关联")).toBeVisible();
  218. await expect(dialog.getByRole("columnheader", { name: "维度类型" }).first()).toBeVisible();
  219. await expect(dialog.getByText("正文: 第二段")).toBeVisible();
  220. await expect(dialog.getByText("主", { exact: true }).first()).toBeVisible();
  221. await dialog.getByRole("tab", { name: "脚本元素 1" }).click();
  222. await expect(dialog.getByRole("row", { name: /份额换订单/ })).toContainText("商业模式");
  223. const dimensions = await dialog.evaluate((element) => ({ width: element.getBoundingClientRect().width, viewport: window.innerWidth }));
  224. expect(dimensions.width).toBeLessThanOrEqual(dimensions.viewport);
  225. await dialog.getByRole("button", { name: "关闭完整主脚本" }).click();
  226. await expect(dialog).toHaveCount(0);
  227. await expect(trigger).toBeFocused();
  228. });