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'; import {randomUUID} from 'node:crypto'; const code=readFileSync(new URL('../cloud/lesson-unit-progress.js',import.meta.url),'utf8'); test('whole-unit acknowledgement is atomic, durable, authorized and idempotent without financial or lesson writes',async()=>{ const db=new PGlite();try{ await db.exec(` CREATE TABLE "CourseAppointment"("objectId" text,company text,id numeric,szyh numeric,pl numeric,dszt text); CREATE TABLE "CourseBinding"("objectId" text,company text,id numeric,yhid text,kcid text,"enrollmentStatus" text,syjd text,"updatedAt" timestamptz); CREATE TABLE "CommonModel"(company text,"generalId" numeric,"modelId" numeric,"itemId" numeric,"nodeId" numeric,status int); CREATE TABLE "Node"(company text,"nodeId" numeric,"parentId" numeric,"nodeName" text,zstatus int); CREATE TABLE "PracticeRecord"("objectId" text primary key,"createdAt" timestamptz,"updatedAt" timestamptz,company text,"sourceKey" text,kcid text,yhid numeric,scid numeric,xxcs numeric,jrscb text); INSERT INTO "CourseAppointment" VALUES('a','co',1,110,69,'10'); INSERT INTO "CourseBinding" VALUES('b','co',2,'110','100','active','{"202":"202@Other@0"}',now()); INSERT INTO "CommonModel" VALUES('co',900,54,1,0,99),('co',901,58,2,0,99); INSERT INTO "Node" VALUES('co',100,0,'Book',99),('co',201,100,'Unit',99),('co',202,100,'Other',99); INSERT INTO "CommonModel" SELECT 'co',n,52,n,201,99 FROM generate_series(1,205) n; INSERT INTO "PracticeRecord" VALUES('old',now(),now(),'co','old','100',110,1,7,'1'); `); await db.exec(readFileSync(new URL('../sql/mobile-teaching.sql',import.meta.url),'utf8')); const adapter=d=>({one:async(s,p)=>(await d.query(s,p)).rows[0],oneOrNone:async(s,p)=>(await d.query(s,p)).rows[0]||null,any:async(s,p)=>(await d.query(s,p)).rows,none:async(s,p)=>{await d.query(s,p);}}); const context={DEFAULT_COMPANY_ID:'co',number:x=>Number(x)||0,ownLegacyId:c=>c.uid,assertLessonTenant:()=>{},mobileCommandKey:i=>i.requestKey,randomObjectId:randomUUID,courseAssignedUnits:()=>[],fail:(status,message)=>{throw Object.assign(Error(message),{status});},Psql:{tx:fn=>db.transaction(t=>fn(adapter(t)))}}; vm.runInNewContext(code+';this.complete=completeLessonUnit;',context); const teacher={id:'teacher',uid:69,get:()=>3},input={orderId:'900',bindingId:'b',nodeId:'201',requestKey:'request-unit-0001'}; await assert.rejects(context.complete(input,{...teacher,uid:70}),/仅预约老师/); await assert.rejects(context.complete({...input,nodeId:'999'},teacher),/不属于/); const out=await context.complete(input,teacher);assert.equal(out.learned,205);assert.equal(out.percent,100); assert.equal((await db.query('SELECT count(*) n FROM "PracticeRecord" WHERE xxcs>0')).rows[0].n,205); assert.equal((await db.query('SELECT xxcs,jrscb FROM "PracticeRecord" WHERE scid=1')).rows[0].xxcs,'7'); assert.equal(JSON.parse(out.syjd)['202'],'202@Other@0'); await context.complete(input,teacher);await context.complete({...input,requestKey:'request-unit-0002'},teacher); assert.equal((await db.query('SELECT count(*) n FROM "PracticeRecord"')).rows[0].n,205); assert.equal((await db.query('SELECT xxcs FROM "PracticeRecord" WHERE scid=2')).rows[0].xxcs,'1'); await db.exec('UPDATE "CourseAppointment" SET dszt=\'30\'');await assert.rejects(context.complete(input,teacher),/已结束/); // A downstream failure must roll back every chunk of word progress. await db.exec('UPDATE "CourseAppointment" SET dszt=\'10\'; DELETE FROM "PracticeRecord" WHERE scid>1; ALTER TABLE "CourseBinding" ADD CONSTRAINT no_completion CHECK (syjd NOT LIKE \'%Blocked%\'); UPDATE "Node" SET "nodeName"=\'Blocked\' WHERE "nodeId"=201;'); await assert.rejects(context.complete({...input,requestKey:'rollback-unit-0003'},teacher),/no_completion/); assert.equal((await db.query('SELECT count(*) n FROM "PracticeRecord"')).rows[0].n,1); }finally{await db.close();} });