文件
hyb-oa/server/tests/unit/scripts/apply-ceo-finance-fallback.test.js
T
wangsli a74cba1112
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
修复生产回填审计标识(07-28-ceo-finance-fallback)
2026-07-28 14:25:03 +08:00

184 行
6.8 KiB
JavaScript

const {
assertPeopleBaseline,
buildRulePlan,
execute,
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");
});
it("supplies an explicit UUID for each raw audit-log insert", async () => {
const ruleIds = [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222",
"33333333-3333-4333-8333-333333333333",
];
const sourceRows = currentRows().map((row, index) => ({ ...row, id: ruleIds[index] }));
const targetRows = sourceRows.map((row) => ({
...row,
level2_role: row.id === ruleIds[2] ? "ceo" : "finance",
level3_role: row.id === ruleIds[2] ? "finance" : null,
owner_self_fallback_role: "finance",
}));
const adminId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const auditCalls = [];
const transactionCommands = [];
let ruleSelectCount = 0;
let invariantCount = 0;
const client = {
query: vi.fn(async (sql, params = []) => {
const normalized = String(sql).replace(/\s+/g, " ").trim();
if (normalized.startsWith("BEGIN ") || normalized.startsWith("ROLLBACK")) {
transactionCommands.push(normalized);
return {};
}
if (normalized.includes("FROM approval_rules")) {
ruleSelectCount += 1;
return { rows: ruleSelectCount === 1 ? sourceRows : targetRows };
}
if (normalized.includes("JOIN employee_roles selected_role")) {
if (params[0] === "ceo") {
return { rows: [{ id: "ceo-id", direct_manager_id: null, has_wecom_binding: true, roles: ["ceo"] }] };
}
if (params[0] === "finance") {
return { rows: [{ id: "finance-id", direct_manager_id: null, has_wecom_binding: true, roles: ["finance"] }] };
}
return { rows: [{ id: adminId, direct_manager_id: null, has_wecom_binding: false, roles: ["admin"] }] };
}
if (normalized.includes("AS active_employees")) {
invariantCount += 1;
return { rows: [{ active_employees: 6, active_role_rows: 8, active_bindings: 6, workflow_snapshots: 0 }] };
}
if (normalized.startsWith("UPDATE approval_rules")) return { rowCount: 1, rows: [{ id: params[0] }] };
if (normalized.startsWith("INSERT INTO audit_logs")) {
auditCalls.push({ sql: normalized, params });
return { rowCount: 1 };
}
throw new Error(`unexpected query: ${normalized}`);
}),
release: vi.fn(),
};
const pool = { connect: vi.fn(async () => client), end: vi.fn(async () => {}) };
await expect(execute({ apply: false, format: "json" }, pool)).resolves.toMatchObject({
status: "DRY_RUN_PASS",
rules: 3,
auditRows: 0,
});
expect(invariantCount).toBe(2);
expect(transactionCommands).toEqual([
"BEGIN ISOLATION LEVEL SERIALIZABLE",
"ROLLBACK",
]);
expect(auditCalls).toHaveLength(3);
for (const [index, call] of auditCalls.entries()) {
expect(call.sql).toContain("(id, request_id, actor_id");
expect(call.sql).toContain("VALUES ($1::uuid, $2, $3::uuid");
expect(call.params[0]).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
expect(call.params[1]).toMatch(/^ceo-finance-fallback-[0-9a-f-]+$/i);
expect(call.params[2]).toBe(adminId);
expect(call.params[3]).toBe(ruleIds[index]);
expect(JSON.parse(call.params[4])).toMatchObject({
task: "07-28-ceo-finance-fallback",
band: ["small", "medium", "large"][index],
});
expect(call.params).toHaveLength(5);
}
});
});