修正CEO报销财务兜底与部署护栏(07-28-ceo-finance-fallback)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-28 13:42:48 +08:00
父节点 37b625c5dc
当前提交 35948729cf
共修改 31 个文件,包含 980 行新增和 74 行删除
@@ -0,0 +1,103 @@
const {
assertPeopleBaseline,
buildRulePlan,
parseArgs,
} = require("../../../src/scripts/apply-ceo-finance-fallback");
function currentRows() {
return [
{
id: "small-id",
min_amount: "0.00",
max_amount: "500.00",
level1_role: "manager",
level2_role: "finance",
level3_role: null,
owner_self_fallback_role: "finance_mgr",
},
{
id: "medium-id",
min_amount: "500.00",
max_amount: "5000.00",
level1_role: "manager",
level2_role: "finance_mgr",
level3_role: null,
owner_self_fallback_role: "finance_mgr",
},
{
id: "large-id",
min_amount: "5000.00",
max_amount: null,
level1_role: "manager",
level2_role: "ceo",
level3_role: "finance_mgr",
owner_self_fallback_role: "finance_mgr",
},
];
}
describe("CEO finance fallback production apply contract", () => {
it("builds the exact three-band target without exposing identifiers in the result shape", () => {
const plan = buildRulePlan(currentRows());
expect(plan.map((item) => ({ band: item.band, after: item.after }))).toEqual([
{
band: "small",
after: {
level1_role: "manager",
level2_role: "finance",
level3_role: null,
owner_self_fallback_role: "finance",
},
},
{
band: "medium",
after: {
level1_role: "manager",
level2_role: "finance",
level3_role: null,
owner_self_fallback_role: "finance",
},
},
{
band: "large",
after: {
level1_role: "manager",
level2_role: "ceo",
level3_role: "finance",
owner_self_fallback_role: "finance",
},
},
]);
});
it.each([
[currentRows().slice(0, 2), "approval_rule_count_drift"],
[[...currentRows(), currentRows()[0]], "approval_rule_count_drift"],
[currentRows().map((row, index) => index === 1 ? { ...row, level2_role: "finance" } : row), "approval_rule_role_drift:medium"],
[currentRows().map((row, index) => index === 2 ? { ...row, min_amount: "6000" } : row), "approval_rule_amount_band_drift"],
])("fails closed on approval-rule drift", (rows, message) => {
expect(() => buildRulePlan(rows)).toThrow(message);
});
it("requires one bound CEO without manager/fallback role, one bound non-CEO finance, and one admin", () => {
expect(() => assertPeopleBaseline({
ceos: [{ direct_manager_id: null, has_wecom_binding: true, roles: ["ceo"] }],
finances: [{ has_wecom_binding: true, roles: ["finance"] }],
admins: [{ roles: ["admin"] }],
})).not.toThrow();
expect(() => assertPeopleBaseline({ ceos: [], finances: [], admins: [] }))
.toThrow("active_ceo_count_drift");
expect(() => assertPeopleBaseline({
ceos: [{ direct_manager_id: null, has_wecom_binding: true, roles: ["ceo", "finance"] }],
finances: [{ has_wecom_binding: true, roles: ["finance"] }],
admins: [{ roles: ["admin"] }],
})).toThrow("ceo_holds_fallback_role");
});
it("defaults to dry-run and rejects unknown arguments", () => {
expect(parseArgs([])).toEqual({ apply: false, format: "text" });
expect(parseArgs(["--apply", "--format=json"])).toEqual({ apply: true, format: "json" });
expect(() => parseArgs(["--force"])).toThrow("Unsupported argument");
});
});
@@ -0,0 +1,67 @@
const {
DEFAULT_APPROVAL_RULES,
seedApprovalRules,
} = require("../../../src/scripts/seed-config");
describe("small-team default reimbursement approval rules", () => {
it("uses finance for every owner fallback and finance review node", () => {
expect(DEFAULT_APPROVAL_RULES).toEqual([
expect.objectContaining({
name: "小额",
level1_role: "manager",
level2_role: "finance",
level3_role: null,
owner_self_fallback_role: "finance",
}),
expect.objectContaining({
name: "中额",
level1_role: "manager",
level2_role: "finance",
level3_role: null,
owner_self_fallback_role: "finance",
}),
expect.objectContaining({
name: "大额",
level1_role: "manager",
level2_role: "ceo",
level3_role: "finance",
owner_self_fallback_role: "finance",
}),
]);
});
it("preserves existing production approval rules instead of replacing them", async () => {
const approvalRule = {
count: vi.fn().mockResolvedValue(3),
createMany: vi.fn(),
deleteMany: vi.fn(),
};
await expect(seedApprovalRules({ approvalRule })).resolves.toEqual({
created: 0,
preserved: 3,
});
expect(approvalRule.createMany).not.toHaveBeenCalled();
expect(approvalRule.deleteMany).not.toHaveBeenCalled();
});
it("creates the three defaults only for an empty approval-rule table", async () => {
const approvalRule = {
count: vi.fn().mockResolvedValue(0),
createMany: vi.fn().mockResolvedValue({ count: 3 }),
};
await expect(seedApprovalRules({ approvalRule })).resolves.toEqual({
created: 3,
preserved: 0,
});
expect(approvalRule.createMany).toHaveBeenCalledWith({
data: DEFAULT_APPROVAL_RULES.map((item) => ({
...item,
enabled: true,
created_by: null,
updated_by: null,
})),
});
});
});