verify-repair-context.cjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. getTexts: () => ({ repairContextTimeout: '补填工时加载超时,请稍后重试', errUnknown: '未知错误' }),
  39. getRepairContextErrorMessage: methods.getRepairContextErrorMessage,
  40. isRepairContextRetryableError: methods.isRepairContextRetryableError,
  41. waitForRepairContextRetry: methods.waitForRepairContextRetry,
  42. getRepairMonthText: (year, month) => String(year) + String(month).padStart(2, '0'),
  43. getCallableDataSource: () => ({ load: async () => response }),
  44. unwrapConnectorResponse: (value) => value,
  45. };
  46. }
  47. /** @returns {Promise<void>} */
  48. async function main() {
  49. const errorContext = createContext(null);
  50. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, new Error('Read timed out')),
  51. '补填工时加载超时,请稍后重试');
  52. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, { errorCode: 'ETIMEDOUT', code: 'ETIMEDOUT' }),
  53. '补填工时加载超时,请稍后重试');
  54. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, new Error('接口返回 500')), '接口返回 500');
  55. assert.equal(methods.getRepairContextErrorMessage.call(errorContext, {}), '未知错误');
  56. const response = { originalRecords: [{ id: 'original' }], repairRecords: [{ id: 'repair' }], records: [] };
  57. const result = await methods.loadTimecardRepairStatus.call(createContext(response), 'user-1', 2026, 9);
  58. assert.equal(result.configured, true);
  59. assert.equal(result.originalRecords[0].id, 'original');
  60. assert.equal(result.repairRecords[0].id, 'repair');
  61. assert.equal(Object.hasOwn(result, 'closed'), false);
  62. let retryCount = 0;
  63. const retryContext = createContext(response);
  64. retryContext.getCallableDataSource = () => ({
  65. load: async () => {
  66. retryCount += 1;
  67. if (retryCount === 1) throw new Error('Read timed out');
  68. return response;
  69. },
  70. });
  71. const retryResult = await methods.loadTimecardRepairStatus.call(retryContext, 'user-1', 2026, 9);
  72. assert.equal(retryCount, 2);
  73. assert.equal(retryResult.originalRecords[0].id, 'original');
  74. let pendingCount = 0;
  75. const pendingContext = createContext(response);
  76. pendingContext.getCallableDataSource = () => ({
  77. load: async () => {
  78. pendingCount += 1;
  79. return pendingCount < 3 ? { ready: false, retryAfterMillis: 500 } : response;
  80. },
  81. });
  82. const pendingResult = await methods.loadTimecardRepairStatus.call(pendingContext, 'user-1', 2026, 9);
  83. assert.equal(pendingCount, 3);
  84. assert.equal(pendingResult.repairRecords[0].id, 'repair');
  85. let emptyCount = 0;
  86. const emptyContext = createContext(response);
  87. emptyContext.getCallableDataSource = () => ({
  88. load: async () => {
  89. emptyCount += 1;
  90. return emptyCount === 1 ? null : response;
  91. },
  92. });
  93. const emptyRetryResult = await methods.loadTimecardRepairStatus.call(emptyContext, 'user-1', 2026, 9);
  94. assert.equal(emptyCount, 2);
  95. assert.equal(emptyRetryResult.originalRecords[0].id, 'original');
  96. let exhaustedCount = 0;
  97. const exhaustedContext = createContext(null);
  98. exhaustedContext.getCallableDataSource = () => ({
  99. load: async () => {
  100. exhaustedCount += 1;
  101. return { originalRecords: [] };
  102. },
  103. });
  104. await assert.rejects(
  105. methods.loadTimecardRepairStatus.call(exhaustedContext, 'user-1', 2026, 9),
  106. /补填工时加载超时,请稍后重试/,
  107. );
  108. assert.equal(exhaustedCount, 45);
  109. let businessErrorCount = 0;
  110. const businessErrorContext = createContext(null);
  111. businessErrorContext.getCallableDataSource = () => ({
  112. load: async () => {
  113. businessErrorCount += 1;
  114. throw new Error('无权限访问');
  115. },
  116. });
  117. await assert.rejects(
  118. methods.loadTimecardRepairStatus.call(businessErrorContext, 'user-1', 2026, 9),
  119. /无权限访问/,
  120. );
  121. assert.equal(businessErrorCount, 1);
  122. let releasedOnUnmount = false;
  123. const waitContext = {
  124. _tcDisposed: false,
  125. _tcRepairContextPollTimers: [],
  126. };
  127. holdTimer = true;
  128. methods.waitForRepairContextRetry.call(waitContext, 2000).then(() => { releasedOnUnmount = true; });
  129. assert.equal(waitContext._tcRepairContextPollTimers.length, 1);
  130. const entry = waitContext._tcRepairContextPollTimers[0];
  131. assert.equal(entry.timerId, pendingTimer);
  132. pendingTimer.cleared = true;
  133. entry.resolve();
  134. await Promise.resolve();
  135. assert.equal(pendingTimer.cleared, true);
  136. assert.equal(releasedOnUnmount, true);
  137. holdTimer = false;
  138. console.log('PASS 补填上下文 pending 轮询、空响应重试、总超时和业务错误识别');
  139. }
  140. main().catch((error) => { console.error(error); process.exitCode = 1; });