Files
fa-agent/tests/e2e/formula-export.spec.ts
T
Jayway Liu 9a5ab26014
Verify / verify (push) Failing after 1s
Verify / verify (pull_request) Failing after 1s
fix(dialogs): F04 载入已保存定义后把原定义带入可见对话上下文
- 载入定义时以“用户消息+助手确认”形式把名称、含义、范围、缺失处理和表达式
  写入对话,后续“把刚才公式改成乘以 100”等指代可关联到已载入定义;
  生成前仍需显式确认,不静默沿用旧定义
- 选择空选项时同步清理当前定义与上下文,选择状态与预览保持一致
- 服务端单条消息长度上限 1000→2000 以容纳完整定义上下文,超长仍拒绝
- E2E 覆盖:载入后请求包含原定义、修改后表达式为 (收入−成本)×100、
  空选项清理;单测覆盖 2000/2001 边界
2026-09-26 01:30:34 +08:00

167 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});