| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 |
- // Executed by the scoped release runner. Only disposable fixture IDs are mutated.
- import assert from 'node:assert/strict';
- import {randomUUID,randomBytes} from 'node:crypto';
- export async function smokeLessonRecords({parse,call,scoped,co,save}){
- const suffix=Date.now(),prefix='lrtest_'+suffix,uid=990000000+suffix%1000000,users=[],checks=[];
- const ok=(value,label)=>{assert.ok(value,label);checks.push(label);console.log('PASS '+label);};
- const sql=async(query,args=[])=>scoped(`return response.json({success:true,data:await Psql.query(${JSON.stringify(query)},${JSON.stringify(args)})});`);
- const ops=async(operation,params={})=>(await call('xiaoshu/ops/gateway-v3',{operation,...params})).data;
- const mobile=async(u,action,params={})=>(await call('xiaoshu/app/gateway',{action,...params},u.sessionToken)).result;
- const taskId=i=>prefix+'_task'+i,gid=i=>uid*100+i;
- try{
- for(const [i,kind] of ['member','coach','coach','member'].entries()){
- const value=await parse('/users','POST',{username:prefix+'_'+i,password:randomBytes(40).toString('base64url'),company:{__type:'Pointer',className:'Company',objectId:co},realName:'隔离上课记录验收'+i,identityType:kind,legacyGroupId:kind==='coach'?3:1,legacyUserId:uid+i,legacyUserData:{UserID:uid+i,GroupID:kind==='coach'?3:1,Purse:3,SilverCoin:3,UserPoint:3,UserExp:3}});users.push({...value,uid:uid+i});
- }
- save('isolated-fixtures.json',{users,prefix});
- await sql(`INSERT INTO "DailyStudyRecord"("objectId","sourceKey",company,id,"userId",dsid,pl,learned,xxqs,"createdAt","updatedAt") VALUES($1,$1,$2,$3,$4,$5,$6,1,$7,now(),now())`,[prefix+'_study',co,gid(1),uid,String(gid(2)),String(uid+1),JSON.stringify([{GeneralID:1,Title:'hello',check:1}])]);
- await sql(`INSERT INTO "CommonModel"("objectId","sourceKey",company,"generalId","itemId","modelId","nodeId",title,status,"createdAt","updatedAt") VALUES($1,$1,$2,$3,$3,56,291,'隔离学习记录',99,now(),now())`,[prefix+'_studyCommon',co,gid(1)]);
- // 25 classes prove cross-page history; only the first owns the review tasks.
- for(let i=0;i<25;i++)await sql(`INSERT INTO "LessonRecord"("objectId","sourceKey",company,id,xymz,jsmz,yyds,kclx,kcmc,pldp,pjnr,plpjsj,"createdAt","updatedAt") VALUES($1,$1,$2,$3,$4,$5,$6,'2','隔离课程','完整老师反馈','完整学生评价',$7,now(),now())`,[prefix+'_lesson'+i,co,gid(20+i),String(uid),String(uid+1),String(gid(2+i)),`2026-10-${String(i+1).padStart(2,'0')} 10:00:00`]);
- for(let i=0;i<4;i++){
- await sql(`INSERT INTO "MemoryPracticeRecord"("objectId","sourceKey",company,id,plid,yhid,xxjlid,"orderId",kywrq,kywsj,fxzt,wcsj,"createdAt","updatedAt") VALUES($1,$1,$2,$3,$4,$5,$6,$7,$8,$8,$9,'',now(),now())`,[taskId(i),co,gid(100+i),uid+1,uid,gid(1),gid(2),i===0?'2026-01-01':`2099-01-0${i+1}`,i===3?'1':'0']);
- await sql(`INSERT INTO "CommonModel"("objectId","sourceKey",company,"generalId","itemId","modelId","nodeId",title,status,"createdAt","updatedAt") VALUES($1,$1,$2,$3,$3,60,388,'隔离复习',99,now(),now())`,[prefix+'_taskCommon'+i,co,gid(100+i)]);
- }
- const p=await ops('ops/lessons/list',{studentId:uid});ok(p.total===25&&p.items.length===20&&p.items[0].lessonId===prefix+'_lesson24','真实接口分页、总课次与时间倒序');
- const second=await ops('ops/lessons/list',{studentId:uid,page:2});ok(second.items.length===5&&second.version===p.version,'分页快照一致且无漏行');
- const detail=await ops('ops/lessons/detail',{lessonId:prefix+'_lesson0'});ok(detail.coachComment==='完整老师反馈'&&detail.studentReview.comment==='完整学生评价'&&detail.tasks.length===4,'历史反馈与复习关联可完整查看');
- const params={studentId:uid,appointmentId:String(gid(2)),scope:'lesson',coachId:uid+2};
- const preview=await ops('ops/lessons/transfer-preview',params);ok(preview.count===3&&preview.dateFrom==='2026-01-01','预览包括逾期与未来,排除已完成');
- const before=await sql('SELECT to_jsonb(r) data FROM "ReviewReward" r WHERE company=$1 AND "taskId"=$2',[co,taskId(3)]);
- const request={...params,version:preview.version,reason:'隔离发布验收',idempotencyKey:randomUUID()};
- const responses=await Promise.all([ops('ops/lessons/transfer',request),ops('ops/lessons/transfer',request)]);ok(responses.every(r=>r.count===3&&r.commandId===responses[0].commandId),'真实并发重复转交只执行一次');
- const after=await sql('SELECT to_jsonb(r) data FROM "ReviewReward" r WHERE company=$1 AND "taskId"=$2',[co,taskId(3)]);assert.deepEqual(after,before);ok(true,'已完成奖励原值保持不变');
- const page=await mobile(users[2],'app_review_page',{uid:uid+2,studentId:uid,pageSize:100,status:'pending'});ok(page.items.length===3,'无授课关系的新陪练可查看接手任务');
- const words=await mobile(users[2],'e_record_detail',{id:gid(1),studentId:uid});ok(words.length===1&&words[0].Title==='hello','新陪练可读取对应来源词表');
- await assert.rejects(mobile(users[2],'app_learning_overview',{studentId:uid}));ok(true,'新陪练没有获得无关学习历史权限');
- const complete=i=>({content:JSON.stringify({GeneralID:gid(100+i)}),addon:JSON.stringify({fxzt:1})});
- await assert.rejects(mobile(users[1],'content_update',complete(0)));ok(true,'原陪练旧页面完成请求被拒绝');
- await Promise.all([mobile(users[2],'content_update',complete(0)),mobile(users[2],'content_update',complete(0))]);
- const reward=await sql('SELECT "coachId",amount FROM "ReviewReward" WHERE company=$1 AND "taskId"=$2',[co,taskId(0)]);ok(reward.length===1&&Number(reward[0].coachId)===uid+2&&Number(reward[0].amount)===1,'新陪练完成只计一次一元工资');
- await sql(`UPDATE "MemoryPracticeRecord" SET plid=$3,fxzt='1',wcsj='2026-01-01' WHERE company=$1 AND "objectId"=$2`,[co,taskId(1),uid+1]);
- const preserved=await sql('SELECT plid,fxzt FROM "MemoryPracticeRecord" WHERE company=$1 AND "objectId"=$2',[co,taskId(1)]);ok(Number(preserved[0].plid)===uid+2&&preserved[0].fxzt==='0','旧来源更新被保护并保留待复习状态');
- const reverse={...params,coachId:uid+1},racePreview=await ops('ops/lessons/transfer-preview',reverse);
- const race=await Promise.allSettled([ops('ops/lessons/transfer',{...reverse,version:racePreview.version,reason:'并发交接验证',idempotencyKey:randomUUID()}),mobile(users[2],'content_update',complete(1))]);
- const actual=await sql('SELECT m.plid,m.fxzt,r."coachId" FROM "MemoryPracticeRecord" m LEFT JOIN "ReviewReward" r ON r.company=m.company AND r."taskId"=m."objectId" WHERE m.company=$1 AND m."objectId"=$2',[co,taskId(1)]);
- ok(race.filter(r=>r.status==='fulfilled').length===1&&(actual[0].fxzt==='1'?Number(actual[0].coachId)===uid+2:Number(actual[0].plid)===uid+1),'真实多连接转交与完成互斥,工资归属一致');
- await assert.rejects(call('xiaoshu/ops/gateway-v3',{operation:'ops/lessons/transfer-preview',...params},users[1].sessionToken));ok(true,'普通老师不能调用管理转交接口');
- await parse('/users/'+users[2].objectId,'PUT',{isDisabled:true});await assert.rejects(ops('ops/lessons/transfer-preview',params));ok(true,'停用目标陪练被拒绝');
- const balances=await sql('SELECT "legacyUserData" FROM "_User" WHERE "objectId"=$1',[users[0].objectId]);ok(['Purse','SilverCoin','UserPoint','UserExp'].every(k=>balances[0].legacyUserData[k]===3),'转交不扣课时或余额');
- save('isolated-result.json',{passed:true,checks});return {passed:true,checks};
- }finally{
- // Fixed fixture prefix and IDs only; reward immutability bypass is transaction-local for teardown.
- await scoped(String.raw`await Psql.tx(async t=>{await t.none("SET LOCAL session_replication_role='replica'");const co=${JSON.stringify(co)},prefix=${JSON.stringify(prefix)},uid=${uid};for(const table of ['ReviewAssignmentConflict','ReviewTransferAudit'])await t.none('DELETE FROM "'+table+'" WHERE company=$1 AND "taskId" LIKE $2',[co,prefix+'_task%']);await t.none('DELETE FROM "ReviewTransferCommand" WHERE company=$1 AND result->>\'studentId\'=$2',[co,String(uid)]);await t.none('DELETE FROM "ReviewReward" WHERE company=$1 AND "taskId" LIKE $2',[co,prefix+'_task%']);for(const table of ['MemoryPracticeRecord','LessonRecord','DailyStudyRecord','CommonModel'])await t.none('DELETE FROM "'+table+'" WHERE company=$1 AND "objectId" LIKE $2',[co,prefix+'_%']);});return response.json({success:true,data:{cleaned:true}});`);
- for(const u of [...users].reverse())await parse('/users/'+u.objectId,'DELETE');save('isolated-fixtures-cleaned.json',{prefix,cleaned:true});console.log('ISOLATED_FIXTURES_CLEANED');
- }
- }
|