import assert from 'node:assert/strict' import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs' import { resolve, join } from 'node:path' import { tmpdir } from 'node:os' import { createHash } from 'node:crypto' import { pathToFileURL } from 'node:url' import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs' import { run as fixedCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs' import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs' import { prepareFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' import { prepareBolAppointments } from './admin-test-env-modules/dispatch-workflow-extension-acceptance.mjs' const ROOT = '/api/platform/v1/last-leg', PDA = '/api/pda/v1/last-leg' const slots = ['LOADING_SCENE','LOADING_LIST','PRE_DEPARTURE_CHECK','DRIVER_LICENSE','RECEIPT'] const id = value => String(value ?? '') const hash = bytes => createHash('sha256').update(bytes).digest('hex') export function parseFlowArguments(args) { const config = { execute: false, keepPda: false } for(let i=0;im.receiptVerified && m.customerId===previous.customerId));report.members=previous.members;report.dispatchNo=previous.dispatchNo;report.resumedFrom=config.resumeReport} let sequence=0,picker const clean=value=>Array.isArray(value)?value.map(clean):value && typeof value==='object'?Object.fromEntries(Object.entries(value).map(([key,v])=>[key,/password|token|authorization/i.test(key)?'[REDACTED]':clean(v)])):typeof value==='string' && secrets.includes(value)?'[REDACTED]':value const save=()=>writeFileSync(join(directory,'report.json'),JSON.stringify(clean({...report,requestCount:sequence,createdResources:context.createdResources}),null,2)) context.checkpoint=async()=>save() const check=(label,condition)=>{report.checks.push({label,passed:!!condition});save();assert(condition,label)} const key=action=>hash(`${context.marker}:${config.runId}:${action}`) for(const method of ['request','requestMultipart']) { const original=context[method].bind(context) context[method]=async(...args)=>{ const file=join(directory,'requests',`${++sequence}.json`), record={method:args[0],path:args[1],body:method==='request'?clean(args[2]):{fileName:args[2]?.name},stage:'INTENT'} writeFileSync(file,JSON.stringify(record,null,2)) try{const response=await original(...args);if(args[1]==='/api/auth/login' && response.body?.data?.accessToken){secrets.push(response.body.data.accessToken,response.body.data.refreshToken);sessions.push(response.body.data)}record.stage='RESPONSE';record.response=clean(response);writeFileSync(file,JSON.stringify(record,null,2));save();return response} catch(error){record.stage='UNKNOWN';record.error=error.message;writeFileSync(file,JSON.stringify(record,null,2));throw error} } } const send=async(method,path,body,token=context.token,deny=false)=>{ const response=await context.request(method,path,body,token,{}) if(deny){check(`rejected ${path}`,id(response.body?.code)==='A0409');return response.body} if(response.http!==200 || id(response.body?.code)!=='200')throw Error(`${method} ${path}: HTTP ${response.http}, ${response.body?.code}, ${response.body?.message}, requestId=${response.requestId}`) return response.body.data } const upload=async(path,name,token=context.token)=>{ const r=await context.requestMultipart('POST',path,{name,type:'image/png',content:image},token) if(r.http!==200 || id(r.body?.code)!=='200')throw Error(`Upload failed ${path}: ${r.body?.message}, requestId=${r.requestId}`) return r.body.data } // 二进制助手使用当前会话;串行切换后立即恢复,确保实际覆盖 PDA 文件权限。 const binary=async(path,token=context.token)=>{const previous=context.token;try{context.token=token;return await context.requestBinary('GET',path)}finally{context.token=previous}} const phase=value=>{report.phase=value;save();console.log(JSON.stringify({phase:value,dispatchNo:report.dispatchNo}))} try { const auth=await send('POST','/api/auth/login',{loginId:options.username,password:options.password},'');context.token=auth.accessToken;context.refreshToken=auth.refreshToken await fixedCustomer(context,{requireDistinctCustomer:false});const customer=context.context.merchantQualificationFixture const merchant=await send('POST','/api/auth/login',{loginId:customer.username,password:customer.password},'');delete context.context.merchantQualificationFixture report.customerId=id(customer.tenantId) const access=await resolvePickerAccess(context);assert(access,'Existing PDA role missing') picker=await createPicker(context,82,config.warehouseId,access);assert(picker?.token,'PDA login failed');secrets.push(picker.password) const master=await send('GET',`/api/platform/v1/warehouse-admin/${config.destinationId}?tenantId=0`) assert(master.warehouse.status==='ACTIVE' && master.warehouse.warehouseType==='FBA' && master.warehouse.timezone.includes('/')) const address=master.addresses.find(a=>a.addressLine1), own=`?customerId=${report.customerId}` if(!previous){ phase('RECEIVING_TWO_TEST_PALLETS') for(const suffix of ['A','B']) { const m={customerId:report.customerId,marker:context.marker,receiptVerified:false};report.members.push(m) const merchantRoot='/api/merchant/v1/last-leg/warehouse-transfers' const body={sourceLastlegOrderIds:[],operatingWarehouseId:config.warehouseId,fbaWarehouseCode:master.warehouse.warehouseCode,destinationType:'FBA',destinationWarehouseId:config.destinationId,destinationWarehouseCode:master.warehouse.warehouseCode,destinationWarehouseName:master.warehouse.warehouseName,deliveryAddress:[address.countryCode,address.city,address.addressLine1,address.postalCode].filter(Boolean).join(' '),packageCount:1,volumeCbm:1,deliveryMethod:'CUSTOMER_DELIVERY',estimatedArrivalAt:new Date(Date.now()+3600000).toISOString().slice(0,19),needRelabel:false,remark:`${context.marker} TEST synthetic ${suffix}`,idempotencyKey:key(`CREATE-${suffix}`),fbaShipmentNo:`TEST-FBA-${config.runId}-${suffix}`,items:[{skuCode:`TEST-${config.runId}-${suffix}`,productNameZh:'TEST验收货物',packageCount:1,quantity:1}]} let incoming=await send('POST',merchantRoot,body,merchant.accessToken);m.incomingNo=incoming.transferNo;save() const mp=`${merchantRoot}/${encodeURIComponent(m.incomingNo)}`, pp=`${PDA}/warehouse-transfers/${encodeURIComponent(m.incomingNo)}` await send('POST',mp+'/submit',undefined,merchant.accessToken) await send('POST',`${ROOT}/warehouse-transfers/${encodeURIComponent(m.incomingNo)}/review${own}`,{decision:'APPROVE',remark:context.marker}) const task=await send('GET',pp,undefined,picker.token);assert.equal(task.cartons.length,1) const draft=await send('POST',pp+'/pallets/drafts',{idempotencyKey:key(`DRAFT-${suffix}`)},picker.token), palletPath=pp+'/pallets/'+draft.pallets[0].id const generated=await send('POST',palletPath+'/generate',{idempotencyKey:key(`GENERATE-${suffix}`)},picker.token) m.palletId=id(generated.pallets[0].warehousePalletId);m.palletNo=generated.pallets[0].palletNo await send('POST',palletPath+'/bind',{palletCode:m.palletNo,idempotencyKey:key(`BIND-${suffix}`)},picker.token) await send('POST',pp+'/cartons/count',{idempotencyKey:key(`COUNT-${suffix}`),forecastPackageCount:1,boxes:[{cartonMark:task.cartons[0].cartonMark,transferItemId:id(task.items[0].id),transferPalletId:id(draft.pallets[0].id),length:100,width:100,height:100,weight:100,items:[{skuCode:task.items[0].skuCode,quantity:1}]}]},picker.token) const photo=await upload(pp+'/measurement-photos/files',`${config.runId}-TEST-measurement.png`,picker.token) await send('POST',pp+'/palletize',{idempotencyKey:key(`PALLETIZE-${suffix}`),photoFileIds:[id(photo.fileId)]},picker.token) incoming=await send('POST',mp+'/confirm-measurement',{idempotencyKey:key(`CUSTOMER-${suffix}`),remark:context.marker},merchant.accessToken) check('received '+m.incomingNo,incoming.status==='READY_FOR_DISPATCH') const grouped=await send('GET',`${ROOT}/inventory/balances?`+new URLSearchParams({customerId:m.customerId,overseasWarehouseId:config.warehouseId,inboundCode:m.incomingNo,page:'1',size:'100'})) assert.equal(grouped.records.length,1,'One exact receipt balance expected');const balance=grouped.records[0] m.balanceId=id(balance.id);m.receipt={balance:await send('GET',`${ROOT}/inventory/balances/${m.balanceId}${own}`)} const positions=await send('GET',`${ROOT}/inventory/balances/${m.balanceId}/positions${own}`);m.positionId=id(positions.find(p=>id(p.palletId)===m.palletId).id) const candidates=await send('GET',`${ROOT}/dispatches/candidates?`+new URLSearchParams({warehouseId:config.warehouseId,orderNo:m.incomingNo})) const candidate=candidates.candidates.find(c=>c.palletNo===m.palletNo && id(c.customerId)===m.customerId);assert(candidate?.selection);m.selection=candidate.selection;m.destination=candidate.destination;m.receiptVerified=true;save() } phase('CONFIRMED_TEST_APPOINTMENT') const allocations=await prepareBolAppointments(context,report.members,'bol-same',config.warehouseId) for(const m of report.members){const fresh=await send('GET',`${ROOT}/dispatches/candidates?`+new URLSearchParams({warehouseId:config.warehouseId,orderNo:m.incomingNo}));m.selection=fresh.candidates.find(c=>c.palletNo===m.palletNo).selection} const created=await send('POST',ROOT+'/dispatches',{warehouseId:config.warehouseId,vehicle:null,driver:null,plannedDepartureAt:null,estimatedArrivalAt:null,remark:`${context.marker} TEST HTTP/UI`,cargo:report.members.map(m=>m.selection),batchAllocations:allocations,idempotencyKey:key('DISPATCH')}) report.dispatchNo=created.dispatchNo;assert(report.dispatchNo);save() } const path=ROOT+'/dispatches/'+report.dispatchNo, pp=PDA+'/dispatches/'+report.dispatchNo if(config.stopAfterDraft){ const draft=await send('GET',path) check('UI fixture remains an unpicked draft',draft.stage==='BEFORE_LOADING' && draft.reviewState==='DRAFT' && Number(draft.counts.pickedPackageCount)===0) report.final=draft;report.executionScope='DRAFT_FIXTURE_ONLY';report.fullFlowPassed=false;report.success=true phase('DRAFT_READY_FOR_UI') return {success:true,executionScope:report.executionScope,dispatchNo:report.dispatchNo,report:join(directory,'report.json')} } if(!previous){const created=await send('GET',path);await send('POST',path+'/submit-review',{recordVersion:created.recordVersion,idempotencyKey:key('NO_SCHEDULE'),remark:'TEST missing transport schedule must block'},context.token,true)} phase('REVIEWS_AND_REAL_PICKS') if(previous){ const current=await send('GET',path), proof=previous.createdResources.fleetTrips[0] assert.equal(current.stage,'BEFORE_LOADING');assert.equal(current.reviewState,'DRAFT');assert.equal(Number(current.counts.pickedPackageCount),0);assert.equal(id(proof.warehouseId),config.warehouseId) assert.equal(previous.success,false) assert((previous.createdResources.fleetTripCommands||[]).every(c=>c.status==='RESPONDED' && c.http===200 && String(c.code)!=='200'),'Only a pre-write failure or definite rejected preparation may resume') report.trip=await prepareFleetTrip(context,'dispatch-loading-flow',report.dispatchNo,config.warehouseId) }else report.trip=await prepareFleetTrip(context,'dispatch-loading-flow',report.dispatchNo,config.warehouseId) if(config.verifyLoadingDocuments){ phase('RESOURCE_PHOTO_ROUNDS_AND_FIVE_PER_CATEGORY') const read=()=>send('GET',path+'/loading-documents') const first=await read(), firstRound=id(first.reviewRound) check('approved trip evidence automatically imported',first.photos.some(p=>p.slot==='LOADING_SCENE') && first.photos.some(p=>p.slot==='LOADING_LIST') && first.photos.every(p=>['LOADING_SCENE','LOADING_LIST'].includes(p.slot))) const append=async(slot,label,round,platform=false,replaceFileId)=>{ const base=(platform?path:pp)+'/loading-documents',token=platform?context.token:picker.token,uploadKey=key('UP-'+label) const photo=await upload(`${base}/${slot}/uploads?idempotencyKey=${uploadKey}&reviewRound=${round}`,`${config.runId}-TEST-${label}.png`,token) const current=await read(),body={recordVersion:current.recordVersion,idempotencyKey:key('SAVE-'+label),fileId:id(photo.fileId),uploadKey,...(replaceFileId?{replaceFileId}:{})} return {photo,body,base,token} } for(const slot of slots.slice(2)){ const p=await append(slot,'BEFORE-'+slot,firstRound) await send('PUT',`${p.base}/${slot}`,p.body,p.token) } const pending=await append('PRE_DEPARTURE_CHECK','OLD-PENDING',firstRound) const beforeChange=await read(),oldFiles=new Set(beforeChange.photos.map(p=>id(p.fileId))) let detail=await send('GET',path) check('replacement resources differ from original',id(detail.vehicleInfo.vehicle.vehicleId)!==config.replacementVehicleId && id(detail.vehicleInfo.driver.driverId)!==config.replacementDriverId) const vehicleArchive=await send('GET',`${ROOT}/fleet/resource-photos/VEHICLE/${config.replacementVehicleId}`) const driverArchive=await send('GET',`${ROOT}/fleet/drivers/${config.replacementDriverId}`) const driverPortrait=await send('GET',`${ROOT}/fleet/resource-photos/DRIVER/${config.replacementDriverId}`) assert(vehicleArchive.photo?.fileId && driverArchive.licenseFileId && driverPortrait.photo?.fileId,'Replacement resources require maintained vehicle photo, approved driver license and driver portrait') await send('POST',path+'/return-to-review',{recordVersion:detail.recordVersion,idempotencyKey:key('RETURN'),reason:'TEST zero-pick resource evidence invalidation'}) detail=await send('GET',path) let result=await send('PUT',path,{recordVersion:detail.recordVersion,idempotencyKey:key('SWAP'),maintenance:detail.vehicleInfo.maintenance, vehicle:{mode:'MASTER',vehicleId:config.replacementVehicleId},driver:{mode:'MASTER',driverId:config.replacementDriverId,evidence:{expectedTaskEndAt:report.trip.expectedDriverEndAt}}, plannedDepartureAt:report.trip.updateRequest.plannedDepartureAt,estimatedArrivalAt:report.trip.updateRequest.estimatedArrivalAt,remark:'TEST replacement with maintained archives'}) for(const action of ['submit-review','dispatch-review','loading-review'])result=await send('POST',`${path}/${action}`,{recordVersion:result.recordVersion,idempotencyKey:key('SECOND-'+action),...(action==='submit-review'?{remark:'TEST second review'}:{decision:'APPROVED',reason:'TEST second review'})}) const second=await read(),round=id(second.reviewRound) check('new review round invalidates all old resource evidence',Number(round)>Number(firstRound) && second.photos.length===2 && second.photos.every(p=>!oldFiles.has(id(p.fileId))) && ['LOADING_SCENE','LOADING_LIST'].every(s=>second.photos.some(p=>p.slot===s))) const sourcePaths={LOADING_SCENE:`${ROOT}/fleet/drivers/${config.replacementDriverId}/license/${driverArchive.licenseFileId}`,LOADING_LIST:`${ROOT}/fleet/resource-photos/VEHICLE/${config.replacementVehicleId}/${vehicleArchive.photo.fileId}/content`} report.importedArchives=[] for(const photo of second.photos){ const original=await context.requestBinary('GET',sourcePaths[photo.slot]) const copied=await context.requestBinary('GET',`${path}/loading-documents/${photo.slot}/files/${photo.fileId}/content`) const pdaCopy=await binary(`${pp}/loading-documents/${photo.slot}/files/${photo.fileId}/content`,picker.token) report.importedArchives.push({slot:photo.slot,fileId:photo.fileId,contentType:copied.contentType,sourceHttp:original.http,copyHttp:copied.http,pdaHttp:pdaCopy.http,sha256:hash(copied.bytes),requestId:copied.requestId,traceId:copied.traceId}) check('maintained archive copied exactly '+photo.slot,original.http===200 && copied.http===200 && pdaCopy.http===200 && original.bytes.length>0 && hash(original.bytes)===hash(copied.bytes) && hash(original.bytes)===hash(pdaCopy.bytes)) } await send('PUT',`${pending.base}/PRE_DEPARTURE_CHECK`,{...pending.body,recordVersion:second.recordVersion},pending.token,true) check('stale upload bind preserves current photos',JSON.stringify(await read())===JSON.stringify(second)) const stale=await context.requestMultipart('POST',`${pp}/loading-documents/PRE_DEPARTURE_CHECK/uploads?idempotencyKey=${key('STALE-ROUND')}&reviewRound=${firstRound}`,{name:'TEST-stale-round.png',type:'image/png',content:image},picker.token) check('stale review round upload rejected',id(stale.body?.code)==='A0409') for(const [slotIndex,slot] of slots.entries()){ let docs=await read(),count=docs.photos.filter(p=>p.slot===slot).length for(let index=count;index<5;index++){ const p=await append(slot,`${slot}-${index}`,round,(index+slotIndex)%2===0) await send('PUT',`${p.base}/${slot}`,p.body,p.token) } docs=await read();check('five saved '+slot,docs.photos.filter(p=>p.slot===slot).length===5) const excess=await append(slot,'EXCESS-'+slot,round) const rejected=await context.request('PUT',`${excess.base}/${slot}`,excess.body,excess.token,{}) check('sixth rejected '+slot,['A0400','A0409'].includes(id(rejected.body?.code))) check('sixth rejection preserves version '+slot,JSON.stringify(await read())===JSON.stringify(docs)) const replace=await append(slot,'REPLACE-'+slot,round,true,id(docs.photos.find(p=>p.slot===slot).fileId)) const replaced=await send('PUT',`${replace.base}/${slot}`,replace.body,replace.token) const replay=await send('PUT',`${replace.base}/${slot}`,replace.body,replace.token) check('full category replacement and replay '+slot,replaced.photos.filter(p=>p.slot===slot).length===5 && replaced.photos.some(p=>id(p.fileId)===id(replace.photo.fileId)) && !replaced.photos.some(p=>id(p.fileId)===replace.body.replaceFileId) && JSON.stringify(replay)===JSON.stringify(replaced)) } const adminDocs=await read(),pdaDocs=await send('GET',pp+'/loading-documents',undefined,picker.token) check('Admin and PDA share all 25 photos',adminDocs.photos.length===25 && adminDocs.complete && JSON.stringify(adminDocs)===JSON.stringify(pdaDocs)) report.sharedFiles=[] for(const photo of adminDocs.photos){ const admin=await context.requestBinary('GET',`${path}/loading-documents/${photo.slot}/files/${photo.fileId}/content`) const pda=await binary(`${pp}/loading-documents/${photo.slot}/files/${photo.fileId}/content`,picker.token) report.sharedFiles.push({slot:photo.slot,fileId:photo.fileId,adminHttp:admin.http,pdaHttp:pda.http,adminSha256:hash(admin.bytes),pdaSha256:hash(pda.bytes),requestId:pda.requestId,traceId:pda.traceId}) check('Admin and PDA file bytes '+photo.slot+' '+photo.fileId,admin.http===200 && pda.http===200 && hash(admin.bytes)===hash(image) && hash(pda.bytes)===hash(image)) } report.loadingDocumentVerification={reviewRound:round,photos:25,replacementVehicleId:config.replacementVehicleId,replacementDriverId:config.replacementDriverId} } for(const [index,m] of report.members.entries()) { const task=await send('GET',pp,undefined,picker.token),item=task.items.find(i=>i.palletNo===m.palletNo);assert(item);m.dispatchItemId=id(item.dispatchItemId) const body={recordVersion:task.recordVersion,positionId:m.positionId,packages:1,productQuantity:1,idempotencyKey:key('PICK-'+index),remark:context.marker} // 新实收库存仍须走当前仓租预校验,不能用旧脚本绕过正式扫码前置。 let preparation=await send('POST',PDA+'/storage-rent/preparations',{mode:'DISPATCH',businessNo:report.dispatchNo,itemId:m.dispatchItemId,positionId:m.positionId,packages:1,productQuantity:1,idempotencyKey:body.idempotencyKey,cartonMarks:[]},picker.token) for(let attempt=0;!preparation.ready && preparation.status==='FREEZE_PENDING' && attempt<20;attempt++){await new Promise(resolve=>setTimeout(resolve,500));preparation=await send('GET',PDA+'/storage-rent/preparations/'+preparation.preparationId,undefined,picker.token)} check('storage rent ready before pick '+index,preparation.ready===true) const picked=await send('POST',`${pp}/items/${m.dispatchItemId}/pick`,body,picker.token) const replay=await send('POST',`${pp}/items/${m.dispatchItemId}/pick`,body,picker.token);check('pick replay '+index,replay.replayed===true && id(replay.recordVersion)===id(picked.recordVersion)) if(index===0){const preview=await send('GET',pp+'/loading',undefined,picker.token);await send('POST',pp+'/loading-complete',{recordVersion:preview.recordVersion,manifestHash:preview.manifestHash,idempotencyKey:key('INCOMPLETE')},picker.token,true)} } const ledger=async()=>Promise.all(report.members.map(m=>send('GET',`${ROOT}/inventory/ledger?`+new URLSearchParams({customerId:m.customerId,overseasWarehouseId:config.warehouseId,inboundCode:m.incomingNo,page:'1',size:'100'})))) report.stockAfterPick=await ledger() phase('PDA_PHOTOS_AND_ADMIN_FILE_READBACK') const initialDocuments=await send('GET',pp+'/loading-documents',undefined,picker.token),reviewRound=id(initialDocuments.reviewRound),uploadedFiles=new Set() assert.match(reviewRound,/^[1-9]\d*$/) if(!config.verifyLoadingDocuments)for(const slot of slots){const uploadKey=key('UPLOAD-'+slot);const up=await upload(`${pp}/loading-documents/${slot}/uploads?idempotencyKey=${uploadKey}&reviewRound=${reviewRound}`,`${config.runId}-TEST-${slot}.png`,picker.token);const docs=await send('GET',pp+'/loading-documents',undefined,picker.token);assert.equal(id(docs.reviewRound),reviewRound);await send('PUT',`${pp}/loading-documents/${slot}`,{recordVersion:docs.recordVersion,idempotencyKey:key('BIND-'+slot),fileId:id(up.fileId),uploadKey},picker.token);uploadedFiles.add(id(up.fileId))} const docs=await send('GET',path+'/loading-documents');check('Admin sees all saved photos',docs.complete && docs.photos.length===initialDocuments.photos.length+(config.verifyLoadingDocuments?0:5)) report.photos=[] for(const photo of docs.photos){const r=await context.requestBinary('GET',`${path}/loading-documents/${photo.slot}/files/${photo.fileId}/content`);check('Admin bytes '+photo.slot,r.http===200 && r.bytes.length>0 && (!uploadedFiles.has(id(photo.fileId)) || hash(r.bytes)===hash(image)));report.photos.push({slot:photo.slot,fileId:photo.fileId,sha256:hash(r.bytes)})} phase('AUTOMATIC_OUTBOUND_AND_DELIVERY') const preview=await send('GET',pp+'/loading',undefined,picker.token),body={recordVersion:preview.recordVersion,manifestHash:preview.manifestHash,idempotencyKey:key('LOAD'),remark:'TEST scanned pallets; no manual measurements'} const loaded=await send('POST',pp+'/loading-complete',body,picker.token);check('automatic departure',loaded.stage==='DELIVERING') const after=await send('GET',path),replay=await send('POST',pp+'/loading-complete',body,picker.token) check('loading replay',replay.replayed && id(replay.recordVersion)===id(loaded.recordVersion)) assert.deepEqual(await send('GET',path),after);assert.deepEqual(await ledger(),report.stockAfterPick) check('one customer outbound and destination delivery',after.documents.outbounds.length===1 && after.documents.deliveries.length===1 && after.documents.outbounds[0].status==='OUTBOUNDED' && after.documents.deliveries[0].status==='DELIVERING' && Number(after.counts.shippedPackageCount)===2) report.final=after;report.success=true;phase('HTTP_PASSED_AWAITING_UI') if(config.keepPda){report.privateUiSession=join(tmpdir(),`dispatch-ui-${config.runId}.json`);writeFileSync(report.privateUiSession,JSON.stringify(picker),{mode:0o600,flag:'wx'})} } catch(error){report.error=error.message;report.success=false;phase('FAILED')} finally { if(picker?.membershipId && !(report.success && config.keepPda))await disablePicker(context,picker,82).catch(e=>{report.cleanupError=e.message}) for(const session of sessions)await context.request('POST','/api/auth/logout',{refreshToken:session.refreshToken},session.accessToken,{}).catch(()=>{}) save() } return {success:report.success,error:report.error,dispatchNo:report.dispatchNo,report:join(directory,'report.json'),privateUiSession:report.privateUiSession} } if(process.argv[1] && import.meta.url===pathToFileURL(resolve(process.argv[1])).href){try{const result=await runLoadingFlow(parseFlowArguments(process.argv.slice(2)));console.log(JSON.stringify(result));if(result.success===false)process.exitCode=1}catch(error){console.error(error.message);process.exitCode=1}}