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 mobile=readFileSync(new URL('../cloud/mobile-teaching.js',import.meta.url),'utf8'); const gateway=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8').match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1].replace("const DEFAULT_COMPANY_ID = '7pIbDBJmKx';","const DEFAULT_COMPANY_ID = 'co';"); async function fixture(run){ const db=new PGlite();try{ await db.exec(readFileSync(new URL('./fixtures/teacher-review-schema.sql',import.meta.url),'utf8')); await db.exec(`ALTER TABLE "DailyStudyRecord" ADD COLUMN pl text,ADD COLUMN "canonicalStudyId" text; ALTER TABLE "CourseAppointment" ADD COLUMN pl text,ADD COLUMN kssj text,ADD COLUMN jssj text; ALTER TABLE "LessonRecord" ADD COLUMN jsmz text; INSERT INTO "CommonModel"(company,"modelId","generalId","itemId",status,"sourceKey") VALUES('co',56,100,101,99,'legacy:model:56:general:100'),('co',54,200,201,99,'legacy:model:54:general:200'); INSERT INTO "DailyStudyRecord"(company,id,learned,xxqs,dsid,"userId",pl) VALUES('co',101,0,'','200',20,'30'); INSERT INTO "CourseAppointment"(company,id,szyh,dszt,"courseCategoryKey",kcid,"sourceKey",pl) VALUES('co',201,'20','30',NULL,'398','legacy:model:54:general:200','30'); INSERT INTO "Node" VALUES('co',398,'阅读写作');`); const query=async(s,a)=>(await db.query(s,a)).rows; const ctx=vm.createContext({Date,Promise,Parse:{},Psql:{query,one:async(s,a)=>(await query(s,a))[0],oneOrNone:async(s,a)=>(await query(s,a))[0]||null}}); vm.runInContext(gateway+'\n'+mobile,ctx); ctx.authorizeRequestedUser=async()=>30;ctx.appOverviewToday=()=> '2026-10-14';ctx.appLiveVersionSnapshot=async()=>({});ctx.legacyStudyRecordWords=async()=>[];ctx.appLearningTarget=async()=>{throw Error('forbidden');}; const coach={get:k=>({legacyUserId:30,legacyGroupId:3}[k])}; await run({db,ctx,coach}); }finally{await db.close();} } test('旧阅读写作398无学段分类时正常列出任务、统计和学员,并能直接进入笔记复习',()=>fixture(async({db,ctx,coach})=>{ const page=await ctx.appReviewPage({scope:'today',pageSize:1,includeIncome:false},coach); assert.equal(page.page.itemCount,2);assert.equal(page.items.length,1);assert.equal(page.summary.todayTotal,2);assert.equal(page.summary.todayPending,1);assert.equal(page.students[0].id,'20'); const next=await ctx.appReviewPage({scope:'today',page:2,pageSize:1,includeIncome:false},coach);assert.equal(next.items.length,1); const detail=await ctx.recordDetailData({id:100},coach);assert.equal(detail.words.length,0);assert.equal(detail.addon[0].reviewContentMode,'notes');assert.equal(detail.record.__notesReviewSource,undefined); assert.equal((await db.query('SELECT "courseCategoryKey" FROM "CourseAppointment"')).rows[0].courseCategoryKey,null); await assert.rejects(ctx.recordDetailData({id:100},{get:k=>({legacyUserId:99,legacyGroupId:3}[k])}),/forbidden/); })); test('普通空词表、错误学生、未完课、回收和无可信来源的398不得冒充旧写作复习',()=>fixture(async({db,ctx,coach})=>{ const reset=`UPDATE "CourseAppointment" SET szyh='20',dszt='30',"courseCategoryKey"=NULL,kcid='398',"sourceKey"='legacy:model:54:general:200';UPDATE "CommonModel" SET status=99,"sourceKey"='legacy:model:54:general:200' WHERE "modelId"=54;`; for(const patch of [ `UPDATE "CourseAppointment" SET "courseCategoryKey"='word'`, `UPDATE "CourseAppointment" SET kcid='399'`, `UPDATE "CourseAppointment" SET szyh='21'`, `UPDATE "CourseAppointment" SET dszt='10'`, `UPDATE "CourseAppointment" SET dszt='20'`, `UPDATE "CourseAppointment" SET "sourceKey"='cloud:appointment:new'`, `UPDATE "CommonModel" SET "sourceKey"='cloud:appointment:new' WHERE "modelId"=54`, `UPDATE "CommonModel" SET status=-2 WHERE "modelId"=54`, ]){ await db.exec(reset+patch);const page=await ctx.appReviewPage({scope:'today',includeIncome:false},coach);assert.equal(page.items.length,0,patch);assert.equal(page.summary.todayTotal,0,patch);assert.equal(page.students.length,0,patch); const detail=await ctx.recordDetailData({id:100},coach);assert.equal(detail.addon[0].reviewContentMode,undefined,patch); } })); test('旧预约状态滞后但已有同一学生同一课次的真实旧完课凭据时恢复笔记复习',()=>fixture(async({db,ctx,coach})=>{ await db.exec(`UPDATE "CourseAppointment" SET dszt='0',"courseCategoryKey"='high_writing'; INSERT INTO "CommonModel"(company,"modelId","generalId","itemId",status,"sourceKey") VALUES('co',59,300,301,99,'legacy:model:59:general:300'); INSERT INTO "LessonRecord"(company,id,yyds,xymz,kcid,"sourceKey",jsmz) VALUES('co',301,'200','20','398','legacy:model:59:general:300','30');`); const page=()=>ctx.appReviewPage({scope:'today',includeIncome:false},coach); assert.equal((await page()).page.itemCount,2); assert.equal((await ctx.recordDetailData({id:100},coach)).addon[0].reviewContentMode,'notes'); assert.equal((await db.query('SELECT dszt FROM "CourseAppointment"')).rows[0].dszt,'0'); for(const patch of [ `UPDATE "LessonRecord" SET xymz='21'`, `UPDATE "LessonRecord" SET yyds='999'`, `UPDATE "LessonRecord" SET kcid='203'`, `UPDATE "LessonRecord" SET "sourceKey"='cloud:complete:new'`, `UPDATE "CommonModel" SET status=-2 WHERE "modelId"=59`, ]){ await db.exec(`UPDATE "LessonRecord" SET xymz='20',yyds='200',kcid='398',"sourceKey"='legacy:model:59:general:300';UPDATE "CommonModel" SET status=99 WHERE "modelId"=59;`+patch); assert.equal((await page()).page.itemCount,0,patch); assert.equal((await ctx.recordDetailData({id:100},coach)).addon[0].reviewContentMode,undefined,patch); } })); test('旧系统已结束、待课后确认的阅读写作任务可显示并打开笔记',()=>fixture(async({db,ctx,coach})=>{ await db.exec(`UPDATE "CourseAppointment" SET dszt='11',kssj='2026-10-13 10:43',jssj='2026-10-13 11:40'; INSERT INTO "CommonModel"(company,"modelId","generalId","itemId",status,"sourceKey") VALUES('co',59,300,301,99,'legacy:model:59:general:300'); INSERT INTO "LessonRecord"(company,id,yyds,xymz,kcid,"sourceKey",jsmz) VALUES('co',301,'200','20','398','legacy:model:59:general:300','30');`); const page=()=>ctx.appReviewPage({scope:'today',includeIncome:false},coach); const detail=()=>ctx.recordDetailData({id:100},coach); for(const status of ['11','20']){ await db.query('UPDATE "CourseAppointment" SET dszt=$1',[status]); assert.equal((await page()).page.itemCount,2); assert.equal((await detail()).addon[0].reviewContentMode,'notes'); } await db.exec(`UPDATE "LessonRecord" SET jsmz='31'`); assert.equal((await page()).page.itemCount,0); await db.exec(`UPDATE "LessonRecord" SET jsmz='30';UPDATE "CourseAppointment" SET jssj=''`); assert.equal((await page()).page.itemCount,0); }));