const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const test = require('node:test') const { parse, compileScript, compileTemplate, compileStyle } = require('@vue/compiler-sfc') const ts = require('typescript') const vue = require('vue') const filename = path.join(__dirname, '../src/views/first_visit/wecom_promotion/index.vue') const { descriptor, errors } = parse(fs.readFileSync(filename, 'utf8'), { filename }) assert.deepEqual(errors, []) const script = compileScript(descriptor, { id: 'member-sync-test' }) const template = compileTemplate({ source: descriptor.template.content, filename, id: 'member-sync-test', compilerOptions: { bindingMetadata: script.bindings } }) assert.deepEqual(template.errors, []) assert.deepEqual(compileStyle({ source: descriptor.styles[0].content, filename, id: 'member-sync-test', scoped: true, preprocessLang: 'scss' }).errors, []) function loadModule(source, mockRequire) { const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText const module = { exports: {} } new Function('require', 'module', 'exports', compiled)(mockRequire, module, module.exports) return module.exports } const automation = loadModule(fs.readFileSync(path.join(path.dirname(filename), 'components/promotion-automation.ts'), 'utf8'), require) const member = (id, enabled = 1) => ({ id, admin_id: id, userid: `member-${id}`, name: `医助 ${id}`, enabled, reception_available: enabled === 1, is_in_remote_range: true }) const pool = (id = 1) => ({ id, name: `方案 ${id}`, status: 1, can_operate: true, can_manage_access: true, member_admin_ids: [1, 2], member_rules: [member(1), member(2, 0)], official_link: { range_userids: ['member-1', 'member-2'] }, dispatch_sync: { status: 1 } }) function page(api = {}, pools = [pool()]) { const messages = [] const emitMessage = (type, value) => messages.push({ type, message: typeof value === 'string' ? value : value.message }) const ElMessage = (value) => emitMessage(value.type, value) for (const type of ['success', 'warning', 'error']) ElMessage[type] = (value) => emitMessage(type, value) const instance = loadModule(script.content, (name) => { if (name === 'vue') return { ...vue, onMounted() {} } if (name === 'element-plus') return { ElMessage, ElMessageBox: {} } if (name === '@element-plus/icons-vue') return {} if (name === '@/api/first_visit') return { wecomPromotionOverview: async () => ({ pools }), ...api } if (name.endsWith('.vue')) return {} if (name === './components/promotion-automation') return automation throw new Error(`Unexpected dependency: ${name}`) }).default.setup({}, { expose() {} }) Object.assign(instance.overview, { pools }) instance.selectedPoolId.value = pools[0]?.id return { state: instance, messages } } test('disabled local member still in remote snapshot is explicitly pending removal', () => { const { state } = page() assert.equal(state.routeStatus(member(2, 0)).label, '待移出(仍在企微)') assert.equal(state.routeStatus({ ...member(1), is_in_remote_range: '0' }).label, '待加入企微') assert.match(state.selectedSyncState.value.description, /当前计划:医助 1;上次企微确认:医助 1、医助 2/) state.overview.pools[0].dispatch_sync = { status: 3, last_error: '可信 IP 校验失败' } assert.match(state.selectedSyncState.value.description, /可信 IP 校验失败/) }) test('single toggles never infer remote success from an empty error or planned dispatch', async () => { const calls = [] const { state, messages } = page({ wecomPromotionToggleMember: async (payload) => { calls.push(payload); return { sync_error: '', dispatch: { queued: true } } } }) await state.handleMemberToggle(member(2), false) assert.deepEqual(calls, [{ id: 2, status: 0 }]) assert.equal(messages.at(-1).type, 'warning') assert.match(messages.at(-1).message, /尚未确认同步/) assert.doesNotMatch(messages.at(-1).message, /已确认同步|已重新计算/) assert.match(state.operationResultText(state.operationResults.value[0]), /尚未确认同步/) }) test('single rule save reports explicit remote confirmation and preserves precise failures', async () => { const { state, messages } = page({ wecomPromotionSaveMember: async () => ({ sync_status: 'failed', sync_error: '企微成员范围不一致' }) }) Object.assign(state.memberForm, { id: 2, active_range: [] }) await state.saveMemberRule() assert.equal(messages.at(-1).type, 'warning') assert.match(messages.at(-1).message, /企微成员范围不一致/) state.notifySavedResult('本地已保存', { sync_status: 'synced' }) assert.equal(messages.at(-1).type, 'success') assert.match(messages.at(-1).message, /企微成员范围已确认同步/) }) test('pool saves with queued work retain a warning instead of implying official link completion', async () => { const { state, messages } = page({ wecomPromotionSavePool: async () => ({ id: 1, sync_status: 'pending', sync_queued: true, sync_error: '' }) }) Object.assign(state.poolForm, { id: 1, name: '方案 1', member_admin_ids: [1] }) await state.savePool() assert.equal(state.poolDialogVisible.value, false) assert.equal(messages.at(-1).type, 'warning') assert.match(messages.at(-1).message, /尚未确认同步/) assert.equal(state.operationResults.value[0].sync_queued, true) }) test('manual retry targets only its pool and does not import remote links', async () => { const calls = [] const { state, messages } = page({ wecomPromotionSyncMemberRange: async (payload) => { calls.push(payload); return { pool_id: 1, sync_status: 'synced', sync_error: '', sync_queued: false } } }) await state.syncMemberRange(1) assert.deepEqual(calls, [{ pool_id: 1 }]) assert.equal(state.operationResults.value[0].sync_status, 'synced') assert.equal(state.syncingPoolId.value, 0) assert.equal(messages.at(-1).type, 'success') state.overview.pools[0].can_operate = false await state.syncMemberRange(1) assert.equal(calls.length, 1) }) test('batch sync awaits all queued successful pools with at most two concurrent requests', async () => { const pending = [] let active = 0 let maximum = 0 const { state } = page({ wecomPromotionSyncMemberRange: ({ pool_id }) => new Promise((resolve) => { active++ maximum = Math.max(maximum, active) pending.push({ id: pool_id, finish: (result) => { active--; resolve({ pool_id, ...result }) } }) }) }) const results = [1, 2, 3].map((id) => ({ id, name: `方案 ${id}`, success: true, sync_queued: true })) results.push({ id: 4, name: '保存失败方案', success: false, sync_queued: true, error: '无权限' }) const saving = state.syncBatchResults(results) assert.deepEqual(pending.map((item) => item.id), [1, 2]) pending[0].finish({ sync_status: 'synced', sync_error: '' }) await new Promise((resolve) => setImmediate(resolve)) assert.deepEqual(pending.map((item) => item.id), [1, 2, 3]) assert.equal(state.batchSyncProgress.completed, 1) pending[1].finish({ sync_status: 'failed', sync_error: '范围校验失败' }) pending[2].finish({ sync_status: 'pending', sync_error: '' }) await saving assert.equal(maximum, 2) assert.equal(state.batchSyncProgress.completed, 3) assert.deepEqual(state.operationResults.value.map((item) => item.sync_status), ['synced', 'failed', 'pending', undefined]) assert.match(state.operationResultText(state.operationResults.value[1]), /范围校验失败/) assert.match(state.operationResultText(state.operationResults.value[3]), /本地保存失败:无权限/) }) test('batch save retains partial failures and never reports queued work as remote success', async () => { const pools = [pool(1), pool(2), pool(3)] const syncedIds = [] const { state, messages } = page({ wecomPromotionBatchUpdatePools: async () => ({ updated: 2, failed: 1, member_updated: 2, results: [ { id: 1, name: '方案 1', success: true, sync_queued: true }, { id: 2, name: '方案 2', success: true, sync_queued: true }, { id: 3, name: '方案 3', success: false, error: '保存失败' } ] }), wecomPromotionSyncMemberRange: async ({ pool_id }) => { syncedIds.push(pool_id) if (pool_id === 2) throw '企微请求超时' return { pool_id, sync_status: 'synced', sync_error: '', sync_queued: false } } }, pools) Object.assign(state.batchConfigApply, { member_status: true }) Object.assign(state.batchConfigForm, { pool_ids: [1, 2, 3], member_admin_ids: [2], member_status: 0 }) state.batchConfigDialogVisible.value = true await state.saveBatchConfig() assert.deepEqual(syncedIds, [1, 2]) assert.equal(state.savingBatchConfig.value, false) assert.equal(state.batchConfigDialogVisible.value, false) assert.deepEqual(state.selectedPoolIds.value, [2, 3]) assert.match(messages.at(-1).message, /企微已确认同步 1 个,1 个尚未确认同步;1 个本地保存失败/) assert.equal(messages.at(-1).type, 'warning') assert.match(state.operationResults.value[1].sync_error, /企微请求超时/) })