local-runtime.mjs 2.8 KB

12345678910111213141516171819202122232425262728293031
  1. // Isolated PostgreSQL fixture. No production credentials or network are used.
  2. import fs from 'node:fs';
  3. import vm from 'node:vm';
  4. import {execFileSync} from 'node:child_process';
  5. import {PGlite} from '@electric-sql/pglite';
  6. export async function localRuntime({scale=0,legacyDelayMs=0}={}){
  7. const db=new PGlite();
  8. const today=new Intl.DateTimeFormat('en-CA',{timeZone:'Asia/Shanghai'}).format(new Date());
  9. const schema=fs.readFileSync('scripts/tests/fixtures/teacher-review-schema.sql','utf8').replaceAll('2026-10-14',today);
  10. await db.exec(schema);
  11. if(scale)await db.exec(`INSERT INTO "MemoryPracticeRecord" SELECT 'scale'||i,'co',i,30,20,100,1,'0','2026-10-20','2026-10-20' FROM generate_series(2000,${1999+Number(scale)}) i;
  12. INSERT INTO "CommonModel" (company,"modelId","generalId","itemId") SELECT company,60,id+100000,id FROM "MemoryPracticeRecord" WHERE id>=2000;`);
  13. await db.exec(`CREATE INDEX ON "MemoryPracticeRecord"(company,yhid,xxjlid,kywsj,id);CREATE INDEX ON "CommonModel"(company,"modelId","itemId");CREATE INDEX ON "CommonModel"(company,"modelId","generalId");CREATE INDEX ON "DailyStudyRecord"(company,id);CREATE INDEX ON "ReviewRewardIncome"(company,"taskId");ANALYZE;`);
  14. const sources={before:execFileSync('git',['show','81f8b44:scripts/cloud/mobile-teaching.js'],{encoding:'utf8'}),after:fs.readFileSync('scripts/cloud/mobile-teaching.js','utf8')};
  15. const results=[];
  16. async function review(variant,input,coach=false){
  17. const metrics={variant,coach,legacyCalls:0,queries:[],versionCalls:0},start=performance.now();
  18. const query=async(sql,args)=>{if(!sql.startsWith('SELECT '))throw Error('Unexpected write in local benchmark');const at=performance.now();const result=await db.query(sql,args);metrics.queries.push({ms:performance.now()-at,sql,args});return result.rows;};
  19. const context=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number.isFinite(Number(v))?Number(v):f,
  20. authorizeRequestedUser:async()=>coach?30:20,appReviewTarget:async()=>20,appOverviewToday:()=>today,legacyAliases:r=>r,
  21. syncLegacyReviews:async()=>{metrics.legacyCalls++;if(legacyDelayMs)await new Promise(r=>setTimeout(r,legacyDelayMs));return {state:'unavailable'};},
  22. appLiveSyncStatus:async()=>({state:'delayed',lastSuccessAt:'',lagSeconds:null,pending:0}),
  23. appLiveVersionSnapshot:async()=>{metrics.versionCalls++;return {version:'v1',scopeVersions:{review:'v1'},syncStatus:{state:'delayed'},refreshedAt:new Date().toISOString()};},
  24. Psql:{query,one:async(s,a)=>(await query(s,a))[0]}});
  25. vm.runInContext(sources[variant],context);
  26. const value=JSON.parse(JSON.stringify(await context.appReviewPage(input,{get:()=>coach?3:1})));
  27. metrics.ms=performance.now()-start;metrics.bytes=Buffer.byteLength(JSON.stringify(value));results.push(metrics);
  28. return {value,metrics};
  29. }
  30. return {db,today,review,results,close:()=>db.close()};
  31. }