'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const babel = require('/Users/malk/.nvm/versions/node/v22.12.0/lib/node_modules/openyida/node_modules/@babel/standalone'); const helperNames = ['closingNativeMonthText', 'closingNativeTimestamp', 'closingNativeRecordData', 'closingNativeRecordMonth', 'closingNativePage', 'summarizeNativeMonthClosing', 'queryNativeMonthClosingStatus']; const APP_TYPE = 'APP_ZQ3I7XO2RSHDJ4QDEVNB'; /** @param {string} source @returns {Map} Exact exported function bodies. */ function functions(source) { const ast = babel.transform(source, { ast: true, code: false, parserOpts: { sourceType: 'module', plugins: ['jsx'] } }).ast; return new Map(ast.program.body.filter(node => node.type === 'ExportNamedDeclaration' && node.declaration && node.declaration.type === 'FunctionDeclaration') .map(node => [node.declaration.id.name, source.slice(node.start, node.end)])); } /** @param {string} source @returns {Object} Isolated frontend methods. */ function methods(source) { const exported = functions(source); const names = helperNames.concat(['loadMonthClosingStatus', 'isMonthLocked', 'isRepairMonthBlocked', 'isCurrentMonthBlocked']) .filter(name => exported.has(name)); const code = names.map(name => exported.get(name).replace(/^export /, '')).join('\n'); return vm.runInNewContext(code + '\n({' + names.join(',') + '})', { APP_TYPE }); } /** @param {Object} data @returns {*} Plain JSON for cross-context assertions. */ function plain(data) { return JSON.parse(JSON.stringify(data)); } /** @param {string} id @param {string} status @param {string} action @param {number} time @param {string} approved @returns {Object} Fixture. */ function record(id, status = 'COMPLETED', action = '封账', time = 1790000000000, approved = 'agree') { return { processInstanceId: id, instanceStatus: status, approvedResult: approved, gmtModified: time, data: { dateField_mt6wzb17: 1785513600000, radioField_mt6wzb18: action } }; } /** @param {Object} m @param {Array<*>} replies @returns {Object} Native API stub and call log. */ function context(m, replies) { const calls = []; const obj = Object.assign({}, m, { getCurrentBusinessMonthText: () => '202610', calls, utils: { yida: { getProcessInstances: async params => { calls.push(params); const reply = replies[Math.min(calls.length - 1, replies.length - 1)]; if (reply instanceof Error) throw reply; return reply; } } }, getCallableDataSource: () => { throw new Error('不得调用旧封账连接器'); } }); return obj; } /** @param {string} filename @returns {Promise} Exercise state, paging and failure contract. */ async function verify(filename) { const source = fs.readFileSync(filename, 'utf8'); const m = methods(source); const count = record('closed'); const ctx = context(m, [{ success: true, content: { data: [count], totalCount: 1 } }]); const closed = plain(await ctx.loadMonthClosingStatus(2026, 8)); assert.deepEqual(closed, { monthText: '202608', closed: true, currentStatus: '封账', approvedClosingCount: 1, approvedReopeningCount: 0, runningClosingCount: 0 }); const params = ctx.calls[0]; assert.equal(params.pageSize, 100); assert.equal(params.formUuid, 'FORM-275BDAFE67FE4226B6BCF2BC6EEFFD4B4215'); assert.deepEqual(JSON.parse(params.searchFieldJson)[0].value, [1785513600000, 1788191999999]); const empty = plain(await context(m, [{ data: [], totalCount: 0 }]).loadMonthClosingStatus('2026', '10')); assert.equal(empty.closed, false); assert.equal(empty.monthText, '202610'); const running = ctx.summarizeNativeMonthClosing('202608', [record('run', 'RUNNING', '启封')]); assert.equal(running.runningClosingCount, 1); assert.equal(running.closed, false); const reopened = ctx.summarizeNativeMonthClosing('202608', [count, record('open', 'COMPLETED', '启封', count.gmtModified + 1)]); assert.equal(reopened.closed, false); assert.equal(reopened.approvedClosingCount, 1); assert.equal(reopened.approvedReopeningCount, 1); const closedAgain = ctx.summarizeNativeMonthClosing('202608', [count, record('open', 'COMPLETED', '启封', count.gmtModified + 1), record('closed-again', 'COMPLETED', '封账', count.gmtModified + 2)]); assert.equal(closedAgain.closed, true); const ignored = ctx.summarizeNativeMonthClosing('202608', [record('rejected', 'COMPLETED', '封账', undefined, 'disagree'), record('terminated', 'TERMINATED'), record('error', 'ERROR')]); assert.equal(ignored.approvedClosingCount, 0); assert.equal(ignored.closed, false); const otherMonth = record('other'); otherMonth.data.dateField_mt6wzb17 = 1788192000000; assert.equal(ctx.summarizeNativeMonthClosing('202608', [otherMonth]).approvedClosingCount, 0); assert.equal(ctx.closingNativeTimestamp('1790000000'), 1790000000000); assert.equal(ctx.closingNativeTimestamp('2026-08-01T00:00:00+08:00'), 1785513600000); assert.equal(ctx.closingNativeRecordMonth('20260801'), '202608'); const priority = record('priority'); priority.modifiedTimeGMT = count.gmtModified - 1; assert.equal(ctx.summarizeNativeMonthClosing('202608', [record('open-first', 'COMPLETED', '启封', count.gmtModified), priority]).closed, false); const tied = ctx.summarizeNativeMonthClosing('202608', [count, record('tie-open', 'COMPLETED', '启封', count.gmtModified)]); assert.equal(tied.closed, true, '与后端一致,时间相等不覆盖先出现动作'); const firstPage = Array.from({ length: 100 }, (_, index) => record('p-' + index)); const paged = context(m, [{ data: firstPage, totalCount: 101 }, { data: [record('last')], totalCount: 101 }]); assert.equal((await paged.loadMonthClosingStatus(2026, 8)).approvedClosingCount, 101); assert.equal(paged.calls.length, 2); for (const reply of [null, {}, { success: false, data: [], totalCount: 0 }, { errorCode: 'NO_PERMISSION' }, { data: [] }, { data: [], totalCount: null }, { data: [], totalCount: -1 }, new Error('timeout')]) { await assert.rejects(context(m, [reply]).loadMonthClosingStatus(2026, 8)); } await assert.rejects(context(m, [{ data: firstPage, totalCount: 101 }, { data: [], totalCount: 101 }]).loadMonthClosingStatus(2026, 8)); await assert.rejects(context(m, [{ data: firstPage, totalCount: 101 }, { data: [count], totalCount: 102 }]).loadMonthClosingStatus(2026, 8)); await assert.rejects(context(m, [{ data: [count, count], totalCount: 2 }]).loadMonthClosingStatus(2026, 8)); await assert.rejects(context(m, [{ data: [{ processInstanceId: 'hidden', data: {} }], totalCount: 1 }]).loadMonthClosingStatus(2026, 8)); await assert.rejects(context(m, [{ data: [], totalCount: 0 }]).loadMonthClosingStatus(2026, 13)); const failed = context(m, [new Error('timeout'), { data: [], totalCount: 0 }]); await assert.rejects(failed.loadMonthClosingStatus(2026, 10)); assert.equal((await failed.loadMonthClosingStatus(2026, 10)).closed, false, '失败不阻断后续串行队列'); const openCurrent = Object.assign({}, empty, { monthText: '202610' }); if (m.isMonthLocked) { assert.equal(ctx.isMonthLocked(closed), true); assert.equal(ctx.isMonthLocked(running), true); assert.equal(ctx.isMonthLocked(empty), false); } if (m.isCurrentMonthBlocked) { assert.equal(ctx.isCurrentMonthBlocked(openCurrent), false); assert.equal(ctx.isCurrentMonthBlocked(closed), true); } if (m.isRepairMonthBlocked) { assert.equal(ctx.isRepairMonthBlocked(closed), false); assert.equal(ctx.isRepairMonthBlocked(Object.assign({}, running, { monthText: '202608' })), false); assert.equal(ctx.isRepairMonthBlocked(openCurrent), filename.includes('change') ? false : true); } process.stdout.write('PASS 原生封账契约、状态归约、分页、异常保护、原准入规则:' + path.basename(filename) + '\n'); } /** @returns {Promise} Verify requested sources. */ async function main() { const files = process.argv.slice(2); if (!files.length) files.push(...['timecard-repair-lifecycle.oyd.jsx', 'timecard-repair.oyd.jsx', 'timecard-change.oyd.jsx'] .map(name => path.resolve(__dirname, '../src', name))); for (const file of files) await verify(file); } main().catch(error => { console.error(error); process.exitCode = 1; });