import { choosePreparation } from "./preparation-helpers"; import { expect, test } from "@playwright/test"; // UI contract fixtures: no external model call or simulation is started by these tests. const fixed = { id: "semi-fixed-input", name: "Semi 准备集合", note: "", version: 1, field_count: 1, dataset_count: 1, dataset_ids: ["TEST_FIN"], fields: [], preparation_ref: { id: "semi-fixed-input", version: 1 }, field_ids: ["TEST_FIN_001"], field_types: { TEST_FIN_001: "MATRIX" }, collection_version: "fixed-v1", scope: { instrument_type: "EQUITY", region: "USA", universe: "TOP3000", delay: 1, }, }; for (const mode of ["templates", "variants", "features"] as const) { test(`${mode}: AI generation keeps its hypothesis, fixed inputs and failure draft`, async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await page.route("**/api/v1/data-preparations?*", (route) => route.fulfill({ json: { items: [fixed], total: 1, limit: 25, offset: 0 }, }), ); await page.route( "**/api/v1/data-preparations/semi-fixed-input/selection?*", (route) => route.fulfill({ json: fixed }), ); await page.route( "**/api/v1/research/input-snapshots/semi-fixed-input", (route) => route.fulfill({ json: { ...fixed, preparation_ref: undefined } }), ); await page.route( "**/api/v1/research/assets/semi-generated/versions", (route) => route.fulfill({ json: [{ version: 1 }] }), ); await page.goto(`/#${mode}`); await page .getByLabel("密码", { exact: true }) .fill("browser-test-password"); await page.getByRole("button", { name: "进入工作空间" }).click(); const feature = mode === "features"; await page .getByRole("button", { name: feature ? "新建特征方案" : "新建", exact: true, }) .click(); const sheet = page.locator(".research-detail:visible"); const input = sheet.getByRole("textbox", { name: feature ? "特征经济假设" : "本次研究假设", exact: true, }); const generate = sheet.getByRole("button", { name: feature ? "AI 生成新方案" : "AI 生成模板", exact: true, }); await expect(generate).toBeDisabled(); await choosePreparation( page, fixed.name, feature ? "选择特征数据准备" : "选择数据准备", ); await input.click(); if (mode === "variants") await sheet.getByLabel("种子 Alpha", { exact: true }).fill("TEST0001"); const hypothesis = "检验 <收益> 与规模\n第二轮保留原始假设"; await input.fill(hypothesis); await expect( sheet.locator("p:visible").filter({ hasText: /Semi 准备集合 ·/ }), ).toBeVisible(); let release!: () => void; const pending = new Promise((resolve) => { release = resolve; }); let requestBody: Record | undefined; await page.route("**/api/v1/research/generate", async (route) => { requestBody = route.request().postDataJSON(); await pending; const template = { name: "AI 生成验收", description: "", expression: "rank({field})", category: "template", variables: { field: { kind: "field", field_type: "MATRIX", values: ["TEST_FIN_001"], }, }, }; await route.fulfill({ json: { id: "semi-generated", kind: feature ? "feature" : "template", name: "AI 生成验收", version: 1, content: feature ? { name: "AI 生成验收", hypothesis, input_ids: [fixed.id], steps: [ { name: "排名", rationale: "降低尺度差异", expression: "rank(TEST_FIN_001)", }, ], template, } : template, }, }); }); await input.press("Shift+Enter"); await expect .poll(() => requestBody) .toMatchObject({ hypothesis, input_ids: [], preparation_refs: [fixed.preparation_ref], method: feature ? "feature" : mode === "variants" ? "structure" : "template", }); if (mode === "variants") expect(requestBody?.parent_alpha_ids).toEqual(["TEST0001"]); await expect(generate).toBeDisabled(); await expect(input).toContainText("第二轮保留原始假设"); release(); await expect( sheet.getByRole("textbox", { name: "模板名称", exact: true }), ).toHaveValue("AI 生成验收"); await expect(generate).toBeEnabled(); if (mode === "templates") { await sheet.getByRole("button", { name: "下一步:回测准备" }).click(); await expect( sheet.getByRole("textbox", { name: "回测研究假设", exact: true }), ).toHaveValue(hypothesis); await expect( sheet.getByRole("button", { name: "生成候选研究集合", exact: true }), ).toBeVisible(); await expect(generate).toBeHidden(); await sheet .getByRole("tab", { name: "模板信息编辑", exact: true }) .click(); await expect( sheet.getByRole("textbox", { name: "模板名称", exact: true }), ).toHaveValue("AI 生成验收"); } await expect(input).toContainText("检验 <收益> 与规模"); await page.screenshot({ path: `../output/playwright/semi-ai-${mode}.png` }); await page.setViewportSize({ width: 390, height: 900 }); await expect(generate).toBeInViewport(); expect(await sheet.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe( true, ); await page.screenshot({ path: `../output/playwright/semi-ai-${mode}-390.png`, }); if (mode === "templates") { await page.evaluate(() => document.body.setAttribute("theme-mode", "dark"), ); await page.screenshot({ path: "../output/playwright/semi-ai-templates-dark.png", }); await page.evaluate(() => document.body.setAttribute("theme-mode", "light"), ); } await page.route("**/api/v1/research/generate", (route) => route.fulfill({ status: 503, json: { detail: "模拟模型暂时不可用" } }), ); await generate.click(); await expect( sheet.getByText("模拟模型暂时不可用", { exact: true }), ).toBeVisible(); await expect(input).toContainText("第二轮保留原始假设"); await expect(generate).toBeEnabled(); await input.fill("a".repeat(10001)); await expect(generate).toBeDisabled(); await expect( sheet.getByRole("alert").filter({ hasText: "10,000" }), ).toBeVisible(); expect(errors).toEqual([]); }); }