'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); /** @param {string} source @param {string} name @returns {string} */ function extractFunction(source, name) { const pattern = new RegExp('export (?:async )?function ' + name + '\\('); const match = pattern.exec(source); assert.ok(match, '缺少方法 ' + name); const end = source.indexOf('\n/**', match.index + match[0].length); return source.slice(match.index, end < 0 ? source.length : end).replace(/^export /, ''); } const defaultSourcePath = path.resolve(__dirname, '../src/timecard-repair-lifecycle.oyd.jsx'); const sourcePath = process.argv[2] ? path.resolve(process.argv[2]) : defaultSourcePath; const source = fs.readFileSync(sourcePath, 'utf8'); let holdTimer = false; let pendingTimer = null; const methods = vm.runInNewContext( ['getRepairContextErrorMessage', 'isRepairContextRetryableError', 'waitForRepairContextRetry', 'loadTimecardRepairStatus'] .map((name) => extractFunction(source, name)).join('\n') + '\n({getRepairContextErrorMessage, isRepairContextRetryableError, waitForRepairContextRetry, loadTimecardRepairStatus})', { REPAIR_DATA_SOURCES: { status: 'getTimecardRepairStatus' }, setTimeout: (callback) => { if (!holdTimer) { callback(); return { immediate: true }; } pendingTimer = { callback, cleared: false }; return pendingTimer; }, }, ); /** @param {*} response @returns {object} */ function createContext(response) { return { state: {}, getTexts: () => ({ repairContextTimeout: '补填工时加载超时,请稍后重试', errUnknown: '未知错误' }), getRepairContextErrorMessage: methods.getRepairContextErrorMessage, isRepairContextRetryableError: methods.isRepairContextRetryableError, waitForRepairContextRetry: methods.waitForRepairContextRetry, getRepairMonthText: (year, month) => String(year) + String(month).padStart(2, '0'), getCallableDataSource: () => ({ load: async () => response }), unwrapConnectorResponse: (value) => value, }; } /** @returns {Promise} */ async function main() { const errorContext = createContext(null); assert.equal(methods.getRepairContextErrorMessage.call(errorContext, new Error('Read timed out')), '补填工时加载超时,请稍后重试'); assert.equal(methods.getRepairContextErrorMessage.call(errorContext, { errorCode: 'ETIMEDOUT', code: 'ETIMEDOUT' }), '补填工时加载超时,请稍后重试'); assert.equal(methods.getRepairContextErrorMessage.call(errorContext, new Error('接口返回 500')), '接口返回 500'); assert.equal(methods.getRepairContextErrorMessage.call(errorContext, {}), '未知错误'); const response = { originalRecords: [{ id: 'original' }], repairRecords: [{ id: 'repair' }], records: [] }; const result = await methods.loadTimecardRepairStatus.call(createContext(response), 'user-1', 2026, 9); assert.equal(result.configured, true); assert.equal(result.originalRecords[0].id, 'original'); assert.equal(result.repairRecords[0].id, 'repair'); assert.equal(Object.hasOwn(result, 'closed'), false); let retryCount = 0; const retryContext = createContext(response); retryContext.getCallableDataSource = () => ({ load: async () => { retryCount += 1; if (retryCount === 1) throw new Error('Read timed out'); return response; }, }); const retryResult = await methods.loadTimecardRepairStatus.call(retryContext, 'user-1', 2026, 9); assert.equal(retryCount, 2); assert.equal(retryResult.originalRecords[0].id, 'original'); let pendingCount = 0; const pendingContext = createContext(response); pendingContext.getCallableDataSource = () => ({ load: async () => { pendingCount += 1; return pendingCount < 3 ? { ready: false, retryAfterMillis: 500 } : response; }, }); const pendingResult = await methods.loadTimecardRepairStatus.call(pendingContext, 'user-1', 2026, 9); assert.equal(pendingCount, 3); assert.equal(pendingResult.repairRecords[0].id, 'repair'); let emptyCount = 0; const emptyContext = createContext(response); emptyContext.getCallableDataSource = () => ({ load: async () => { emptyCount += 1; return emptyCount === 1 ? null : response; }, }); const emptyRetryResult = await methods.loadTimecardRepairStatus.call(emptyContext, 'user-1', 2026, 9); assert.equal(emptyCount, 2); assert.equal(emptyRetryResult.originalRecords[0].id, 'original'); let exhaustedCount = 0; const exhaustedContext = createContext(null); exhaustedContext.getCallableDataSource = () => ({ load: async () => { exhaustedCount += 1; return { originalRecords: [] }; }, }); await assert.rejects( methods.loadTimecardRepairStatus.call(exhaustedContext, 'user-1', 2026, 9), /补填工时加载超时,请稍后重试/, ); assert.equal(exhaustedCount, 45); let businessErrorCount = 0; const businessErrorContext = createContext(null); businessErrorContext.getCallableDataSource = () => ({ load: async () => { businessErrorCount += 1; throw new Error('无权限访问'); }, }); await assert.rejects( methods.loadTimecardRepairStatus.call(businessErrorContext, 'user-1', 2026, 9), /无权限访问/, ); assert.equal(businessErrorCount, 1); let releasedOnUnmount = false; const waitContext = { _tcDisposed: false, _tcRepairContextPollTimers: [], }; holdTimer = true; methods.waitForRepairContextRetry.call(waitContext, 2000).then(() => { releasedOnUnmount = true; }); assert.equal(waitContext._tcRepairContextPollTimers.length, 1); const entry = waitContext._tcRepairContextPollTimers[0]; assert.equal(entry.timerId, pendingTimer); pendingTimer.cleared = true; entry.resolve(); await Promise.resolve(); assert.equal(pendingTimer.cleared, true); assert.equal(releasedOnUnmount, true); holdTimer = false; console.log('PASS 补填上下文 pending 轮询、空响应重试、总超时和业务错误识别'); } main().catch((error) => { console.error(error); process.exitCode = 1; });