verify-native-closing.cjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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. const babel = require('/Users/malk/.nvm/versions/node/v22.12.0/lib/node_modules/openyida/node_modules/@babel/standalone');
  7. const helperNames = ['closingNativeMonthText', 'closingNativeTimestamp', 'closingNativeRecordData',
  8. 'closingNativeRecordMonth', 'closingNativePage', 'summarizeNativeMonthClosing', 'queryNativeMonthClosingStatus'];
  9. const APP_TYPE = 'APP_ZQ3I7XO2RSHDJ4QDEVNB';
  10. /** @param {string} source @returns {Map<string,string>} Exact exported function bodies. */
  11. function functions(source) {
  12. const ast = babel.transform(source, { ast: true, code: false, parserOpts: { sourceType: 'module', plugins: ['jsx'] } }).ast;
  13. return new Map(ast.program.body.filter(node => node.type === 'ExportNamedDeclaration'
  14. && node.declaration && node.declaration.type === 'FunctionDeclaration')
  15. .map(node => [node.declaration.id.name, source.slice(node.start, node.end)]));
  16. }
  17. /** @param {string} source @returns {Object} Isolated frontend methods. */
  18. function methods(source) {
  19. const exported = functions(source);
  20. const names = helperNames.concat(['loadMonthClosingStatus', 'isMonthLocked', 'isRepairMonthBlocked', 'isCurrentMonthBlocked'])
  21. .filter(name => exported.has(name));
  22. const code = names.map(name => exported.get(name).replace(/^export /, '')).join('\n');
  23. return vm.runInNewContext(code + '\n({' + names.join(',') + '})', { APP_TYPE });
  24. }
  25. /** @param {Object} data @returns {*} Plain JSON for cross-context assertions. */
  26. function plain(data) { return JSON.parse(JSON.stringify(data)); }
  27. /** @param {string} id @param {string} status @param {string} action @param {number} time @param {string} approved @returns {Object} Fixture. */
  28. function record(id, status = 'COMPLETED', action = '封账', time = 1790000000000, approved = 'agree') {
  29. return { processInstanceId: id, instanceStatus: status, approvedResult: approved, gmtModified: time,
  30. data: { dateField_mt6wzb17: 1785513600000, radioField_mt6wzb18: action } };
  31. }
  32. /** @param {Object} m @param {Array<*>} replies @returns {Object} Native API stub and call log. */
  33. function context(m, replies) {
  34. const calls = [];
  35. const obj = Object.assign({}, m, { getCurrentBusinessMonthText: () => '202610', calls,
  36. utils: { yida: { getProcessInstances: async params => {
  37. calls.push(params);
  38. const reply = replies[Math.min(calls.length - 1, replies.length - 1)];
  39. if (reply instanceof Error) throw reply;
  40. return reply;
  41. } } }, getCallableDataSource: () => { throw new Error('不得调用旧封账连接器'); } });
  42. return obj;
  43. }
  44. /** @param {string} filename @returns {Promise<void>} Exercise state, paging and failure contract. */
  45. async function verify(filename) {
  46. const source = fs.readFileSync(filename, 'utf8');
  47. const m = methods(source);
  48. const count = record('closed');
  49. const ctx = context(m, [{ success: true, content: { data: [count], totalCount: 1 } }]);
  50. const closed = plain(await ctx.loadMonthClosingStatus(2026, 8));
  51. assert.deepEqual(closed, { monthText: '202608', closed: true, currentStatus: '封账',
  52. approvedClosingCount: 1, approvedReopeningCount: 0, runningClosingCount: 0 });
  53. const params = ctx.calls[0];
  54. assert.equal(params.pageSize, 100);
  55. assert.equal(params.formUuid, 'FORM-275BDAFE67FE4226B6BCF2BC6EEFFD4B4215');
  56. assert.deepEqual(JSON.parse(params.searchFieldJson)[0].value, [1785513600000, 1788191999999]);
  57. const empty = plain(await context(m, [{ data: [], totalCount: 0 }]).loadMonthClosingStatus('2026', '10'));
  58. assert.equal(empty.closed, false);
  59. assert.equal(empty.monthText, '202610');
  60. const running = ctx.summarizeNativeMonthClosing('202608', [record('run', 'RUNNING', '启封')]);
  61. assert.equal(running.runningClosingCount, 1);
  62. assert.equal(running.closed, false);
  63. const reopened = ctx.summarizeNativeMonthClosing('202608', [count, record('open', 'COMPLETED', '启封', count.gmtModified + 1)]);
  64. assert.equal(reopened.closed, false);
  65. assert.equal(reopened.approvedClosingCount, 1);
  66. assert.equal(reopened.approvedReopeningCount, 1);
  67. const closedAgain = ctx.summarizeNativeMonthClosing('202608', [count, record('open', 'COMPLETED', '启封', count.gmtModified + 1),
  68. record('closed-again', 'COMPLETED', '封账', count.gmtModified + 2)]);
  69. assert.equal(closedAgain.closed, true);
  70. const ignored = ctx.summarizeNativeMonthClosing('202608', [record('rejected', 'COMPLETED', '封账', undefined, 'disagree'),
  71. record('terminated', 'TERMINATED'), record('error', 'ERROR')]);
  72. assert.equal(ignored.approvedClosingCount, 0);
  73. assert.equal(ignored.closed, false);
  74. const otherMonth = record('other'); otherMonth.data.dateField_mt6wzb17 = 1788192000000;
  75. assert.equal(ctx.summarizeNativeMonthClosing('202608', [otherMonth]).approvedClosingCount, 0);
  76. assert.equal(ctx.closingNativeTimestamp('1790000000'), 1790000000000);
  77. assert.equal(ctx.closingNativeTimestamp('2026-08-01T00:00:00+08:00'), 1785513600000);
  78. assert.equal(ctx.closingNativeRecordMonth('20260801'), '202608');
  79. const priority = record('priority'); priority.modifiedTimeGMT = count.gmtModified - 1;
  80. assert.equal(ctx.summarizeNativeMonthClosing('202608', [record('open-first', 'COMPLETED', '启封', count.gmtModified), priority]).closed, false);
  81. const tied = ctx.summarizeNativeMonthClosing('202608', [count, record('tie-open', 'COMPLETED', '启封', count.gmtModified)]);
  82. assert.equal(tied.closed, true, '与后端一致,时间相等不覆盖先出现动作');
  83. const firstPage = Array.from({ length: 100 }, (_, index) => record('p-' + index));
  84. const paged = context(m, [{ data: firstPage, totalCount: 101 }, { data: [record('last')], totalCount: 101 }]);
  85. assert.equal((await paged.loadMonthClosingStatus(2026, 8)).approvedClosingCount, 101);
  86. assert.equal(paged.calls.length, 2);
  87. for (const reply of [null, {}, { success: false, data: [], totalCount: 0 }, { errorCode: 'NO_PERMISSION' },
  88. { data: [] }, { data: [], totalCount: null }, { data: [], totalCount: -1 }, new Error('timeout')]) {
  89. await assert.rejects(context(m, [reply]).loadMonthClosingStatus(2026, 8));
  90. }
  91. await assert.rejects(context(m, [{ data: firstPage, totalCount: 101 }, { data: [], totalCount: 101 }]).loadMonthClosingStatus(2026, 8));
  92. await assert.rejects(context(m, [{ data: firstPage, totalCount: 101 }, { data: [count], totalCount: 102 }]).loadMonthClosingStatus(2026, 8));
  93. await assert.rejects(context(m, [{ data: [count, count], totalCount: 2 }]).loadMonthClosingStatus(2026, 8));
  94. await assert.rejects(context(m, [{ data: [{ processInstanceId: 'hidden', data: {} }], totalCount: 1 }]).loadMonthClosingStatus(2026, 8));
  95. await assert.rejects(context(m, [{ data: [], totalCount: 0 }]).loadMonthClosingStatus(2026, 13));
  96. const failed = context(m, [new Error('timeout'), { data: [], totalCount: 0 }]);
  97. await assert.rejects(failed.loadMonthClosingStatus(2026, 10));
  98. assert.equal((await failed.loadMonthClosingStatus(2026, 10)).closed, false, '失败不阻断后续串行队列');
  99. const openCurrent = Object.assign({}, empty, { monthText: '202610' });
  100. if (m.isMonthLocked) { assert.equal(ctx.isMonthLocked(closed), true); assert.equal(ctx.isMonthLocked(running), true); assert.equal(ctx.isMonthLocked(empty), false); }
  101. if (m.isCurrentMonthBlocked) { assert.equal(ctx.isCurrentMonthBlocked(openCurrent), false); assert.equal(ctx.isCurrentMonthBlocked(closed), true); }
  102. if (m.isRepairMonthBlocked) {
  103. assert.equal(ctx.isRepairMonthBlocked(closed), false);
  104. assert.equal(ctx.isRepairMonthBlocked(Object.assign({}, running, { monthText: '202608' })), false);
  105. assert.equal(ctx.isRepairMonthBlocked(openCurrent), filename.includes('change') ? false : true);
  106. }
  107. process.stdout.write('PASS 原生封账契约、状态归约、分页、异常保护、原准入规则:' + path.basename(filename) + '\n');
  108. }
  109. /** @returns {Promise<void>} Verify requested sources. */
  110. async function main() {
  111. const files = process.argv.slice(2);
  112. if (!files.length) files.push(...['timecard-repair-lifecycle.oyd.jsx', 'timecard-repair.oyd.jsx', 'timecard-change.oyd.jsx']
  113. .map(name => path.resolve(__dirname, '../src', name)));
  114. for (const file of files) await verify(file);
  115. }
  116. main().catch(error => { console.error(error); process.exitCode = 1; });