- 载入定义时以“用户消息+助手确认”形式把名称、含义、范围、缺失处理和表达式 写入对话,后续“把刚才公式改成乘以 100”等指代可关联到已载入定义; 生成前仍需显式确认,不静默沿用旧定义 - 选择空选项时同步清理当前定义与上下文,选择状态与预览保持一致 - 服务端单条消息长度上限 1000→2000 以容纳完整定义上下文,超长仍拒绝 - E2E 覆盖:载入后请求包含原定义、修改后表达式为 (收入−成本)×100、 空选项清理;单测覆盖 2000/2001 边界
167 lines
7.5 KiB
TypeScript
167 lines
7.5 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import fs from "node:fs";
|
||
|
||
const password =
|
||
process.env.E2E_PASSWORD ||
|
||
fs.readFileSync("var/local-access.txt", "utf8").match(/密码:(.+)/)![1];
|
||
const csv = (customerCode: string) =>
|
||
`期间,科室,学科,医生等级,医生,产品一级分类,实际收入,成本,毛利贡献,客户编码\n2025-08,测试儿保,普儿,高级,医生A,医疗服务,10000,4000,6000,${customerCode}\n2026-08,测试儿保,普儿,高级,医生A,医疗服务,5000,4000,3000,${customerCode}\n`;
|
||
|
||
test("新增列:补充信息触发追问并重新确认后生成公式工作簿", async ({ page }) => {
|
||
await page.setViewportSize({ width: 1280, height: 900 });
|
||
await page.goto("/");
|
||
await page.getByLabel("用户名", { exact: true }).fill("admin");
|
||
await page.getByLabel("密码", { exact: true }).fill(password);
|
||
await page.getByRole("button", { name: "登录", exact: true }).click();
|
||
await expect(page.getByRole("button", { name: "导入新表格" })).toBeVisible();
|
||
|
||
const uploaded = page.waitForResponse(
|
||
(response) =>
|
||
response.url().endsWith("/api/datasets") &&
|
||
response.request().method() === "POST",
|
||
);
|
||
await page.getByLabel("上传财务表格").setInputFiles({
|
||
name: `formula-e2e-${Date.now()}.csv`,
|
||
mimeType: "text/csv",
|
||
buffer: Buffer.from(csv(`E2E-${Date.now()}`)),
|
||
});
|
||
await uploaded;
|
||
await page
|
||
.getByRole("dialog", { name: "确认导入字段" })
|
||
.getByRole("button", { name: "确认并导入" })
|
||
.click();
|
||
await expect(
|
||
page.getByRole("heading", { name: "集团经营总览", exact: true }),
|
||
).toBeVisible({ timeout: 60000 });
|
||
|
||
let designCalls = 0;
|
||
let sentMessages: { role: string; content: string }[] = [];
|
||
await page.route("**/formula-design", async (route) => {
|
||
sentMessages = JSON.parse(route.request().postData()!).messages;
|
||
designCalls++;
|
||
await route.fulfill({
|
||
json:
|
||
designCalls === 1 || designCalls === 3
|
||
? {
|
||
complete: false,
|
||
questions: [
|
||
designCalls === 1
|
||
? "缺失值和零分母如何处理?"
|
||
: "请说明新增边界条件的计算口径。",
|
||
],
|
||
column: null,
|
||
}
|
||
: {
|
||
complete: true,
|
||
questions: [],
|
||
column: {
|
||
name: "收入成本差",
|
||
meaning: "实际收入减成本",
|
||
scope: "所选分组与期间",
|
||
missing: "结果不可用(#N/A);零分母不可用",
|
||
expression:
|
||
designCalls === 5
|
||
? {
|
||
operator: "multiply",
|
||
left: {
|
||
operator: "subtract",
|
||
left: { metric: "revenue" },
|
||
right: { metric: "cost" },
|
||
},
|
||
right: { number: 100 },
|
||
}
|
||
: {
|
||
operator: "subtract",
|
||
left: { metric: "revenue" },
|
||
right: { metric: "cost" },
|
||
},
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
await page.getByRole("button", { name: "生成公式工作簿" }).click();
|
||
const dialog = page.getByRole("dialog", { name: "生成公式工作簿" });
|
||
await dialog.getByRole("checkbox").check();
|
||
await dialog
|
||
.getByLabel("列计算描述")
|
||
.fill("新增收入成本差;请询问缺失值和零分母处理方式");
|
||
await dialog.getByRole("button", { name: "发送描述" }).click();
|
||
await expect(dialog).toContainText("缺失值和零分母如何处理?");
|
||
await dialog.getByLabel("列计算描述").fill("都显示不可用");
|
||
await dialog.getByRole("button", { name: "发送描述" }).click();
|
||
await expect(dialog.getByText("收入成本差", { exact: true })).toBeVisible();
|
||
await expect(dialog).toContainText("计算表达式:(实际收入 − 成本)");
|
||
await expect(dialog).toContainText("本轮描述 2/6");
|
||
|
||
await dialog.getByLabel("列计算描述").fill("增加一项特殊边界条件");
|
||
await dialog.getByRole("button", { name: "发送描述" }).click();
|
||
await expect(dialog).toContainText("请说明新增边界条件的计算口径。");
|
||
await expect(
|
||
dialog.getByRole("button", { name: "确认并生成公式工作簿" }),
|
||
).toHaveCount(0);
|
||
await dialog.getByLabel("列计算描述").fill("边界条件按收入减成本计算");
|
||
await dialog.getByRole("button", { name: "发送描述" }).click();
|
||
await expect(
|
||
dialog.getByRole("button", { name: "确认并生成公式工作簿" }),
|
||
).toBeVisible();
|
||
await expect(dialog).toContainText("本轮描述 4/6");
|
||
expect(sentMessages.map((message) => message.role)).toEqual([
|
||
"user",
|
||
"assistant",
|
||
"user",
|
||
"assistant",
|
||
"user",
|
||
"assistant",
|
||
"user",
|
||
]);
|
||
expect(sentMessages.at(-1)?.content).toContain("边界条件按收入减成本");
|
||
await dialog.getByRole("button", { name: "保存到定义库" }).click();
|
||
await expect(dialog.getByLabel("已保存定义")).toHaveValue(/.+/);
|
||
await dialog.getByRole("button", { name: "确认并生成公式工作簿" }).click();
|
||
await expect(dialog.getByRole("status")).toContainText("生成完成", {
|
||
timeout: 30000,
|
||
});
|
||
|
||
const download = page.waitForEvent("download");
|
||
await dialog.getByRole("button", { name: "下载公式工作簿" }).click();
|
||
expect((await download).suggestedFilename()).toBe(
|
||
"traceiq-formula-analysis.xlsx",
|
||
);
|
||
await dialog.getByLabel("关闭", { exact: true }).click();
|
||
|
||
await page.getByRole("button", { name: "生成公式工作簿" }).click();
|
||
const reopened = page.getByRole("dialog", { name: "生成公式工作簿" });
|
||
await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" });
|
||
await expect(reopened).toContainText("计算表达式:(实际收入 − 成本)");
|
||
// F04 回归:载入定义后,原定义作为可见上下文进入对话
|
||
await expect(reopened).toContainText("已载入已保存定义「收入成本差」");
|
||
await expect(reopened).toContainText("我:已载入已保存定义");
|
||
await expect(reopened).toContainText("助手:已载入该定义作为当前基准");
|
||
// F04 回归:选择空选项时同步清理当前定义与上下文,选择状态与预览一致
|
||
await reopened.getByLabel("已保存定义").selectOption({ label: "选择定义" });
|
||
await expect(reopened.getByText("计算表达式:(实际收入 − 成本)")).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(reopened.getByText("已载入已保存定义")).toHaveCount(0);
|
||
|
||
await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" });
|
||
await reopened.getByRole("checkbox").check();
|
||
await reopened.getByLabel("列计算描述").fill("把刚才公式改成乘以 100");
|
||
await reopened.getByRole("button", { name: "发送描述" }).click();
|
||
await expect(reopened).toContainText(
|
||
"计算表达式:((实际收入 − 成本) × 100)",
|
||
);
|
||
// 模型请求必须包含载入的原定义上下文和新的修改描述
|
||
expect(sentMessages.at(0)?.content).toContain(
|
||
"已载入已保存定义「收入成本差」",
|
||
);
|
||
expect(sentMessages.at(0)?.content).toContain('"operator":"subtract"');
|
||
expect(sentMessages.at(-1)?.content).toBe("把刚才公式改成乘以 100");
|
||
await reopened.getByRole("button", { name: "确认并生成公式工作簿" }).click();
|
||
await expect(reopened.getByRole("status")).toContainText("生成完成", {
|
||
timeout: 30000,
|
||
});
|
||
expect(designCalls).toBe(5);
|
||
});
|