feat: 使用 Semi AI 组件升级研究助手与研究入口
This commit is contained in:
@@ -93,7 +93,9 @@ test("model settings, text stream, contextual query, confirmation and preserved
|
||||
await page.getByRole("button", { name: "切换研究助手" }).click();
|
||||
const chat = page.getByRole("complementary", { name: "AI 研究助手" });
|
||||
await chat.getByRole("button", { name: "新会话", exact: true }).click();
|
||||
await expect(chat.locator(".ai-context")).toContainText("TEST0001");
|
||||
await expect(chat.locator(".research-ai-reference")).toContainText(
|
||||
"TEST0001",
|
||||
);
|
||||
const input = chat.getByRole("textbox", { name: "发送给研究助手" });
|
||||
await input.fill("修改当前 Alpha 的备注");
|
||||
await chat.getByRole("button", { name: "发送", exact: true }).click();
|
||||
@@ -263,7 +265,7 @@ test("Lark workspace keeps pagination, account details and chat usable at every
|
||||
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
||||
await page.getByRole("button", { name: "用户菜单", exact: true }).click();
|
||||
await page.getByRole("button", { name: "查看个人信息", exact: true }).click();
|
||||
await expect(input).toHaveValue("保留聊天草稿");
|
||||
await expect(input).toHaveText("保留聊天草稿");
|
||||
await chat.getByRole("button", { name: "收起研究助手" }).click();
|
||||
await page.getByRole("button", { name: "配置", exact: true }).click();
|
||||
await expect(
|
||||
@@ -406,3 +408,57 @@ test("Markdown streams incomplete code, finishes safely and restores history", a
|
||||
await expect(chat.getByRole("cell", { name: "1.2" })).toBeVisible();
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("AI composer preserves literal text, IME input and drafts while generating", async ({
|
||||
page,
|
||||
}) => {
|
||||
await login(page);
|
||||
await configure(page);
|
||||
await page.getByRole("button", { name: "切换研究助手" }).click();
|
||||
const chat = page.getByRole("complementary", { name: "AI 研究助手" });
|
||||
await chat.getByRole("button", { name: "新会话", exact: true }).click();
|
||||
const input = chat.getByRole("textbox", { name: "发送给研究助手" });
|
||||
await expect(input.locator("p").first()).toHaveAttribute(
|
||||
"data-placeholder",
|
||||
"描述研究问题或要执行的操作…",
|
||||
);
|
||||
const sent: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (
|
||||
request.method() === "POST" &&
|
||||
/\/ai\/conversations\/[^/]+\/runs$/.test(request.url())
|
||||
)
|
||||
sent.push(request.postDataJSON().message);
|
||||
});
|
||||
await chat
|
||||
.getByRole("button", { name: "查询当前研究对象与已有结果", exact: true })
|
||||
.click();
|
||||
await expect(input).toHaveText("查询当前研究对象与已有结果");
|
||||
expect(sent).toEqual([]);
|
||||
await input.fill("SLOW <字段>");
|
||||
await input.dispatchEvent("keydown", {
|
||||
key: "Enter",
|
||||
code: "Enter",
|
||||
isComposing: true,
|
||||
});
|
||||
expect(sent).toEqual([]);
|
||||
await input.press("Shift+Enter");
|
||||
await input.pressSequentially("next");
|
||||
await input.press("Enter");
|
||||
await expect.poll(() => sent).toEqual(["SLOW <字段>\nnext"]);
|
||||
await expect(chat.getByRole("button", { name: "停止生成" })).toBeVisible();
|
||||
await input.fill("下一轮 <收益> 假设");
|
||||
await chat.getByRole("button", { name: "停止生成" }).click();
|
||||
await expect(chat.locator(".ai-run-state")).toContainText("已停止");
|
||||
await expect(input).toHaveText("下一轮 <收益> 假设");
|
||||
await input.fill("a".repeat(20001));
|
||||
await expect(
|
||||
chat.getByRole("button", { name: "发送", exact: true }),
|
||||
).toBeDisabled();
|
||||
await input.press("Enter");
|
||||
expect(sent).toHaveLength(1);
|
||||
await input.fill("");
|
||||
await expect(
|
||||
chat.getByRole("button", { name: "发送", exact: true }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -139,6 +139,8 @@ test("fixed input, AI approval, backtest and research source remain connected",
|
||||
await inputSheet
|
||||
.getByRole("button", { name: "用此输入研究", exact: true })
|
||||
.click();
|
||||
await expect(chat.locator(".ai-context")).toContainText("固定研究输入");
|
||||
await expect(
|
||||
chat.locator(".research-ai-reference").filter({ hasText: "固定输入" }),
|
||||
).toContainText(input.id);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
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",
|
||||
dataset_id: "TEST_FIN",
|
||||
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/research/inputs", (route) =>
|
||||
route.fulfill({ json: { items: [fixed] } }),
|
||||
);
|
||||
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 sheet
|
||||
.getByRole("combobox", {
|
||||
name: feature ? "特征固定输入" : "固定研究输入",
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
await page
|
||||
.getByRole("option")
|
||||
.filter({ hasText: fixed.id.slice(0, 8) })
|
||||
.click();
|
||||
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(".research-ai-reference").filter({ hasText: fixed.id }),
|
||||
).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: [fixed.id],
|
||||
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();
|
||||
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([]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user