research-collection-control.test.cjs 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. const test=require('node:test');
  2. const assert=require('node:assert/strict');
  3. const fs=require('node:fs');
  4. const path=require('node:path');
  5. const vm=require('node:vm');
  6. const source=fs.readFileSync(path.resolve('work/database-enrichment-20261110/collect.cjs'),'utf8');
  7. async function simulate(httpStatus,size){
  8. const directory=path.resolve('work/test-collector'),files=new Map();let calls=0;
  9. const products=Array.from({length:size},(_,i)=>({productId:'jd:'+i,itemId:String(i),platform:'jd',missingReviews:true}));
  10. files.set(path.join(directory,'product-queue.json'),JSON.stringify(products));
  11. files.set(path.join(directory,'social-gaps.json'),'[]');
  12. const fakeFs={readFileSync:p=>{if(!files.has(p))throw Error('UNEXPECTED_FILE_READ');return files.get(p);},
  13. writeFileSync:(p,b)=>files.set(p,b),existsSync:p=>files.has(p),mkdirSync(){},
  14. copyFileSync:(a,b)=>files.set(b,files.get(a)),renameSync:(a,b)=>{files.set(b,files.get(a));files.delete(a);}};
  15. const fromVoc=id=>{
  16. if(id.includes('catalog'))return {searchVocApis:async()=>({}),getVocApiDoc:async()=>({status:'ok',data:{endpoint:{method:'GET',channel:'ecommerce',params:[]}}})};
  17. if(id.includes('credentials'))return {readCredentialContext:()=>({newApiToken:'synthetic-test-token'})};
  18. const call=async()=>{calls++;throw Object.assign(Error('SIMULATED_GATEWAY_RESPONSE'),{httpStatus,kind:'upstream'});};
  19. return {callSocialGateway:call,callEcommerceGateway:call};
  20. };
  21. const fakeProcess={argv:['node','collect.cjs','--all'],exitCode:0};
  22. const sandbox={__dirname:directory,Buffer,AbortSignal,Date,Promise,process:fakeProcess,
  23. console:{log(){},error(){}},fetch:()=>{throw Error('NETWORK_NOT_ALLOWED_IN_TEST');},
  24. setTimeout:fn=>{fn();return 0;},
  25. require:id=>id==='node:fs'?fakeFs:id==='node:module'?{createRequire:()=>fromVoc}:require(id)};
  26. sandbox.global=sandbox;
  27. await vm.runInNewContext(source,sandbox);
  28. return {calls,result:JSON.parse(files.get(path.join(directory,'all-collection-result.json'))),exitCode:fakeProcess.exitCode};
  29. }
  30. test('terminal gateway failures stop the queue without claiming source deletion',async()=>{
  31. for(const status of [401,402,403]){
  32. const actual=await simulate(status,5000);
  33. assert.ok(actual.calls<=6,'Only the active workers may complete');
  34. assert.equal(actual.result.stopReason.code,'UPSTREAM_HTTP_'+status);
  35. assert.equal(actual.result.stopReason.sourceRemovalConfirmed,false);
  36. assert.ok(actual.result.remainingJobs>4990);
  37. assert.equal(actual.exitCode,2);
  38. }
  39. });
  40. test('a transient upstream failure does not mark all remaining links unavailable',async()=>{
  41. const actual=await simulate(503,8);
  42. assert.equal(actual.calls,8);
  43. assert.equal(actual.result.stopReason,null);
  44. assert.equal(actual.result.failed,8);
  45. });