import { expect, test } from "@playwright/test"; const headers = { "X-WQ-Request": "1" }; const scope = { instrument_type: "EQUITY", region: "USA", universe: "TOP3000", delay: 1, }; test("native QuantFlow canvas saves versions, branches and restores artifacts", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (e) => errors.push(e.message)); await page.goto("/"); await page.getByLabel("密码", { exact: true }).fill("browser-test-password"); await page.getByRole("button", { name: "进入工作空间" }).click(); await page .getByRole("navigation", { name: "主导航" }) .getByRole("button", { name: "QuantFlow", exact: true }) .click(); await expect( page.getByRole("heading", { name: "QuantFlow", exact: true }), ).toBeVisible(); await page.request.put("/api/v1/account/credentials", { headers, data: { email: "test@example.com", password: "synthetic-password" }, }); await page.request.post("/api/v1/account/connect", { headers }); await expect .poll( async () => (await (await page.request.get("/api/v1/account")).json()) .connection_status, ) .toBe("connected"); const config = { base_url: "https://model.test/v1", model: "test-model" }; await page.request.put("/api/v1/ai/settings", { headers, data: { ...config, api_key: "synthetic-key" }, }); expect( ( await ( await page.request.post("/api/v1/ai/settings/test", { headers }) ).json() ).ready, ).toBe(true); await page.request.put("/api/v1/ai/settings", { headers, data: { ...config, enabled: true }, }); for (const dataset_id of [null, "TEST_FIN"]) { const job = await ( await page.request.post("/api/v1/catalog/sync-jobs", { headers, data: { scope, dataset_id }, }) ).json(); await expect .poll( async () => (await (await page.request.get(`/api/v1/sync-jobs/${job.id}`)).json()) .status, ) .toBe("completed"); } const params = new URLSearchParams( Object.entries(scope).map(([k, v]) => [k, String(v)]), ); const fields = await ( await page.request.get(`/api/v1/catalog/datasets/TEST_FIN/fields?${params}`) ).json(); const fixed = await ( await page.request.post("/api/v1/catalog/inputs", { headers, data: { scope, dataset_id: "TEST_FIN", collection_version: fields.collection_version, selection: "all", }, }) ).json(); expect( ( await page.request.post("/api/v1/catalog/operators/refresh", { headers }) ).ok(), ).toBe(true); expect( ( await page.request.post("/api/v1/catalog/setting-options/refresh", { headers, }) ).ok(), ).toBe(true); const template = await ( await page.request.post("/api/v1/research/assets", { headers, data: { kind: "template", content: { name: "画布测试模板", expression: "rank({field})", variables: { field: { kind: "field", field_type: "MATRIX", values: ["TEST_FIN_001", "TEST_FIN_002"], }, }, }, }, }) ).json(); await page.reload(); await page.getByLabel("流程名称", { exact: true }).fill("画布原生研究"); async function add(kind: string, label: string) { await page.getByLabel("新增节点类型").selectOption(kind); await page.getByRole("button", { name: "添加节点", exact: true }).click(); await page.getByLabel("节点名称", { exact: true }).fill(label); } await add("expand", "模板展开"); await page.getByLabel("节点素材", { exact: true }).selectOption(template.id); await add("backtest", "模拟比较"); await add("evaluate", "规则评估"); await add("condition", "分支选择"); await add("summarize", "通过汇总"); await add("summarize", "待核实汇总"); await add("summarize", "失败汇总"); async function connect(source: string, target: string, branch?: string) { await page.getByLabel("连线起点").selectOption({ label: source }); await page.getByLabel("连线终点").selectOption({ label: target }); if (branch) await page.getByLabel("连线条件").selectOption(branch); await page.getByRole("button", { name: "添加连线", exact: true }).click(); } await connect("固定输入", "模板展开"); await connect("模板展开", "模拟比较"); await connect("模拟比较", "规则评估"); await connect("规则评估", "分支选择"); await connect("分支选择", "通过汇总", "pass"); await connect("分支选择", "待核实汇总", "review"); await connect("分支选择", "失败汇总", "block"); // Native node movement remains keyboard accessible and is saved in the version. const input = page.getByRole("button", { name: "节点 固定输入", exact: true, }); await input.focus(); await input.press("ArrowRight"); await page.getByRole("button", { name: "校验连接", exact: true }).click(); await expect( page.getByText("连接校验通过。", { exact: false }), ).toBeVisible(); const savedResponse = page.waitForResponse( (r) => r.url().endsWith("/research/assets") && r.request().method() === "POST", ); await page.getByRole("button", { name: "保存流程版本", exact: true }).click(); const saved = await (await savedResponse).json(); expect(saved.content.nodes[0].x).toBe(50); await page.getByLabel("流程名称", { exact: true }).fill("画布原生研究 v2"); await expect( page.getByRole("button", { name: "启动已保存流程", exact: true }), ).toBeDisabled(); await page.getByRole("button", { name: "保存流程版本", exact: true }).click(); await expect(page.getByLabel("流程版本", { exact: true })).toHaveValue("2"); await page.getByRole("button", { name: "适应画布", exact: true }).click(); await page.screenshot({ path: "/tmp/wq-stage-four-canvas.png", fullPage: true, }); await page .getByRole("button", { name: "启动已保存流程", exact: true }) .click(); await page.getByLabel("自动研究假设").fill("画布条件分支与固定版本"); await page.getByRole("combobox", { name: "自动研究固定输入" }).click(); await page .getByRole("option") .filter({ hasText: fixed.id.slice(0, 8) }) .click(); await page.getByRole("heading", { name: "QuantFlow", exact: true }).click(); for (const [label, value] of [ ["最大轮数", "1"], ["最大模拟条目数", "2"], ["最大模型调用数", "1"], ["每轮候选上限", "2"], ]) await page.getByLabel(label, { exact: true }).fill(value); await page .getByRole("button", { name: "核对并启动研究", exact: true }) .click(); const dialog = page.getByRole("dialog", { name: "确认自动研究授权" }); await expect(dialog).toContainText("流程:画布原生研究 v2 · v2"); const started = page.waitForResponse( (r) => r.url().endsWith("/research/flows/runs") && r.request().method() === "POST", ); await dialog.getByRole("button", { name: "confirm", exact: true }).click(); const response = await started; expect(response.status()).toBe(201); const run = await response.json(); await expect .poll( async () => ( await ( await page.request.get(`/api/v1/research/flows/runs/${run.id}`) ).json() ).status, { timeout: 40000 }, ) .toBe("completed"); const completed = await ( await page.request.get(`/api/v1/research/flows/runs/${run.id}`) ).json(); expect(completed.model_calls_used).toBe(0); expect(completed.simulations_used).toBe(2); expect( completed.steps.filter((s: { status: string }) => s.status === "skipped"), ).toHaveLength(2); expect(completed.authorization.workflow.version).toBe(2); await page.reload(); await expect( page.getByRole("region", { name: "研究运行详情" }), ).toContainText("画布原生研究 v2"); await page .getByRole("button", { name: "节点 规则评估", exact: true }) .click(); await expect( page.getByRole("heading", { name: "所选节点产物" }), ).toBeVisible(); await page.screenshot({ path: "/tmp/wq-stage-four-run.png", fullPage: true }); expect(errors).toEqual([]); });