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}`)); }); }