import test from 'node:test'; import assert from 'node:assert/strict'; import vm from 'node:vm'; import { readFileSync } from 'node:fs'; const gatewaySource = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8'); const source = gatewaySource.match(/const LEGACY_REVIEW_PULLS=new Map\(\);[\s\S]*?\nconst APP_LIVE_SCOPES=/)?.[0] .replace(/\nconst APP_LIVE_SCOPES=[\s\S]*$/, ''); assert.ok(source, '未找到旧系统抗遗忘记录拉取实现'); function contextFor(rows, initial = [], filter = 'yhid=42') { let sequence = 0; let stored = initial; const conflicts = []; const writes = []; const context = { console, URLSearchParams, AbortSignal, Map, Date, JSON, String, Math, Promise, DEFAULT_COMPANY_ID: 'company', number(value, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; }, firstValue(...values) { return values.find((value) => value !== undefined && value !== null && value !== ''); }, legacyLearningIso(value) { const parsed = new Date(String(value).replace(' ', 'T') + '+08:00'); return parsed.toISOString(); }, randomObjectId() { sequence += 1; return `object${String(sequence).padStart(14, '0')}`; }, async appLegacyConfig() { return { url: 'https://legacy.example/api', apiId: 'id', apiKey: 'key' }; }, async fetch(url) { assert.equal(new URL(url).searchParams.get('myfield2'), filter); return { ok: true, async text() { return JSON.stringify({ retcode: 1, result: JSON.stringify(rows), page: { itemCount: rows.length, pageCount: 1 } }); } }; }, Psql: { async none(sql, args) { if (sql.startsWith('SET LOCAL lock_timeout')) return; if (sql.includes('ReviewAssignmentConflict')) { conflicts.push(args); return; } throw Error(sql); }, async one(sql, args) { if (sql.includes('pg_advisory_xact_lock')) return {}; if(sql.includes('xs_legacy_review_patch')) {const item=stored.find(r=>(r.taskObjectId||r.objectId)===args[1]);if(Number(item?.assignmentVersion)>0)return {result:{applied:false,reason:'protected'}};Object.assign(item,JSON.parse(args[2]));return {result:{applied:true}};} if (sql.includes('COUNT(*)') && sql.includes('MemoryPracticeRecord')) return { total: stored.length }; throw new Error(`unexpected one sql: ${sql}`); }, async query(sql) { if (sql.includes('SELECT c."generalId"')) return stored.map(r=>({generalId:r.generalId,itemId:r.itemId,taskSnapshot:{...r,objectId:r.taskObjectId||r.objectId}})); if (sql.includes('SELECT CAST("id" AS text)')) return stored.map((row) => ({ id: row.itemId })); throw new Error(`unexpected query sql: ${sql}`); }, }, async mobileAtomicWrite(_input, _current, operations) { writes.push(operations); for (let index = 0; index < operations.length; index += 2) { const addon = operations[index].data; const common = operations[index + 1].data; stored = stored.filter((item) => Number(item.generalId) !== Number(common.generalId)); stored.push({ ...common, ...addon, itemId: addon.id }); } return operations.map((operation) => operation.data); }, }; context.Psql.tx=async callback=>callback(context.Psql); vm.runInNewContext(`${source};this.normalize=legacyReviewRecord;this.sync=performLegacyReviewSync;this.syncToday=(...args)=>syncLegacyCoachTodayReviews(...args)`, context); return { context, writes, conflicts }; } test('教练刷新仅补齐本人当天的抗遗忘任务', async () => { const day = '2026-11-08'; const base = { yhid: 42, plid: 155, kywrq: day, CreateTime: day + ' 15:03:00', fxzt: '1' }; const { context, writes } = contextFor([ { ...base, GeneralID: 889405 }, { ...base, GeneralID: 889406, yhid: 43 }, { ...base, GeneralID: 889407, plid: 156 }, { ...base, GeneralID: 889408, kywrq: '2026-11-09' }, ], [], 'plid=155|kywrq=' + day); const coach = { id: 'coach' }; const result = await context.syncToday(155, coach, day); assert.equal(result.state, 'current'); assert.equal(result.written, 2); assert.deepEqual(writes.map(batch => batch[1].match.generalId), [889405, 889406]); assert.ok(writes.every(batch => batch[0].data.fxzt === '1')); }); test('旧系统抗遗忘记录只接收当前学员并保留计划与完成状态', () => { const { context } = contextFor([]); assert.equal(context.normalize({ GeneralID: 1, yhid: 7 }, 42), null); const row = context.normalize({ GeneralID: 866548, CreateTime: '2026-09-24 20:11:52', Title: '学员@21天抗遗忘@2026-09-24', yhid: 42, plid: 155, orderId: 859224, kcid: 171, kywsj: '2026-09-24 19:00', kywrq: '2026-09-24', xxjlid: 866545, fxzt: '1', wcsj: '2026-09-24 20:11', }, 42); assert.equal(row.generalId, 866548); assert.equal(row.addon.yhid, 42); assert.equal(row.addon.xxjlid, 866545); assert.equal(row.addon.fxzt, '1'); assert.equal(row.addon.wcsj, '2026-09-24 20:11'); }); test('抗遗忘记录首次补齐后重复核对不再写入', async () => { const rows = [{ GeneralID: 866548, CreateTime: '2026-09-24 20:11:52', Title: '学员@21天抗遗忘@2026-09-24', yhid: 42, plid: 155, orderId: 859224, kcid: 171, kywsj: '2026-09-24 19:00', kywrq: '2026-09-24', xxjlid: 866545, fxzt: '1', wcsj: '2026-09-24 20:11', }]; const { context, writes } = contextFor(rows); const first = await context.sync(42, { id: 'actor' }, true); const second = await context.sync(42, { id: 'actor' }, true); assert.equal(first.written, 1); assert.equal(second.written, 0); assert.equal(writes.length, 1); assert.equal(writes[0][0].table, 'MemoryPracticeRecord'); assert.equal(writes[0][1].match.generalId, 866548); }); test('页面旧接口刷新保留已转交老师与完成状态并记录冲突',async()=>{ const {context,writes,conflicts}=contextFor([{GeneralID:99,yhid:42,plid:155,fxzt:'1',CreateTime:'2026-10-13 10:00:00'}],[{generalId:99,itemId:99,taskObjectId:'task99',assignmentVersion:1,plid:166,fxzt:'0'}]); const result=await context.sync(42,{id:'actor'},true);assert.equal(result.written,0);assert.equal(result.conflicts,1);assert.equal(result.state,'conflict');assert.equal(writes.length,0);assert.equal(conflicts.length,1); });