word-classroom-performance.test.mjs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142
  1. import {test} from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import fs from 'node:fs';
  4. import vm from 'node:vm';
  5. import {PGlite} from '@electric-sql/pglite';
  6. export async function classroomFixture(source) {
  7. const db=new PGlite();
  8. await db.exec(`CREATE TABLE "CommonModel" ("objectId" text,company text,"modelId" double precision,"nodeId" double precision,"generalId" double precision,"itemId" double precision,title text,status double precision,"updatedAt" timestamptz);
  9. CREATE TABLE "VocabularyWord" ("objectId" text,company text,id double precision,sy text,yb text,yp text);
  10. INSERT INTO "CommonModel" SELECT 'c'||i,'7pIbDBJmKx',CASE WHEN i<=1000 THEN 52 ELSE 56 END,CASE WHEN i<=100 THEN 40 ELSE 41 END,i,i,'word-'||i,99,'2026-11-07' FROM generate_series(1,12000) i;
  11. INSERT INTO "VocabularyWord" SELECT 'v'||i,'7pIbDBJmKx',i,'meaning-'||i,'','word-'||i||'.mp3' FROM generate_series(1,1000) i;
  12. INSERT INTO "CommonModel" VALUES ('foreign','other',52,40,99999,1,'foreign',99,'2026-11-07');
  13. INSERT INTO "VocabularyWord" VALUES ('foreign-v','other',1,'wrong','','wrong.mp3');
  14. CREATE INDEX cm_model_node ON "CommonModel" (company,"modelId","nodeId");
  15. CREATE INDEX vocab_id ON "VocabularyWord" (company,id); ANALYZE;`);
  16. const queries=[];
  17. const gateway=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
  18. const context=vm.createContext({Psql:{one:async(sql,args)=>{queries.push({sql,args});return(await db.query(sql,args)).rows[0];},query:async(sql,args)=>{queries.push({sql,args});return(await db.query(sql,args)).rows;}}});
  19. vm.runInContext(gateway,context);
  20. return{db,queries,read:input=>context.contentPage(input,true)};
  21. }
  22. if(process.argv[1]===new URL(import.meta.url).pathname || process.env.NODE_TEST_CONTEXT) {
  23. const source=fs.readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8');
  24. test('词库课堂查询只扫描选中单元,保留分页、释义和租户隔离',async t=>{
  25. const {db,queries,read}=await classroomFixture(source);t.after(()=>db.close());
  26. const page=await read({modelId:52,nid:40,psize:10,cpage:2,orders:'GeneralID=ASC',__publishedOnly:true});
  27. assert.equal(page.page.itemCount,100);assert.equal(page.rows.length,10);
  28. assert.equal(page.rows[0].GeneralID,11);assert.equal(page.rows[0].sy,'meaning-11');assert.equal(page.rows.at(-1).GeneralID,20);
  29. let discarded=0;
  30. const walk=p=>{discarded+=Number(p['Rows Removed by Filter']||0)*Number(p['Actual Loops']||1);for(const child of p.Plans||[])walk(child);};
  31. for(const q of queries){const plan=(await db.query('EXPLAIN (ANALYZE,FORMAT JSON) '+q.sql,q.args)).rows[0]['QUERY PLAN'][0].Plan;walk(plan);}
  32. assert.ok(discarded<1000,`单元读取丢弃了 ${discarded} 行,应使用已有数字索引而不是全表转换过滤`);
  33. });
  34. test('随机干扰词排除本轮词表且保留正确释义',async t=>{
  35. const {db,read}=await classroomFixture(source);t.after(()=>db.close());
  36. const result=await read({modelId:52,nid:'40',psize:60,orders:'NEWID()',excludeWordIds:'1,2,3',__publishedOnly:true});
  37. assert.equal(result.page.itemCount,97);assert.equal(result.rows.length,60);
  38. assert.ok(result.rows.every(r=>r.GeneralID>3&&r.GeneralID<=100&&r.sy===`meaning-${r.GeneralID}`));
  39. });
  40. }