| 123456789101112131415161718192021222324252627282930313233343536373839404142 |
- import {test} from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import vm from 'node:vm';
- import {PGlite} from '@electric-sql/pglite';
- export async function classroomFixture(source) {
- const db=new PGlite();
- 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);
- CREATE TABLE "VocabularyWord" ("objectId" text,company text,id double precision,sy text,yb text,yp text);
- 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;
- INSERT INTO "VocabularyWord" SELECT 'v'||i,'7pIbDBJmKx',i,'meaning-'||i,'','word-'||i||'.mp3' FROM generate_series(1,1000) i;
- INSERT INTO "CommonModel" VALUES ('foreign','other',52,40,99999,1,'foreign',99,'2026-11-07');
- INSERT INTO "VocabularyWord" VALUES ('foreign-v','other',1,'wrong','','wrong.mp3');
- CREATE INDEX cm_model_node ON "CommonModel" (company,"modelId","nodeId");
- CREATE INDEX vocab_id ON "VocabularyWord" (company,id); ANALYZE;`);
- const queries=[];
- const gateway=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
- 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;}}});
- vm.runInContext(gateway,context);
- return{db,queries,read:input=>context.contentPage(input,true)};
- }
- if(process.argv[1]===new URL(import.meta.url).pathname || process.env.NODE_TEST_CONTEXT) {
- const source=fs.readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8');
- test('词库课堂查询只扫描选中单元,保留分页、释义和租户隔离',async t=>{
- const {db,queries,read}=await classroomFixture(source);t.after(()=>db.close());
- const page=await read({modelId:52,nid:40,psize:10,cpage:2,orders:'GeneralID=ASC',__publishedOnly:true});
- assert.equal(page.page.itemCount,100);assert.equal(page.rows.length,10);
- assert.equal(page.rows[0].GeneralID,11);assert.equal(page.rows[0].sy,'meaning-11');assert.equal(page.rows.at(-1).GeneralID,20);
- let discarded=0;
- const walk=p=>{discarded+=Number(p['Rows Removed by Filter']||0)*Number(p['Actual Loops']||1);for(const child of p.Plans||[])walk(child);};
- 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);}
- assert.ok(discarded<1000,`单元读取丢弃了 ${discarded} 行,应使用已有数字索引而不是全表转换过滤`);
- });
- test('随机干扰词排除本轮词表且保留正确释义',async t=>{
- const {db,read}=await classroomFixture(source);t.after(()=>db.close());
- const result=await read({modelId:52,nid:'40',psize:60,orders:'NEWID()',excludeWordIds:'1,2,3',__publishedOnly:true});
- assert.equal(result.page.itemCount,97);assert.equal(result.rows.length,60);
- assert.ok(result.rows.every(r=>r.GeneralID>3&&r.GeneralID<=100&&r.sy===`meaning-${r.GeneralID}`));
- });
- }
|