legacy-review-pull.test.mjs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import vm from 'node:vm';
  4. import { readFileSync } from 'node:fs';
  5. const gatewaySource = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8');
  6. const source = gatewaySource.match(/const LEGACY_REVIEW_PULLS=new Map\(\);[\s\S]*?\nconst APP_LIVE_SCOPES=/)?.[0]
  7. .replace(/\nconst APP_LIVE_SCOPES=[\s\S]*$/, '');
  8. assert.ok(source, '未找到旧系统抗遗忘记录拉取实现');
  9. function contextFor(rows, initial = [], filter = 'yhid=42') {
  10. let sequence = 0;
  11. let stored = initial;
  12. const conflicts = [];
  13. const writes = [];
  14. const context = {
  15. console,
  16. URLSearchParams,
  17. AbortSignal,
  18. Map,
  19. Date,
  20. JSON,
  21. String,
  22. Math,
  23. Promise,
  24. DEFAULT_COMPANY_ID: 'company',
  25. number(value, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; },
  26. firstValue(...values) { return values.find((value) => value !== undefined && value !== null && value !== ''); },
  27. legacyLearningIso(value) { const parsed = new Date(String(value).replace(' ', 'T') + '+08:00'); return parsed.toISOString(); },
  28. randomObjectId() { sequence += 1; return `object${String(sequence).padStart(14, '0')}`; },
  29. async appLegacyConfig() { return { url: 'https://legacy.example/api', apiId: 'id', apiKey: 'key' }; },
  30. async fetch(url) {
  31. assert.equal(new URL(url).searchParams.get('myfield2'), filter);
  32. return { ok: true, async text() { return JSON.stringify({ retcode: 1, result: JSON.stringify(rows), page: { itemCount: rows.length, pageCount: 1 } }); } };
  33. },
  34. Psql: {
  35. async none(sql, args) {
  36. if (sql.startsWith('SET LOCAL lock_timeout')) return;
  37. if (sql.includes('ReviewAssignmentConflict')) { conflicts.push(args); return; }
  38. throw Error(sql);
  39. },
  40. async one(sql, args) {
  41. if (sql.includes('pg_advisory_xact_lock')) return {};
  42. 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}};}
  43. if (sql.includes('COUNT(*)') && sql.includes('MemoryPracticeRecord')) return { total: stored.length };
  44. throw new Error(`unexpected one sql: ${sql}`);
  45. },
  46. async query(sql) {
  47. if (sql.includes('SELECT c."generalId"')) return stored.map(r=>({generalId:r.generalId,itemId:r.itemId,taskSnapshot:{...r,objectId:r.taskObjectId||r.objectId}}));
  48. if (sql.includes('SELECT CAST("id" AS text)')) return stored.map((row) => ({ id: row.itemId }));
  49. throw new Error(`unexpected query sql: ${sql}`);
  50. },
  51. },
  52. async mobileAtomicWrite(_input, _current, operations) {
  53. writes.push(operations);
  54. for (let index = 0; index < operations.length; index += 2) {
  55. const addon = operations[index].data;
  56. const common = operations[index + 1].data;
  57. stored = stored.filter((item) => Number(item.generalId) !== Number(common.generalId));
  58. stored.push({ ...common, ...addon, itemId: addon.id });
  59. }
  60. return operations.map((operation) => operation.data);
  61. },
  62. };
  63. context.Psql.tx=async callback=>callback(context.Psql);
  64. vm.runInNewContext(`${source};this.normalize=legacyReviewRecord;this.sync=performLegacyReviewSync;this.syncToday=(...args)=>syncLegacyCoachTodayReviews(...args)`, context);
  65. return { context, writes, conflicts };
  66. }
  67. test('教练刷新仅补齐本人当天的抗遗忘任务', async () => {
  68. const day = '2026-11-08';
  69. const base = { yhid: 42, plid: 155, kywrq: day, CreateTime: day + ' 15:03:00', fxzt: '1' };
  70. const { context, writes } = contextFor([
  71. { ...base, GeneralID: 889405 },
  72. { ...base, GeneralID: 889406, yhid: 43 },
  73. { ...base, GeneralID: 889407, plid: 156 },
  74. { ...base, GeneralID: 889408, kywrq: '2026-11-09' },
  75. ], [], 'plid=155|kywrq=' + day);
  76. const coach = { id: 'coach' };
  77. const result = await context.syncToday(155, coach, day);
  78. assert.equal(result.state, 'current');
  79. assert.equal(result.written, 2);
  80. assert.deepEqual(writes.map(batch => batch[1].match.generalId), [889405, 889406]);
  81. assert.ok(writes.every(batch => batch[0].data.fxzt === '1'));
  82. });
  83. test('旧系统抗遗忘记录只接收当前学员并保留计划与完成状态', () => {
  84. const { context } = contextFor([]);
  85. assert.equal(context.normalize({ GeneralID: 1, yhid: 7 }, 42), null);
  86. const row = context.normalize({
  87. GeneralID: 866548,
  88. CreateTime: '2026-09-24 20:11:52',
  89. Title: '学员@21天抗遗忘@2026-09-24',
  90. yhid: 42,
  91. plid: 155,
  92. orderId: 859224,
  93. kcid: 171,
  94. kywsj: '2026-09-24 19:00',
  95. kywrq: '2026-09-24',
  96. xxjlid: 866545,
  97. fxzt: '1',
  98. wcsj: '2026-09-24 20:11',
  99. }, 42);
  100. assert.equal(row.generalId, 866548);
  101. assert.equal(row.addon.yhid, 42);
  102. assert.equal(row.addon.xxjlid, 866545);
  103. assert.equal(row.addon.fxzt, '1');
  104. assert.equal(row.addon.wcsj, '2026-09-24 20:11');
  105. });
  106. test('抗遗忘记录首次补齐后重复核对不再写入', async () => {
  107. const rows = [{
  108. GeneralID: 866548,
  109. CreateTime: '2026-09-24 20:11:52',
  110. Title: '学员@21天抗遗忘@2026-09-24',
  111. yhid: 42,
  112. plid: 155,
  113. orderId: 859224,
  114. kcid: 171,
  115. kywsj: '2026-09-24 19:00',
  116. kywrq: '2026-09-24',
  117. xxjlid: 866545,
  118. fxzt: '1',
  119. wcsj: '2026-09-24 20:11',
  120. }];
  121. const { context, writes } = contextFor(rows);
  122. const first = await context.sync(42, { id: 'actor' }, true);
  123. const second = await context.sync(42, { id: 'actor' }, true);
  124. assert.equal(first.written, 1);
  125. assert.equal(second.written, 0);
  126. assert.equal(writes.length, 1);
  127. assert.equal(writes[0][0].table, 'MemoryPracticeRecord');
  128. assert.equal(writes[0][1].match.generalId, 866548);
  129. });
  130. test('页面旧接口刷新保留已转交老师与完成状态并记录冲突',async()=>{
  131. 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'}]);
  132. 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);
  133. });