import {test} from 'node:test';import assert from 'node:assert/strict';import{readFileSync}from'node:fs';import vm from'node:vm';import{PGlite}from'@electric-sql/pglite'; const sql=readFileSync(new URL('../sql/mobile-teaching.sql',import.meta.url),'utf8'); const gatewaySource=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8'); test('教学事务:重复及并发请求只增一次,关联记录同事务,失败整体回滚',async()=>{ const db=new PGlite();try{ await db.exec(`CREATE TABLE "PracticeRecord"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,company text,"yhid" int,"scid" int,"xxcs" int);CREATE TABLE "DailyStudyRecord"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,company text,id bigint,"sourceKey" text);CREATE TABLE "MemoryPracticeRecord"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,company text,id bigint,"sourceKey" text,"yhid" int,"fxzt" text);CREATE TABLE "CommonModel"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,company text,"itemId" bigint,"generalId" bigint,"sourceKey" text);`);await db.exec(sql); const run=(key,ops,fingerprint='same')=>db.query('select xs_mobile_teaching_write($1,$2,$3,$4,$5::jsonb) result',['co','learner',key,fingerprint,JSON.stringify(ops)]); const op={table:'PracticeRecord',match:{company:'co',yhid:1,scid:2},data:{objectId:'word',createdAt:new Date(),company:'co',yhid:1,scid:2,xxcs:1},increment:['xxcs']}; await Promise.all(Array.from({length:8},()=>run('request-0001',[op])));assert.equal((await db.query('SELECT xxcs FROM "PracticeRecord"')).rows[0].xxcs,1); await run('request-0002',[op]);assert.equal((await db.query('SELECT xxcs FROM "PracticeRecord"')).rows[0].xxcs,2); await assert.rejects(run('request-0001',[op],'changed'),/different content/); const review={table:'MemoryPracticeRecord',match:{company:'co',id:88},data:{objectId:'review',createdAt:new Date(),company:'co',id:88,sourceKey:'legacy:model:60:general:88',yhid:1,fxzt:'0'}};await run('request-review-0001',[review]);assert.equal((await db.query('SELECT COUNT(*) n FROM "MemoryPracticeRecord"')).rows[0].n,1); await assert.rejects(run('request-0003',[op,{table:'Wrong',match:{id:1},data:{}}]),/not allowed/);assert.equal((await db.query('SELECT xxcs FROM "PracticeRecord"')).rows[0].xxcs,2); const pair=[{table:'DailyStudyRecord',match:{company:'co',sourceKey:'study'},data:{objectId:'d',createdAt:new Date(),company:'co',id:77,sourceKey:'study'}},{table:'CommonModel',match:{company:'co',sourceKey:'study'},data:{objectId:'c',createdAt:new Date(),company:'co',itemId:999,generalId:88,sourceKey:'study'},reference:{itemId:'id'}}]; await run('request-0004',pair);await run('request-0005',pair);assert.equal((await db.query('SELECT "itemId" FROM "CommonModel"')).rows[0].itemId,77);assert.equal((await db.query('SELECT COUNT(*) n FROM "DailyStudyRecord"')).rows[0].n,1); }finally{await db.close();} }); test('完课确认:学生与老师依次确认,重复确认幂等,越权和取消预约拒绝',async()=>{ const db=new PGlite();try{ await db.exec(`CREATE TABLE "CourseAppointment"("objectId" text,company text,id bigint,szyh bigint,pl bigint,dszt text,"updatedAt" timestamptz);CREATE TABLE "CommonModel"(company text,"modelId" int,"itemId" bigint,"generalId" bigint,status int);INSERT INTO "CourseAppointment" VALUES('a','co',1,10,20,'11',now());INSERT INTO "CommonModel" VALUES('co',54,1,100,0);`);await db.exec(sql); const confirm=(status,actor)=>db.query('select xs_mobile_confirm_appointment($1,$2,$3,$4,false) result',['co','100',status,actor]); await assert.rejects(confirm(20,99),/无权/);await assert.rejects(confirm(30,20),/状态已变更/); assert.equal((await confirm(20,10)).rows[0].result.status,20); assert.equal((await confirm(20,10)).rows[0].result.unchanged,true); assert.equal((await confirm(30,20)).rows[0].result.status,30); assert.equal((await confirm(20,10)).rows[0].result.status,30); await db.exec('UPDATE "CommonModel" SET status=-2');await assert.rejects(confirm(30,20),/已取消/); }finally{await db.close();} }); test('学习记录:统计已生成文章,未作答文章也计入,已完成记录保持关联',async()=>{ const db=new PGlite();try{ await db.exec('CREATE TABLE "SurveyItem"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,"user" text,"type" text,"title" text,"createOptions" jsonb,"isDeleted" boolean);CREATE TABLE "SurveyLog"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,"user" text,"surveyItem" text);'); for(const [id,studyId,owner,deleted] of [ ['article201a','201','_User$learner',false],['article201b','201','learner',false], ['article202a','202','_User$learner',false],['article202b','202','_User$learner',false],['article202c','202','_User$learner',false], ['otherOwner','202','_User$other',false],['deletedArticle','202','_User$learner',true], ]) await db.query('INSERT INTO "SurveyItem" VALUES($1,now(),now(),$2,$3,$4,$5::jsonb,$6)',[id,owner,'reading',id,JSON.stringify({params:{sourceStudyId:studyId}}),deleted]); await db.query('INSERT INTO "SurveyLog" VALUES($1,now(),now(),$2,$3)', ['log201','_User$learner','SurveyItem$article201a']); const source=gatewaySource.match(/async function appOverviewReadingMap[\s\S]*?\n}\nasync function appLearningTarget/)?.[0].replace(/\nasync function appLearningTarget[\s\S]*$/,'');assert.ok(source); const context={Psql:{query:async(statement,args)=>(await db.query(statement,args)).rows},legacyUserObject:async()=>({id:'learner'})};vm.runInNewContext(source+';this.readingMap=appOverviewReadingMap',context); const map=await context.readingMap([{GeneralID:201},{GeneralID:202},{GeneralID:203}],100); assert.equal(map.get('201').readingArticleCount,2);assert.equal(map.get('201').readingArticleId,'article201a'); assert.equal(map.get('202').readingArticleCount,3);assert.equal(map.get('202').readingArticleId,undefined); assert.equal(map.has('203'),false); }finally{await db.close();} });