const assert = require('node:assert/strict'); const { randomUUID, createHash } = require('node:crypto'); module.exports = async function employeeFixture(h) { const { prisma, pool, call, app, req, finance, manager, ordinary, config, actor, company, bank, confirmedLoan, repay, exportOrder, receipt } = h; const { encryptStringOrThrow, decryptStringOrThrow } = req('./src/services/crypto'); const checks = []; const sourceUrl = id => `/loan-lenders/employee-source/${id}?legalEntityId=${company.id}`; const source = who => call(finance, 'GET', sourceUrl(who.id)); async function employee(label, roles = ['employee']) { const who = await actor(roles); await prisma.employee.update({ where: { id: who.id }, data: { name: `合成员工-${label}`, bank_institution_id: bank.id, bank_name: '合成建行员工收款资料', bank_account_encrypted: encryptStringOrThrow(config, '2'.repeat(16)) } }); return who; } const input = (who, preview, key = randomUUID()) => ({ employeeId: who.id, legalEntityId: company.id, expectedSourceVersion: preview.sourceVersion, detailsConfirmed: true, requestKey: key }); const link = (body, status = 200, reason) => call(finance, 'POST', '/loan-lenders/from-employee', body, status, reason); const refreshInput = preview => ({ expectedSourceVersion: preview.sourceVersion, detailsConfirmed: true, expectedIdentityVersion: preview.existingLender.identityVersion, expectedAccountVersion: preview.existingLender.accountVersion, reason: '合成根据员工档案重新核对姓名及收款资料' }); const refresh = (id, body, status = 200, reason) => call(finance, 'POST', `/loan-lenders/${id}/employee-refresh`, body, status, reason); const staff = await employee('资料复用', ['manager']); const employeeCount = await prisma.employee.count(); const preview = await source(staff); assert.equal(preview.linkState, 'new'); assert.deepEqual(preview.problems, []); assert.ok(!JSON.stringify(preview).includes('2'.repeat(16)), 'preview must contain only a masked account'); assert.ok(!JSON.stringify(preview).includes('encrypted'), 'preview never exposes encrypted source columns'); const body = input(staff, preview); const created = await link(body); const lender = created.lender; assert.equal(created.reused, false); assert.equal(lender.employeeId, staff.id); assert.equal(lender.identityStatus, 'verified'); const persisted = await prisma.loanLender.findUniqueOrThrow({ where: { id: lender.id } }); assert.equal(persisted.name, preview.employee.name); assert.ok(decryptStringOrThrow(config, persisted.bank_account_encrypted) === '2'.repeat(16), 'account comes from employee'); assert.ok(decryptStringOrThrow(config, persisted.account_name_encrypted) === preview.employee.name, 'account holder comes from employee name'); assert.equal((await link(body)).lender.id, lender.id); assert.equal((await link(input(staff, await source(staff)))).reused, true); assert.equal(await prisma.employee.count(), employeeCount, 'linking never creates an employee'); assert.equal(await prisma.loanLender.count({ where: { employee_id: staff.id, legal_entity_id: company.id } }), 1); const reuseKey = randomUUID(); await link(input(staff, await source(staff), reuseKey)); const keyTarget = await employee('复用请求键冲突'); await link(input(keyTarget, await source(keyTarget), reuseKey), 409, 'loan.duplicate_registration'); assert.equal(await prisma.loanLender.count({ where: { employee_id: keyTarget.id } }), 0); assert.equal((await link(input(staff, await source(staff), reuseKey))).lender.id, lender.id); checks.push('employee_source_masked_server_copy_verified_link_and_reuse'); for (const who of [manager, ordinary, null]) { await call(who, 'GET', sourceUrl(staff.id), undefined, who ? 403 : 401); await call(who, 'POST', '/loan-lenders/from-employee', body, who ? 403 : 401); await call(who, 'POST', `/loan-lenders/${lender.id}/employee-refresh`, refreshInput(await source(staff)), who ? 403 : 401); } // An otherwise qualified finance user still needs employee bank permission. const hasPermission = app.hasPermission; app.hasPermission = (person, code) => code === 'employee.bank.read' || code === 'employee.bank.manage' ? false : hasPermission(person, code); try { await call(finance, 'GET', sourceUrl(staff.id), undefined, 403); await call(finance, 'POST', '/loan-lenders/from-employee', body, 403); await call(finance, 'POST', `/loan-lenders/${lender.id}/employee-refresh`, {}, 403); } finally { app.hasPermission = hasPermission; } for (const addition of [{ name: '伪造姓名' }, { bankAccount: '9'.repeat(16) }, { accountName: '伪造账户名' }, { detailsConfirmed: false }]) { await link({ ...body, ...addition }, 400, 'loan.invalid_input'); } await link({ ...body, legalEntityId: randomUUID() }, 409, 'loan.source_unavailable'); const otherCompany = await prisma.legalEntity.create({ data: { code: `SYN-${randomUUID()}`, name: '合成另一个借款公司' } }); const explicitCompanySource = await call(finance, 'GET', `/loan-lenders/employee-source/${staff.id}?legalEntityId=${otherCompany.id}`); assert.equal(explicitCompanySource.employee.legalEntityId, company.id); const otherCompanyLender = await link({ ...input(staff, explicitCompanySource), legalEntityId: otherCompany.id }); assert.equal(otherCompanyLender.lender.legalEntityId, otherCompany.id, 'employee employer must not replace the explicitly chosen debtor'); checks.push('employee_source_role_bank_permission_and_strict_input_guards'); const incomplete = await employee('资料缺项'); const normal = await prisma.employee.findUniqueOrThrow({ where: { id: incomplete.id } }); const unsupported = await prisma.bankInstitution.create({ data: { code: `SYN-${randomUUID().slice(0, 8)}`, name: '合成其他银行' } }); for (const [patch, problem] of [ [{ name: '' }, 'name_invalid'], [{ bank_name: '' }, 'bank_missing'], [{ bank_institution_id: null }, 'bank_missing'], [{ bank_institution_id: unsupported.id }, 'bank_unsupported'], [{ bank_account_encrypted: '' }, 'account_missing'], [{ bank_account_encrypted: 'synthetic-unreadable' }, 'account_unavailable'], [{ bank_account_encrypted: encryptStringOrThrow({ fieldEncryptionKey: Buffer.alloc(32, 7).toString('base64') }, '3'.repeat(16)) }, 'account_unavailable'], [{ bank_account_encrypted: encryptStringOrThrow(config, 'invalid-account') }, 'account_invalid'], ]) { await prisma.employee.update({ where: { id: incomplete.id }, data: { name: normal.name, bank_name: normal.bank_name, bank_institution_id: normal.bank_institution_id, bank_account_encrypted: normal.bank_account_encrypted, ...patch } }); const invalid = await source(incomplete); assert.ok(invalid.problems.includes(problem), `expected ${problem}; observed ${invalid.problems.join(',')}`); await link(input(incomplete, invalid), 409, 'loan.employee_source_incomplete'); } await prisma.employee.update({ where: { id: incomplete.id }, data: { status: 'archived' } }); await call(finance, 'GET', sourceUrl(incomplete.id), undefined, 409, 'loan.source_unavailable'); assert.equal(await prisma.loanLender.count({ where: { employee_id: incomplete.id } }), 0); checks.push('employee_missing_unreadable_unsupported_and_inactive_sources_rejected'); const stale = await employee('预览变更'); const old = await source(stale); const row = await prisma.employee.findUniqueOrThrow({ where: { id: stale.id } }); // Holding updated_at constant proves the version binds actual fields too. await prisma.employee.update({ where: { id: stale.id }, data: { bank_account_encrypted: encryptStringOrThrow(config, '3'.repeat(16)), updated_at: row.updated_at } }); await link(input(stale, old), 409, 'loan.employee_source_changed'); assert.equal(await prisma.loanLender.count({ where: { employee_id: stale.id } }), 0); const fresh = await source(stale); const freshLender = (await link(input(stale, fresh))).lender; await link({ ...input(stale, fresh), requestKey: body.requestKey }, 409, 'loan.duplicate_registration'); checks.push('employee_source_version_covers_fields_and_request_key_conflicts'); async function manualFor(who) { const e = await prisma.employee.findUniqueOrThrow({ where: { id: who.id } }); return call(finance, 'POST', '/loan-lenders', { requestKey: randomUUID(), legalEntityId: company.id, name: e.name, bankInstitutionId: bank.id, bankName: e.bank_name, bankAccount: decryptStringOrThrow(config, e.bank_account_encrypted), accountName: e.name }); } const other = await manualFor(staff); await call(finance, 'POST', `/loan-lenders/${other.id}/identity-confirmation`, { expectedVersion: other.identityVersion, employeeId: staff.id, externalPersonConfirmed: false, reason: '合成旧入口重复关联拒绝' }, 409, 'loan.employee_lender_conflict'); assert.equal((await prisma.loanLender.findUniqueOrThrow({ where: { id: other.id } })).employee_id, null); // Model an old ambiguous dataset directly; this is not an application path. await prisma.loanLender.update({ where: { id: other.id }, data: { employee_id: staff.id } }); assert.equal((await source(staff)).linkState, 'conflict'); await link(input(staff, await source(staff)), 409, 'loan.employee_lender_conflict'); await refresh(lender.id, refreshInput({ ...preview, existingLender: lender }), 409, 'loan.employee_lender_conflict'); await prisma.loanLender.update({ where: { id: other.id }, data: { employee_id: null } }); await prisma.loanLender.update({ where: { id: freshLender.id }, data: { archived_at: new Date() } }); assert.equal((await source(stale)).linkState, 'conflict'); await link(input(stale, await source(stale)), 409, 'loan.employee_lender_conflict'); checks.push('legacy_identity_entry_and_ambiguous_or_archived_links_cannot_duplicate'); const { Pool } = req('pg'); // Pause the real refresh immediately before its audit INSERT checks actor A's // FK. It already holds employee B; B's concurrent link locks A then waits on B. // An UPDATE lock on A would deadlock with the audit's implicit KEY SHARE. const [financeA, financeB] = (await Promise.all([ employee('外键交错甲', ['finance']), employee('外键交错乙', ['finance']), ])).sort((a, b) => a.id.localeCompare(b.id)); const fkLender = (await call(financeA, 'POST', '/loan-lenders/from-employee', input(financeB, await source(financeB)))).lender; const fkRefresh = refreshInput(await source(financeB)); const fkLink = input(financeA, await source(financeA)); const fkLocks = new Pool({ ...pool.options, max: 2 }); const fkHolder = await fkLocks.connect(); const fkResponses = []; const deadlocksBefore = Number((await pool.query('SELECT deadlocks FROM pg_stat_database WHERE datname=current_database()')).rows[0].deadlocks); await pool.query(`CREATE FUNCTION synthetic_pause_employee_audit() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.target_id = TG_ARGV[0]::uuid THEN PERFORM pg_advisory_xact_lock(293174183); END IF; RETURN NEW; END $$`); await pool.query(`CREATE TRIGGER synthetic_employee_fk_pause BEFORE INSERT ON audit_logs FOR EACH ROW WHEN (NEW.action = 'LOAN_LENDER_EMPLOYEE_REFRESHED') EXECUTE FUNCTION synthetic_pause_employee_audit('${fkLender.id}')`); let fkResults; let fkLinkResponse; try { await fkHolder.query('SELECT pg_advisory_lock(293174183)'); fkResponses.push(app.inject({ method: 'POST', url: `/api/loan-lenders/${fkLender.id}/employee-refresh`, headers: financeA.headers, payload: fkRefresh }).then(r => r)); async function observeFkWait(count) { for (let i = 0; i < 200; i++) { const rows = await fkLocks.query(`SELECT count(*)::int AS n, count(*) FILTER (WHERE wait_event='advisory')::int AS advisory FROM pg_stat_activity WHERE datname=current_database() AND wait_event_type='Lock'`); if (rows.rows[0].n >= count && rows.rows[0].advisory === 1) return; await new Promise(resolve => setTimeout(resolve, 10)); } if (count === 2 && fkLinkResponse?.statusCode === 200) { throw Object.assign(new Error('source association finished without waiting for the audit-held employee'), { code: 'EMPLOYEE_SOURCE_LOCK_NOT_HELD' }); } throw new Error('synthetic audit/employee interleaving did not reach both database lock waits'); } await observeFkWait(1); fkResponses.push(app.inject({ method: 'POST', url: '/api/loan-lenders/from-employee', headers: financeB.headers, payload: fkLink }).then(r => { fkLinkResponse = r; return r; })); await observeFkWait(2); await fkHolder.query('SELECT pg_advisory_unlock(293174183)'); fkResults = await Promise.all(fkResponses); } finally { await fkHolder.query('SELECT pg_advisory_unlock(293174183)'); fkHolder.release(); await Promise.allSettled(fkResponses); await fkLocks.end(); await pool.query('DROP TRIGGER synthetic_employee_fk_pause ON audit_logs'); await pool.query('DROP FUNCTION synthetic_pause_employee_audit()'); } if (fkResults.some(r => r.statusCode !== 200)) { // Classify the reverse mutation only with PostgreSQL's actual deadlock // counter, never a generic 500, timeout or fixture/environment failure. for (let i = 0; i < 200; i++) { const count = Number((await pool.query('SELECT deadlocks FROM pg_stat_database WHERE datname=current_database()')).rows[0].deadlocks); if (count > deadlocksBefore) throw Object.assign(new Error('observed employee source/audit FK deadlock'), { code: 'EMPLOYEE_SOURCE_FK_DEADLOCK' }); await new Promise(resolve => setTimeout(resolve, 10)); } } assert.deepEqual(fkResults.map(r => r.statusCode), [200, 200], 'refresh audit and concurrent source association must both finish'); assert.equal(fkResults[0].json().data.identityVersion, fkLender.identityVersion + 1); assert.equal(fkResults[1].json().data.lender.employeeId, financeA.id); assert.equal(await prisma.auditLog.count({ where: { action: 'LOAN_LENDER_EMPLOYEE_REFRESHED', target_id: fkLender.id } }), 1); checks.push('employee_source_locks_allow_implicit_audit_fk_without_deadlock'); const locks = new Pool({ ...pool.options, max: 2 }); async function waiters(count) { for (let i = 0; i < 200; i++) { const result = await locks.query("SELECT count(*)::int AS n FROM pg_stat_activity WHERE datname=current_database() AND wait_event_type='Lock'"); if (result.rows[0].n >= count) return; await new Promise(resolve => setTimeout(resolve, 10)); } throw new Error('synthetic employee operation did not actually wait for the row lock'); } async function interleave(who, starts) { const holder = await locks.connect(); const responses = []; try { await holder.query('BEGIN'); await holder.query('SELECT id FROM employees WHERE id=$1 FOR UPDATE', [who.id]); for (const start of starts) { responses.push(start()); await waiters(responses.length); } await holder.query('COMMIT'); return await Promise.all(responses); } finally { await holder.query('ROLLBACK'); holder.release(); await Promise.allSettled(responses); } } const startLink = body => () => app.inject({ method: 'POST', url: '/api/loan-lenders/from-employee', headers: finance.headers, payload: body }).then(r => r); try { const simultaneous = await employee('并发新建'); const p = await source(simultaneous); const replies = await interleave(simultaneous, [startLink(input(simultaneous, p)), startLink(input(simultaneous, p))]); assert.deepEqual(replies.map(r => r.statusCode), [200, 200]); assert.equal(replies[0].json().data.lender.id, replies[1].json().data.lender.id); assert.equal(await prisma.loanLender.count({ where: { employee_id: simultaneous.id, legal_entity_id: company.id } }), 1); const keyed = await employee('并发复用请求键'); const concurrentKey = randomUUID(); const keyedReplies = await interleave(finance, [startLink(input(staff, await source(staff), concurrentKey)), startLink(input(keyed, await source(keyed), concurrentKey))]); assert.deepEqual(keyedReplies.map(r => r.statusCode), [200, 409]); assert.equal(keyedReplies[1].json().error?.details.reasonCode, 'loan.duplicate_registration'); assert.equal(await prisma.loanLender.count({ where: { employee_id: keyed.id } }), 0); for (const legacyFirst of [false, true]) { const who = await employee(`并发旧入口-${legacyFirst}`); const p = await source(who); const manual = await manualFor(who); const legacy = () => app.inject({ method: 'POST', url: `/api/loan-lenders/${manual.id}/identity-confirmation`, headers: finance.headers, payload: { expectedVersion: manual.identityVersion, employeeId: who.id, externalPersonConfirmed: false, reason: '合成并发身份关联' } }).then(r => r); const replies = await interleave(who, legacyFirst ? [legacy, startLink(input(who, p))] : [startLink(input(who, p)), legacy]); assert.deepEqual(replies.map(r => r.statusCode), legacyFirst ? [200, 200] : [200, 409]); if (!legacyFirst) assert.equal(replies[1].json().error?.details.reasonCode, 'loan.employee_lender_conflict'); assert.equal(await prisma.loanLender.count({ where: { employee_id: who.id, legal_entity_id: company.id } }), 1); } } finally { await locks.end(); } checks.push('employee_creation_and_legacy_entry_observed_lock_interleavings'); const loan = await confirmedLoan(lender.id, '100.00', `合成员工原借入-${randomUUID()}`); async function authorize() { const auth = await call(finance, 'POST', '/loan-related-authorizations', { lenderId: lender.id, loanIds: [loan.id], requestKey: randomUUID(), basis: '合成已存在的公司代表权及此笔借款范围' }); await call(staff, 'POST', `/loan-related-authorizations/${auth.id}/actions`, { action: 'confirm', reason: '合成公司代表本人确认授权范围' }); return auth; } const auth = await authorize(); const batch = await repay(lender.id, loan.id, '10.00'); assert.equal(batch.loanRepayment.approvalMode, 'related_party'); await call(staff, 'POST', `/payment-batches/${batch.id}/review`, {}); await exportOrder(batch.id); async function snapshots() { const repayment = await prisma.loanRepayment.findUniqueOrThrow({ where: { id: batch.loanRepaymentId }, select: { lender_identity_version: true, lender_account_version: true, payee_snapshot_json: true, approval_context: true } }); const savedBatch = await prisma.paymentBatch.findUniqueOrThrow({ where: { id: batch.id }, select: { payment_account_snapshot_json: true, items: { select: { snapshot_json: true } } } }); return createHash('sha256').update(JSON.stringify({ repayment, savedBatch })).digest('hex'); } const originalSnapshots = await snapshots(); const beforeChange = await source(staff); await prisma.employee.update({ where: { id: staff.id }, data: { bank_account_encrypted: encryptStringOrThrow(config, '3'.repeat(16)) } }); assert.equal(await snapshots(), originalSnapshots); const changed = await source(staff); assert.equal(changed.linkState, 'refresh_required'); assert.equal(changed.existingLender.hasInFlightRepayment, true); await link(input(staff, changed), 409, 'loan.employee_lender_refresh_required'); await refresh(lender.id, refreshInput(changed), 409, 'loan.lender_in_flight'); const proof = await receipt(batch.id, '1000', `SYN-EMPLOYEE-${randomUUID()}`, { payeeAccount: '2'.repeat(16) }); assert.equal(proof.result.status, 'matched'); assert.equal((await call(finance, 'POST', `/payment-batches/${batch.id}/execute`, {})).item.status, 'executed'); await refresh(lender.id, refreshInput(beforeChange), 409, 'loan.employee_source_changed'); const updated = await refresh(lender.id, refreshInput(await source(staff))); assert.equal(updated.accountVersion, lender.accountVersion + 1); assert.equal(updated.identityVersion, lender.identityVersion + 1); assert.equal(updated.identityStatus, 'verified'); assert.equal(updated.employeeId, staff.id); assert.equal(await snapshots(), originalSnapshots, 'explicit source refresh never rewrites historical payment snapshots'); assert.equal((await call(finance, 'GET', `/company-loans/${loan.id}`)).outstanding, '90.00'); await refresh(lender.id, refreshInput(changed), 409, 'loan.version_changed'); await call(finance, 'PATCH', `/loan-lenders/${lender.id}/account`, { expectedVersion: updated.accountVersion, bankInstitutionId: bank.id, bankName: '伪造覆盖', bankAccount: '9'.repeat(16), accountName: '伪造覆盖' }, 409, 'loan.employee_account_managed'); await call(finance, 'POST', '/loan-repayments', { requestKey: randomUUID(), lenderId: lender.id, reason: '合成旧授权拒绝', allocations: [{ loanId: loan.id, amount: '1.00' }] }, 409, 'loan.related_scope_invalid'); assert.equal((await prisma.loanRelatedAuthorization.findUniqueOrThrow({ where: { id: auth.id } })).identity_version, lender.identityVersion); await authorize(); const renewed = await repay(lender.id, loan.id, '1.00'); await call(finance, 'POST', `/payment-batches/${renewed.id}/cancel`, { reason: '合成刷新后新授权验证结束' }); checks.push('employee_refresh_inflight_guard_old_payment_snapshot_settlement_and_authority_versions'); const raceLocks = new Pool({ ...pool.options, max: 2 }); try { for (const first of ['create', 'refresh']) { await authorize(); const current = await source(staff); const holder = await raceLocks.connect(); const responses = []; let results; try { await holder.query('BEGIN'); await holder.query('SELECT id FROM loan_lenders WHERE id=$1 FOR UPDATE', [lender.id]); const starts = { create: () => app.inject({ method: 'POST', url: '/api/loan-repayments', headers: finance.headers, payload: { requestKey: randomUUID(), lenderId: lender.id, reason: '合成员工刷新并发还款', allocations: [{ loanId: loan.id, amount: '1.00' }] } }).then(r => r), refresh: () => app.inject({ method: 'POST', url: `/api/loan-lenders/${lender.id}/employee-refresh`, headers: finance.headers, payload: refreshInput(current) }).then(r => r), }; for (const action of [first, first === 'create' ? 'refresh' : 'create']) { responses.push(starts[action]()); let waiting = false; for (let i = 0; i < 200; i++) { const count = await raceLocks.query("SELECT count(*)::int AS n FROM pg_stat_activity WHERE datname=current_database() AND wait_event_type='Lock'"); if (count.rows[0].n >= responses.length) { waiting = true; break; } await new Promise(resolve => setTimeout(resolve, 10)); } assert.ok(waiting, 'both source refresh and repayment creation must actually wait on the lender lock'); } await holder.query('COMMIT'); results = await Promise.all(responses); } finally { await holder.query('ROLLBACK'); holder.release(); await Promise.allSettled(responses); } assert.deepEqual(results.map(r => r.statusCode), [200, 409], `${first} wins and the other action refuses stale or reserved data`); assert.equal(results[1].json().error?.details.reasonCode, first === 'create' ? 'loan.lender_in_flight' : 'loan.related_scope_invalid'); const currentLender = await prisma.loanLender.findUniqueOrThrow({ where: { id: lender.id } }); assert.equal(await prisma.loanRepayment.count({ where: { lender_id: lender.id, allocations: { some: { reservation_state: 'reserved' } }, OR: [{ lender_identity_version: { not: currentLender.identity_version } }, { lender_account_version: { not: currentLender.account_version } }] } }), 0); if (first === 'create') await call(finance, 'POST', `/payment-batches/${results[0].json().data.item.id}/cancel`, { reason: '合成员工刷新交错验证结束' }); } } finally { await raceLocks.end(); } checks.push('employee_refresh_and_repayment_creation_both_observed_lender_lock_orders'); const rollbackStaff = await employee('审计回滚'); const rollbackSource = await source(rollbackStaff); await pool.query(`CREATE FUNCTION synthetic_fail_employee_lender_audit() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.action IN ('LOAN_LENDER_EMPLOYEE_CREATED','LOAN_LENDER_EMPLOYEE_REFRESHED') THEN RAISE EXCEPTION 'synthetic audit unavailable'; END IF; RETURN NEW; END $$`); await pool.query('CREATE TRIGGER synthetic_employee_lender_audit BEFORE INSERT ON audit_logs FOR EACH ROW EXECUTE FUNCTION synthetic_fail_employee_lender_audit()'); try { await link(input(rollbackStaff, rollbackSource), 500); assert.equal(await prisma.loanLender.count({ where: { employee_id: rollbackStaff.id } }), 0); const state = await source(staff); await refresh(lender.id, refreshInput(state), 500); const retained = await prisma.loanLender.findUniqueOrThrow({ where: { id: lender.id } }); assert.equal(retained.identity_version, state.existingLender.identityVersion); assert.equal(retained.account_version, state.existingLender.accountVersion); } finally { await pool.query('DROP TRIGGER synthetic_employee_lender_audit ON audit_logs'); await pool.query('DROP FUNCTION synthetic_fail_employee_lender_audit()'); } checks.push('employee_source_creation_and_refresh_audit_failure_roll_back'); const ui = []; for (const mode of ['desktop', 'mobile']) ui.push({ mode, staff: await employee(`浏览器-${mode}`, ['manager']), switchStaff: await employee(`浏览器切换-${mode}`), company }); console.log(JSON.stringify({ stage: 'loan_employee_http', status: 'PASS', checks })); return { ui, checks, otherFinance: financeB }; };