import {test} from 'node:test';import assert from 'node:assert/strict';import {PGlite} from '@electric-sql/pglite';import {readFileSync,existsSync} from 'node:fs';import vm from 'node:vm'; const file=new URL('../cloud/lesson-export.js',import.meta.url); test('export jobs are idempotent, owner scoped, globally leased and cancellation fences workers',async()=>{ assert.ok(existsSync(file),'durable export implementation exists');const db=new PGlite(); try{ await db.exec(readFileSync(new URL('../sql/lesson-export.sql',import.meta.url),'utf8')); const statements=[];const adapter=tx=>({query:async(s,p=[])=>{statements.push(s);return(await tx.query(s,p)).rows;},one:async(s,p=[])=>(await tx.query(s,p)).rows[0],none:async(s,p=[])=>{await tx.query(s,p);}}); const Psql={...adapter(db),tx:work=>db.transaction(tx=>work(adapter(tx)))}; const context={current:{id:'owner'},company:'co',roles:['ops-manager'],isSuperAdmin:true}; const ctx=vm.createContext({Psql,text:v=>String(v??''),number:v=>Number(v)||0,companyIdOf:c=>c.company,roleOf:c=>c.roles[0],requireRole:()=>{},requireSuperAdmin:c=>{if(!c.isSuperAdmin)throw Error('forbidden');},fail:(_,m)=>{throw Error(m);},Date,JSON});vm.runInContext(readFileSync(file,'utf8'),ctx); const input={idempotencyKey:'export-key-12345678',search:'林'}; const first=await ctx.lessonExportCreate(context,input),again=await ctx.lessonExportCreate(context,input);assert.equal(first.jobId,again.jobId); await assert.rejects(ctx.lessonExportStatus({...context,current:{id:'other'}},{jobId:first.jobId}),/未找到/); const claim=await ctx.lessonExportClaim(context);assert.equal(claim.jobId,first.jobId);assert.ok(claim.lease);assert.equal((await ctx.lessonExportClaim(context)).jobId,undefined); await ctx.lessonExportCancel(context,{jobId:first.jobId});await assert.rejects(ctx.lessonExportChunk(context,{jobId:first.jobId,lease:claim.lease,offset:0}),/租约/); assert.equal((await ctx.lessonExportStatus(context,{jobId:first.jobId})).status,'cancelled');assert.ok(!/SELECT \*/.test(statements.at(-1)),'polling must not load file_base64'); const resumed=await ctx.lessonExportCreate(context,{idempotencyKey:'resume-key-12345678'}),lease1=await ctx.lessonExportClaim(context); await db.query("UPDATE xs_history_export_job SET snapshot_ready=true,total=1 WHERE id=$1",[resumed.jobId]);await db.query("INSERT INTO xs_history_export_row(job_id,position,value) VALUES($1,1,'{\"lessonId\":\"fixed\"}')",[resumed.jobId]); await ctx.lessonExportRelease(context,{jobId:resumed.jobId,lease:lease1.lease});await assert.rejects(ctx.lessonExportChunk(context,{jobId:resumed.jobId,lease:lease1.lease}),/租约/); const lease2=await ctx.lessonExportClaim(context);assert.notEqual(lease2.lease,lease1.lease);assert.equal(lease2.snapshotReady,true); assert.equal((await ctx.lessonExportChunk(context,{jobId:resumed.jobId,lease:lease2.lease})).items[0].lessonId,'fixed'); await assert.rejects(ctx.lessonExportFinish(context,{jobId:resumed.jobId,lease:lease1.lease,rowCount:1,base64:'eA=='}),/租约/); await db.query("UPDATE xs_history_export_job SET status='ready',file_base64='eA==' WHERE id=$1",[resumed.jobId]);assert.equal((await ctx.lessonExportDownload(context,{jobId:resumed.jobId})).base64,'eA==');assert.ok(statements.at(-1).includes('file_base64'),'download explicitly fetches the file'); }finally{await db.close();} }); test('worker releases transient failures and retains fixed snapshots for retry',async()=>{ const {processLessonExport}=await import('../lesson-export-worker.mjs');const calls=[]; await assert.rejects(processLessonExport(async(action,params)=>{calls.push({action,params});if(action==='claim')return{jobId:'job',lease:'owner-lease',snapshotReady:true};if(action==='chunk')throw Object.assign(Error('export_gateway_504'),{transient:true});return{};}),e=>e.message==='export_job_failed'&&e.transient===true&&e.reason==='export_gateway_504'); assert.deepEqual(calls.map(c=>c.action),['claim','chunk','release']);assert.deepEqual(calls.at(-1).params,{jobId:'job',lease:'owner-lease'}); }); test('worker recognizes PM2 and direct CLI entrypoints but stays idle when imported',async()=>{ const module=await import('../lesson-export-worker.mjs');assert.equal(typeof module.isLessonExportEntrypoint,'function'); const url='file:///opt/exports/worker.mjs';assert.equal(module.isLessonExportEntrypoint(url,['node','/pm2/ProcessContainerFork.js'],{pm_exec_path:'/opt/exports/worker.mjs'}),true); assert.equal(module.isLessonExportEntrypoint(url,['node','/opt/exports/worker.mjs'],{}),true); assert.equal(module.isLessonExportEntrypoint(url,['node','/opt/exports/worker.mjs'],{pm_exec_path:'/opt/server/main.js'}),true); assert.equal(module.isLessonExportEntrypoint(url,['node','/opt/tests/test.mjs'],{}),false); });