205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
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<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
let requestBody: Record<string, unknown> | 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([]);
|
|
});
|
|
}
|