13 lines
8.3 KiB
JavaScript
13 lines
8.3 KiB
JavaScript
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),path=require('node:path'),{webcrypto}=require('node:crypto'),Core=require('../core');
|
|
const source=fs.readFileSync(path.join(__dirname,'..','service-worker.js'),'utf8');
|
|
function harness({initial={},content,fetcher,initialURL='https://mobile.yangkeduo.com/orders.html'}={}){const session={...initial},listeners=[];let tabURL=initialURL;const area={async get(k){if(Array.isArray(k))return Object.fromEntries(k.map(x=>[x,session[x]]));return{[k]:session[k]}},async set(v){Object.assign(session,v)},async setAccessLevel(){}};const events=()=>({addListener(){}});const chrome={runtime:{id:'extid',onInstalled:events(),onStartup:events(),onMessage:{addListener(fn){listeners.push(fn)}}},storage:{session:area,local:area},tabs:{async get(){return{id:7,url:tabURL,status:'complete'}},async sendMessage(_id,m){return content?content(m):{ok:true,empty:true,entryCount:0,endEvidence:true,scrollY:0}},async update(_id,v){if(v.url)tabURL=v.url},async goBack(){tabURL='https://mobile.yangkeduo.com/orders.html'}}};const ctx={self:null,chrome,URL,AbortController,setTimeout,clearTimeout,fetch:fetcher||global.fetch,crypto:webcrypto,console,__GOAUTO_TEST_TIMEOUT__:5};ctx.self=ctx;ctx.importScripts=()=>ctx.self.GoAutoBackfillCore=Core;vm.runInNewContext(source,ctx);const send=(m,s={id:'extid',url:'chrome-extension://extid/popup.html'})=>new Promise(resolve=>listeners[0](m,s,resolve));return{session,send}}
|
|
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
|
test('rejects content sender, serializes START, and persists stopped terminal',async()=>{const h=harness({content:async()=>new Promise(()=>{})});let r=await h.send({source:'goauto-popup',type:'START',tabId:7,clientKey:'k',frozen:{origin:'https://admin.test',days:2,startedAt:new Date().toISOString()}},{id:'extid',url:'https://mobile.yangkeduo.com/orders.html',tab:{}});assert.equal(r.ok,false);r=await h.send({source:'goauto-popup',type:'START',tabId:7,clientKey:'k',frozen:{origin:'https://admin.test',days:2,startedAt:new Date().toISOString()}});assert.equal(r.ok,true);const second=await h.send({source:'goauto-popup',type:'START',tabId:7,clientKey:'k2',frozen:{origin:'https://other.test',days:2,startedAt:new Date().toISOString()}});assert.equal(second.ok,false);await h.send({source:'goauto-popup',type:'STOP'});await sleep(120);assert.equal(h.session.orderBackfillRun.phase,'stopped')});
|
|
test('runtime error persists failed terminal state',async()=>{const h=harness({content:async()=>{throw Error('synthetic failure')}});await h.send({source:'goauto-popup',type:'START',tabId:7,clientKey:'k',frozen:{origin:'https://admin.test',days:2,startedAt:new Date().toISOString()}});await sleep(120);assert.equal(h.session.orderBackfillRun.phase,'failed');assert.equal(h.session.orderBackfillRun.error.message,'synthetic failure')});
|
|
test('restart resumes stable batch with frozen origin/key and validates response',async()=>{let call;const stable='00000000-0000-4000-8000-000000000316',item={addressSuffix:'_cg123',pddOrderNo:'A',orderSubmittedAt:'2026-09-18T02:00:00Z',pddOrderAmountCent:null},run={id:'run1',tabId:7,listURL:'https://mobile.yangkeduo.com/orders.html',phase:'submitting',frozen:{origin:'https://frozen.test',days:2,startedAt:'2026-09-18T03:00:00Z'},batches:[{index:0,requestId:stable,status:'sending',items:[item],results:[],attempts:1}],results:[],rows:[],localErrors:[],stats:{read:1,ready:1,written:0,conflicts:0,skippedNoCG:0},scanComplete:true,stopRequested:false};const h=harness({initial:{orderBackfillRun:run,orderBackfillSecret:{runId:'run1',origin:'https://frozen.test',clientKey:'secret'}},fetcher:async(url,o)=>{call={url,auth:o.headers.Authorization,body:JSON.parse(o.body)};return{ok:true,async json(){return{data:{items:[{index:0,taskId:123,result:'backfilled',code:'BACKFILLED',retryable:false}]}}}}}});await sleep(10);assert.equal(call.url,'https://frozen.test/api/client/v1/purchase-tasks/order-backfill');assert.equal(call.auth,'Bearer secret');assert.equal(call.body.requestId,stable);assert.equal(h.session.orderBackfillRun.phase,'completed')});
|
|
test('timeout remains retryable and does not confirm batch',async()=>{const item={addressSuffix:'_cg123',pddOrderNo:'A'},run={id:'run2',tabId:7,listURL:'https://mobile.yangkeduo.com/orders.html',phase:'submitting',frozen:{origin:'https://frozen.test',days:2,startedAt:'2026-09-18T03:00:00Z'},batches:[{index:0,requestId:'run2:batch:0',status:'pending',items:[item],results:[],attempts:0}],results:[],rows:[],localErrors:[],stats:{read:1,ready:1,written:0,conflicts:0,skippedNoCG:0},scanComplete:true,stopRequested:false};const h=harness({initial:{orderBackfillRun:run,orderBackfillSecret:{runId:'run2',origin:'https://frozen.test',clientKey:'secret'}},fetcher:(_u,o)=>new Promise((_r,reject)=>o.signal.addEventListener('abort',()=>reject(Object.assign(Error('abort'),{name:'AbortError'}))))});h.session.__unused=1;await sleep(30);assert.notEqual(h.session.orderBackfillRun.batches[0].status,'confirmed')});
|
|
test('retryable item response remains resumable and unconfirmed',async()=>{const item={addressSuffix:'_cg123',pddOrderNo:'A'},run={id:'run3',tabId:7,listURL:'https://mobile.yangkeduo.com/orders.html',phase:'submitting',frozen:{origin:'https://frozen.test',days:2,startedAt:'2026-09-18T03:00:00Z'},batches:[{index:0,requestId:'00000000-0000-4000-8000-000000000317',status:'pending',items:[item],results:[],attempts:0}],results:[],rows:[],localErrors:[],stats:{read:1,ready:1,written:0,conflicts:0,skippedNoCG:0},scanComplete:true,stopRequested:false};const h=harness({initial:{orderBackfillRun:run,orderBackfillSecret:{runId:'run3',origin:'https://frozen.test',clientKey:'secret'}},fetcher:async()=>({ok:true,async json(){return{data:{items:[{index:0,taskId:123,result:'failed',code:'INTERNAL',retryable:true}]}}}})});await sleep(15);assert.equal(h.session.orderBackfillRun.phase,'incomplete');assert.equal(h.session.orderBackfillRun.batches[0].status,'retryable');assert.equal(h.session.orderBackfillRun.results.length,0)});
|
|
test('scan compares after lazy load and reaches explicit end evidence',async()=>{let count=1;const h=harness({content:async m=>{if(m.type==='SCROLL'){setTimeout(()=>count++,20);return{ok:true}}if(m.type==='LIST')return{ok:true,entryCount:count,endEvidence:count>=4,scrollY:count*100};return new Promise(()=>{})}});await h.send({source:'goauto-popup',type:'START',tabId:7,clientKey:'k',frozen:{origin:'https://admin.test',days:2,startedAt:new Date().toISOString()}});await sleep(3200);assert.equal(h.session.orderBackfillRun.entryCount,4);assert.equal(h.session.orderBackfillRun.scanComplete,true);assert.notEqual(h.session.orderBackfillRun.incompleteReason,'未看到列表末尾标记')});
|
|
test('reading restart after saved row on list advances cursor without duplicate',async()=>{const saved={kind:'skip',code:'CG_MISSING',item:{}},run={id:'read1',tabId:7,listURL:'https://mobile.yangkeduo.com/orders.html',phase:'reading',detailState:'returning',entryCount:1,cursor:0,scrollY:0,rows:[saved],results:[],batches:[],localErrors:[],stats:{read:1,ready:0,written:0,conflicts:0,skippedNoCG:0},frozen:{origin:'https://admin.test',days:2,startedAt:new Date().toISOString()},scanComplete:true,stopRequested:false};const h=harness({initial:{orderBackfillRun:run,orderBackfillSecret:{runId:'read1',origin:'https://admin.test',clientKey:'k'}}});await sleep(20);assert.equal(h.session.orderBackfillRun.cursor,1);assert.equal(h.session.orderBackfillRun.rows.length,1);assert.equal(h.session.orderBackfillRun.phase,'completed')});
|
|
test('reading restart on detail finishes current row then returns to list',async()=>{const run={id:'read2',tabId:7,listURL:'https://mobile.yangkeduo.com/orders.html',phase:'reading',detailState:'detail',entryCount:1,cursor:0,scrollY:200,rows:[],results:[],batches:[],localErrors:[],stats:{read:0,ready:0,written:0,conflicts:0,skippedNoCG:0},frozen:{origin:'https://admin.test',days:2,startedAt:new Date().toISOString()},scanComplete:true,stopRequested:false};const h=harness({initialURL:'https://mobile.yangkeduo.com/order.html',initial:{orderBackfillRun:run,orderBackfillSecret:{runId:'read2',origin:'https://admin.test',clientKey:'k'}},content:async m=>m.type==='DETAIL'?{ok:true,classified:{kind:'skip',code:'CG_MISSING',item:{}}}:{ok:true}});await sleep(1100);assert.equal(h.session.orderBackfillRun.cursor,1);assert.equal(h.session.orderBackfillRun.rows.length,1);assert.equal(h.session.orderBackfillRun.phase,'completed')});
|