文件
hyb-oa/scripts/qa/company-loan-employee-mutations.cjs
T
wangsli和Claude Opus 5.5 45737ccb9a
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
借款出借人改为从现有员工选择并复用资料(09-17-company-loan-principal-repayment)
- 新增员工来源预览、关联与显式刷新接口,受 loan.manage 与员工银行卡权限保护;服务端读取并加密保存,客户端不能另传姓名账号
- 同公司同员工唯一复用,请求键绑定来源;锁序出借人→员工,员工使用 FOR NO KEY UPDATE
- 员工关联出借人禁止手工改账号;在途还款期间禁止刷新,刷新推进版本并使旧公司代表授权失效
- 电脑/手机共用对话框,写请求在卸载或身份切换时取消
- 无新增迁移;本地 126 组 HTTP、45 组浏览器、9 个变异通过,两名只读复核及 Claude 独立复核无阻断

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 21:03:07 +08:00

50 行
3.9 KiB
JavaScript

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const path = require('node:path');
const Module = require('node:module');
// Mutate only the isolated fixture process. Repository files and the production
// module cache remain unchanged; all route exports are restored after each case.
module.exports = async function employeeMutations(h, runFixture) {
const filename = h.req.resolve('./src/services/loanLenderEmployee');
const source = readFileSync(filename, 'utf8');
const live = h.req('./src/services/loanLenderEmployee'); const original = { ...live };
const mutations = [
['bank_permission', 'if (!fastify.hasPermission(request.employee, "employee.bank.read") &&\n !fastify.hasPermission(request.employee, "employee.bank.manage"))', 'if (false)'],
['source_version', 'if (source.sourceVersion !== expectedSourceVersion)', 'if (false)'],
['legacy_duplicate', 'if (links.some((lender) => lender.id !== lenderId))', 'if (false)'],
['employee_lock', 'if (unique.length) await tx.$queryRaw', 'if (false) await tx.$queryRaw'],
['employee_lock_fk_compatibility', 'ORDER BY id FOR NO KEY UPDATE', 'ORDER BY id FOR UPDATE'],
['in_flight', 'await assertLenderNotInFlight(tx, id);', ''],
['lender_versions', 'assertCurrentVersion(lender.identity_version, input.expectedIdentityVersion);\n assertCurrentVersion(lender.account_version, input.expectedAccountVersion);', ''],
['reuse_request_binding', 'if (!previousRequest) await auditLoan(tx, request, "LOAN_LENDER_EMPLOYEE_REUSED", lender.id, {\n ...requestBinding, employeeSourceRequestKey: input.requestKey,\n });', ''],
['required_audit', 'await auditLoan(tx, request, "LOAN_LENDER_EMPLOYEE_CREATED", created.id, {\n ...requestBinding, employeeSourceRequestKey: input.requestKey,\n });', ''],
];
const results = [];
for (const [name, from, to] of mutations) {
assert.equal(source.split(from).length, 2, `mutation ${name} must have exactly one target`);
const mutant = new Module(filename, module); mutant.filename = filename;
mutant.paths = Module._nodeModulePaths(path.dirname(filename));
mutant._compile(source.replace(from, to), filename);
let failure;
Object.assign(live, mutant.exports);
try { await runFixture(h); } catch (error) { failure = error; }
finally { Object.assign(live, original); }
assert.ok(failure, `mutation ${name} survived`);
// A generic environment, SQL or parsing failure is not a killed guard.
const wrongAcceptance = failure.code === 'ERR_ASSERTION' && failure.actual === 200 && [403, 409, 500].includes(failure.expected);
const duplicateIdentity = name === 'employee_lock' && failure.code === 'ERR_ASSERTION' &&
typeof failure.actual === 'string' && typeof failure.expected === 'string' &&
/^[0-9a-f-]{36}$/.test(failure.actual) && /^[0-9a-f-]{36}$/.test(failure.expected) && failure.actual !== failure.expected;
const missingLock = name === 'employee_lock' && (failure.message === 'synthetic employee operation did not actually wait for the row lock' ||
failure.code === 'EMPLOYEE_SOURCE_LOCK_NOT_HELD');
const fkDeadlock = name === 'employee_lock_fk_compatibility' && failure.code === 'EMPLOYEE_SOURCE_FK_DEADLOCK';
const hint = failure.code === 'ERR_ASSERTION' ? String(failure.message).split('\n')[0] : failure.name;
assert.ok(wrongAcceptance || duplicateIdentity || missingLock || fkDeadlock, `mutation ${name} failed outside its guard: ${hint} ${failure.code ?? ''}`);
results.push({ name, caught: true, witness: wrongAcceptance ? 'unsafe_http_acceptance' : duplicateIdentity ? 'duplicate_employee_lender' : fkDeadlock ? 'observed_postgres_deadlock' : 'missing_row_lock' });
console.log(JSON.stringify({ stage: 'loan_employee_mutation', ...results.at(-1) }));
}
console.log(JSON.stringify({ stage: 'loan_employee_mutations', status: 'PASS', count: results.length, sourceFilesChanged: false }));
return results;
};