Files
fa-agent/tests/e2e/formula-export.spec.ts
T
Jayway Liu 509b406254
Verify / verify (push) Failing after 1s
Verify / verify (pull_request) Failing after 1s
AI Review / review (pull_request) Successful in 16s
fix(formula): 导出文件安全、清理回收、撤权校验与定义库可靠性(F01/F02/F05-F08)
- F01: _safe_unlink_export 不跟随符号链接、逐层校验父目录,导出失败路径与
  清理命令统一使用,只清理受控目录内本次创建的文件
- F02: 清理失败记录告警日志;清理命令回收 failed 任务残留文件
- F05: 定义保存事务内锁数据集行,配额原子检查;revision 冲突返回 409;
  保留最近 20 版历史;支持个人/团队共享;审计与变更同事务
- F06: 工作簿参数页毛利率改为只读快照;可编辑单元格解锁并加数值校验
- F07: 对账按指标分容差(人数 0、比率 0.0001、金额 0.01);自定义列使用
  服务端 Decimal 表达式求值参考
- F08: 导出任务执行/下载阶段重校验账号有效性与角色
- 对话草稿会话恢复;载入定义后追问携带原定义(F04);下载改浏览器直传
- 迁移 0006-0008;PostgreSQL 咨询锁语义修正(import_worker)
2026-10-11 09:43:27 +08:00

155 lines
6.9 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: {
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 columns = page.getByRole("dialog", { name: "选择网页自定义列" });
await columns.getByRole("checkbox", { name: /收入成本差/ }).check();
const customQuery = page.waitForResponse(
(response) =>
response.url().includes("/query") && response.request().method() === "POST",
);
await columns.getByRole("button", { name: "应用", exact: true }).click();
const customResult = await (await customQuery).json();
expect(customResult.summary.custom_0.current).toBe("1000");
expect(customResult.rows[0].values.custom_0.current).toBe("1000");
await page.getByRole("button", { name: "生成公式工作簿" }).click();
const reopened = page.getByRole("dialog", { name: "生成公式工作簿" });
await reopened.getByLabel("已保存定义").selectOption({ label: "收入成本差" });
await expect(reopened).toContainText("计算表达式:(实际收入 − 成本)");
await reopened.getByLabel("列计算描述").fill("把刚才公式乘以 100");
await expect(
reopened.getByRole("button", { name: "先发送补充内容" }),
).toBeDisabled();
await reopened
.getByLabel("同意将描述、指标定义和当前筛选范围发送至已配置的外部 AI 服务")
.check();
await reopened.getByRole("button", { name: "发送描述" }).click();
expect(sentMessages[0].content).toContain('"name":"收入成本差"');
expect(sentMessages[0].content).toContain('"operator":"subtract"');
expect(sentMessages[0].content).toContain("把刚才公式乘以 100");
await reopened.getByRole("button", { name: "确认并生成公式工作簿" }).click();
await expect(reopened.getByRole("status")).toContainText("生成完成", {
timeout: 30000,
});
expect(designCalls).toBe(5);
});