verify-repair-context.cjs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. 'use strict';
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const path = require('node:path');
  5. const vm = require('node:vm');
  6. /** @param {string} source @param {string} name @returns {string} */
  7. function extractFunction(source, name) {
  8. const pattern = new RegExp('export (?:async )?function ' + name + '\\(');
  9. const match = pattern.exec(source);
  10. assert.ok(match, '缺少方法 ' + name);
  11. const end = source.indexOf('\n/**', match.index + match[0].length);
  12. return source.slice(match.index, end < 0 ? source.length : end).replace(/^export /, '');
  13. }
  14. const defaultSourcePath = path.resolve(__dirname, '../src/timecard-repair-lifecycle.oyd.jsx');
  15. const sourcePath = process.argv[2] ? path.resolve(process.argv[2]) : defaultSourcePath;
  16. const source = fs.readFileSync(sourcePath, 'utf8');
  17. let holdTimer = false;
  18. let pendingTimer = null;
  19. const methods = vm.runInNewContext(
  20. ['getRepairContextErrorMessage', 'isRepairContextRetryableError', 'waitForRepairContextRetry', 'loadTimecardRepairStatus']
  21. .map((name) => extractFunction(source, name)).join('\n')
  22. + '\n({getRepairContextErrorMessage, isRepairContextRetryableError, waitForRepairContextRetry, loadTimecardRepairStatus})',
  23. {
  24. REPAIR_DATA_SOURCES: { status: 'getTimecardRepairStatus' },
  25. setTimeout: (callback) => {
  26. if (!holdTimer) {
  27. callback();
  28. return { immediate: true };
  29. }
  30. pendingTimer = { callback, cleared: false };
  31. return pendingTimer;
  32. },
  33. },
  34. );
  35. /** @param {*} response @returns {object} */
  36. function createContext(response) {
  37. return {
  38. state: {},
  39. getTexts: () => ({ repairContextTimeout: '补填工时加载超时,请稍后重试', errUnknown: '未知错误' }),
  40. getRepairContextErrorMessage: methods.getRepairContextErrorMessage,
  41. isRepairContextRetryableError: methods.isRepairContextRetryableError,
  42. waitForRepairContextRetry: methods.waitForRepairContextRetry,
  43. getRepairMonthText: (year, month) => String(year) + String(month).padStart(2, '0'),
  44. getCallableDataSource: () => ({ load: async () => response }),
  45. unwrapConnectorResponse: (value) => value,
  46. };
  47. }
  48. /** @returns {Promise<void>} */
  49. async function main() {
  50. const errorContext = createContext(null);
  51. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, new Error('Read timed out')),
  52. '补填工时加载超时,请稍后重试');
  53. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, { errorCode: 'ETIMEDOUT', code: 'ETIMEDOUT' }),
  54. '补填工时加载超时,请稍后重试');
  55. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, new Error('接口返回 500')), '接口返回 500');
  56. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, {}), '未知错误');
  57. const response = { originalRecords: [{ id: 'original' }], repairRecords: [{ id: 'repair' }], records: [] };
  58. const result = await methods.loadTimecardRepairStatus.call(createContext(response), 'user-1', 2026, 9);
  59. assert.equal(result.configured, true);
  60. assert.equal(result.originalRecords[0].id, 'original');
  61. assert.equal(result.repairRecords[0].id, 'repair');
  62. assert.equal(Object.hasOwn(result, 'closed'), false);
  63. let retryCount = 0;
  64. const retryContext = createContext(response);
  65. retryContext.getCallableDataSource = () => ({
  66. load: async () => {
  67. retryCount += 1;
  68. if (retryCount === 1) throw new Error('Read timed out');
  69. return response;
  70. },
  71. });
  72. const retryResult = await methods.loadTimecardRepairStatus.call(retryContext, 'user-1', 2026, 9);
  73. assert.equal(retryCount, 2);
  74. assert.equal(retryResult.originalRecords[0].id, 'original');
  75. let pendingCount = 0;
  76. const pendingContext = createContext(response);
  77. pendingContext.getCallableDataSource = () => ({
  78. load: async () => {
  79. pendingCount += 1;
  80. return pendingCount < 3 ? { ready: false, retryAfterMillis: 500 } : response;
  81. },
  82. });
  83. const pendingResult = await methods.loadTimecardRepairStatus.call(pendingContext, 'user-1', 2026, 9);
  84. assert.equal(pendingCount, 3);
  85. assert.equal(pendingResult.repairRecords[0].id, 'repair');
  86. let emptyCount = 0;
  87. const emptyContext = createContext(response);
  88. emptyContext.getCallableDataSource = () => ({
  89. load: async () => {
  90. emptyCount += 1;
  91. return emptyCount === 1 ? null : response;
  92. },
  93. });
  94. const emptyRetryResult = await methods.loadTimecardRepairStatus.call(emptyContext, 'user-1', 2026, 9);
  95. assert.equal(emptyCount, 2);
  96. assert.equal(emptyRetryResult.originalRecords[0].id, 'original');
  97. let exhaustedCount = 0;
  98. const exhaustedContext = createContext(null);
  99. exhaustedContext.getCallableDataSource = () => ({
  100. load: async () => {
  101. exhaustedCount += 1;
  102. return { originalRecords: [] };
  103. },
  104. });
  105. await assert.rejects(
  106. methods.loadTimecardRepairStatus.call(exhaustedContext, 'user-1', 2026, 9),
  107. /补填工时加载超时,请稍后重试/,
  108. );
  109. assert.equal(exhaustedCount, 45);
  110. let businessErrorCount = 0;
  111. const businessErrorContext = createContext(null);
  112. businessErrorContext.getCallableDataSource = () => ({
  113. load: async () => {
  114. businessErrorCount += 1;
  115. throw new Error('无权限访问');
  116. },
  117. });
  118. await assert.rejects(
  119. methods.loadTimecardRepairStatus.call(businessErrorContext, 'user-1', 2026, 9),
  120. /无权限访问/,
  121. );
  122. assert.equal(businessErrorCount, 1);
  123. let releasedOnUnmount = false;
  124. const waitContext = {
  125. _tcDisposed: false,
  126. _tcRepairContextPollTimers: [],
  127. };
  128. holdTimer = true;
  129. methods.waitForRepairContextRetry.call(waitContext, 2000).then(() => { releasedOnUnmount = true; });
  130. assert.equal(waitContext._tcRepairContextPollTimers.length, 1);
  131. const entry = waitContext._tcRepairContextPollTimers[0];
  132. assert.equal(entry.timerId, pendingTimer);
  133. pendingTimer.cleared = true;
  134. entry.resolve();
  135. await Promise.resolve();
  136. assert.equal(pendingTimer.cleared, true);
  137. assert.equal(releasedOnUnmount, true);
  138. holdTimer = false;
  139. console.log('PASS 补填上下文 pending 轮询、空响应重试、总超时和业务错误识别');
  140. }
  141. main().catch((error) => { console.error(error); process.exitCode = 1; });