| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 |
- 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 source=readFileSync(new URL('../cloud/mobile-teaching.js',import.meta.url),'utf8');
- const plain=v=>JSON.parse(JSON.stringify(v));
- 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(`INSERT INTO "ReviewRewardIncome"(company,"taskId",amount,status) VALUES('co','r2',1,'earned');
- INSERT INTO "_User"(company,"legacyUserId","legacyUserData") VALUES('co',20,'{"UserPoint":12.5,"Purse":8,"SilverCoin":7}'::jsonb);`);
- const calls=[];
- const query=async(sql,args)=>{calls.push(sql);assert.match(sql,/^SELECT /,'homepage must not write');return (await db.query(sql,args)).rows;};
- const ctx=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number.isFinite(Number(v))?Number(v):f,
- authorizeRequestedUser:async(_c,uid)=>Number(uid),appReviewTarget:async()=>20,appOverviewToday:()=> '2026-10-14',legacyAliases:r=>r,
- appLiveVersionSnapshot:async()=>({version:'v1'}),appLiveSyncStatus:async()=>({state:'delayed',lastSuccessAt:'2026-10-13T00:00:00Z',lagSeconds:100,pending:2}),
- syncLegacyReviews:async()=>{throw Error('legacy sync must not run on a database-only home read');},
- Psql:{query,one:async(s,a)=>(await query(s,a))[0]}});
- vm.runInContext(source,ctx);
- const snapshot=async()=>{const result={};for(const t of ['MemoryPracticeRecord','CommonModel','ReviewRewardIncome','_User','LessonRecord','CourseAppointment','DailyStudyRecord'])result[t]=(await db.query(`SELECT * FROM "${t}"`)).rows;return result;};
- await run({db,ctx,calls,snapshot});
- }finally{await db.close();}
- }
- test('student home reads synced summaries without old-system requests, fee joins or any balance/receipt mutation',async()=>fixture(async({ctx,calls,snapshot})=>{
- const before=await snapshot();
- const result=plain(await ctx.appReviewPage({uid:20,view:'home',source:'database',status:'pending',refresh:true},{get:()=>1}));
- assert.deepEqual(result.summary,{total:50,completed:1,due:2,today:2,todayTotal:2,todayCompleted:1,todayPending:1,overduePending:1,income:0,incomeScope:'all'});
- assert.equal(result.view,'home');assert.deepEqual(result.items,[]);
- assert.deepEqual(result.dates.sort((a,b)=>a.date.localeCompare(b.date)),[{date:'2026-10-13',total:1,completed:0},{date:'2026-10-14',total:2,completed:1},{date:'2026-10-20',total:47,completed:0}]);
- assert.equal(result.legacyReviewSync.state,'snapshot');assert.equal(result.syncStatus.state,'delayed');
- assert.equal(calls.length,1,'one date aggregation replaces list/count/summary/version scans');
- assert.ok(!calls.some(s=>s.includes('ReviewRewardIncome')));
- assert.deepEqual(await snapshot(),before);
- }));
- test('home snapshot retains empty/undated/completed and other-tenant ownership semantics',async()=>fixture(async({db,ctx})=>{
- await db.exec(`UPDATE "MemoryPracticeRecord" SET kywrq='',kywsj=NULL WHERE id=3;
- INSERT INTO "MemoryPracticeRecord" VALUES('foreign','other',500,30,20,100,1,'0','2026-10-14','2026-10-14');`);
- const result=plain(await ctx.appReviewPage({uid:20,view:'home',source:'database'},{get:()=>1}));
- assert.equal(result.summary.total,50);assert.equal(result.summary.due,1);assert.equal(result.summary.overduePending,0);
- const empty=plain(await ctx.appReviewPage({uid:99,view:'home',source:'database'},{get:()=>1}));
- assert.equal(empty.summary.total,0);assert.equal(empty.summary.income,0);assert.deepEqual(empty.dates,[]);
- }));
- test('today teacher list reuses exact summary count while preserving rows and fee result',async()=>fixture(async({ctx,calls})=>{
- const result=plain(await ctx.appReviewPage({uid:30,scope:'today',includeMetadata:false,includeIncome:false,pageSize:1},{get:()=>3}));
- assert.equal(result.page.itemCount,2);assert.equal(result.page.pageCount,2);assert.equal(result.items.length,1);assert.equal(result.items[0].fxzt,'0');
- assert.equal(result.summary.total,50);assert.equal(result.summary.completed,1);assert.equal(result.summary.income,0);
- assert.equal(calls.length,2,'reuse today from summary rather than issuing another count');
- calls.length=0;
- const filtered=plain(await ctx.appReviewPage({uid:30,scope:'today',status:'completed',includeMetadata:false,includeIncome:false},{get:()=>3}));
- assert.equal(filtered.page.itemCount,1);assert.equal(filtered.items[0].money,1);assert.equal(filtered.items[0].rewardStatus,'earned');
- }));
- test('database home mode cannot bypass account authorization',async()=>fixture(async({ctx,calls})=>{
- ctx.authorizeRequestedUser=async()=>{throw Error('denied');};
- await assert.rejects(ctx.appReviewPage({uid:99,view:'home',source:'database'},{get:()=>1}),/denied/);
- assert.equal(calls.length,0);
- }));
- test('teacher home may omit unused version scans without changing tasks, summary or reward values',async()=>fixture(async({ctx})=>{
- let versionReads=0;ctx.appLiveVersionSnapshot=async()=>{versionReads++;return {version:'v1'};};
- const input={uid:30,scope:'today',includeMetadata:false,includeIncome:false};
- const before=plain(await ctx.appReviewPage(input,{get:()=>3}));versionReads=0;
- const after=plain(await ctx.appReviewPage({...input,includeVersions:false},{get:()=>3}));
- assert.equal(versionReads,0);delete before.version;delete before.refreshedAt;delete after.refreshedAt;
- assert.deepEqual(after,before);
- }));
|