const test = require('node:test'); const assert = require('node:assert/strict'); const {harness,response} = require('./helpers/arr_ui_harness.cjs'); const requestId='a'.repeat(32); const task={request_id:requestId,report_date:'2026-10-07',status:'needs_data_review',job_id:null,can_retry:false}; const item=(extra={})=>({item_id:'1:BLOCK_CODE',source_sequence:1,confirmation_no:'SYNTHETIC-001',room_no:'SYNTHETIC-ROOM',company_name:'Synthetic Company',rate_code:'SYNTHETIC',field:'BLOCK_CODE',field_label:'团队代码',reason_code:'MISSING',can_be_empty:true,value:null,confirmed:false,...extra}); const review=(items,revision=1)=>({request_id:requestId,report_date:task.report_date,status:'editing',revision,items,pending_count:items.filter(x=>!x.confirmed).length,total_count:items.length,can_finalize:items.every(x=>x.confirmed)}); const plain=value=>JSON.parse(JSON.stringify(value)); test('source review opens before a job exists and choosing another date retains the original request',async()=>{ const h=harness(()=>response(200,review([item()]))); await h.acceptARRDownloadTask(task,{sync:false}); assert.equal(h.element('#arr-download-data-review').hidden,false); assert.equal(h.element('#arr-data-review-panel').hidden,false); assert.equal(h.element('#arr-download-review').hidden,true); assert.equal(h.element('#arr-data-review-finalize').disabled,true); h.element('#arr-download-date').value='2026-10-08'; h.handleARRDownloadDateChange(); await h.submit(); assert.equal(h.state.arrDownloadIntent.request_id,requestId); assert.equal(h.state.arrDownloadTask.report_date,'2026-10-07'); assert.equal(h.element('#arr-download-date').value,'2026-10-08'); assert.equal(h.calls.every(call=>call.method==='GET'),true,'review never creates a replacement download'); }); test('incomplete review cannot finalize even when server flag says it can',async()=>{ const h=harness(()=>response(200,{...review([item()]),can_finalize:true,pending_count:0})); await h.acceptARRDownloadTask(task,{sync:false}); await h.finalizeARRDataReview(); assert.equal(h.arrDataReviewCanFinalize(),false); assert.equal(h.calls.length,1); assert.equal(h.element('#arr-data-review-message').textContent,'data_review.complete_first'); }); test('blank text is not silently confirmed; explicit empty saves its revision and reloads the review',async()=>{ let current=review([item()]); const h=harness((url,options)=>{ if(options.method==='POST') { assert.equal(url,`/api/arr-downloads/${requestId}/data-review/items/1%3ABLOCK_CODE`); assert.deepEqual(JSON.parse(options.body),{revision:1,value:''}); current=review([item({confirmed:true,value:''})],2); return response(200,{revision:2}); } return response(200,current); }); await h.acceptARRDownloadTask(task,{sync:false}); const row=h.row('1:BLOCK_CODE',''); await h.saveARRDataReviewItem(row.button); assert.equal(h.calls.length,1); assert.equal(h.element('#arr-data-review-message').textContent,'data_review.use_empty'); await h.saveARRDataReviewItem(row.button,{empty:true}); assert.equal(h.calls.length,3); assert.equal(h.state.arrDataReview.revision,2); assert.equal(h.arrDataReviewCanFinalize(),true); assert.equal(h.element('#arr-data-review-finalize').disabled,false); }); test('products are an ordered code array and explicit empty products are an empty array',()=>{ const h=harness(()=>{throw new Error('no request');}); const product=item({field:'PRODUCTS'}); assert.deepEqual(plain(h.parseARRDataReviewValue(product,'BREAKFAST\nDINNER\nBREAKFAST',false)),['BREAKFAST','DINNER','BREAKFAST']); assert.deepEqual(plain(h.parseARRDataReviewValue(product,'ignored',true)),[]); assert.equal(h.parseARRDataReviewValue(item({field:'EFFECTIVE_RATE_AMOUNT'}),'900.000000000001',false),'900.000000000001'); assert.equal(h.parseARRDataReviewValue(item({field:'ADULTS'}),'0',false),'0'); assert.throws(()=>h.parseARRDataReviewValue(item({field:'ADULTS'}),'1.5',false)); assert.throws(()=>h.parseARRDataReviewValue(item({field:'ARRIVAL'}),'2026-02-30',false)); }); test('revision conflict reloads the current revision before any next save',async()=>{ let reads=0; const h=harness((url,options)=>options.method==='POST' ? response(409,null,'ARR_DATA_REVIEW_CONFLICT') : response(200,review([item()],++reads===1 ? 1 : 5))); await h.acceptARRDownloadTask(task,{sync:false}); const row=h.row('1:BLOCK_CODE','VERIFIED'); h.trackARRDataReviewDraft(row.input); await h.saveARRDataReviewItem(row.button); assert.equal(h.state.arrDataReview.revision,5); assert.equal(h.state.arrDataReviewDrafts['1:BLOCK_CODE'],'VERIFIED'); assert.equal(h.element('#arr-data-review-message').textContent,'data_review.conflict'); assert.equal(h.state.arrDataReviewMutating,false); }); test('unconfirmed source values are never prefilled and all labels and stored values are escaped',async()=>{ const h=harness(()=>response(200,review([ item({company_name:'',value:'GUESSED-BLOCK'}), item({item_id:'2:RES_COMMENT',field:'RES_COMMENT',confirmed:true,value:''}), ]))); await h.acceptARRDownloadTask(task,{sync:false}); const markup=h.element('#arr-data-review-body').innerHTML; assert.equal(markup.includes('GUESSED-BLOCK'),false); assert.equal(markup.includes(''),false); assert.match(markup,/<img/); assert.match(markup,/<\/textarea>/); }); test('unsaved edits of confirmed fields block finalization until explicitly saved',async()=>{ const h=harness(()=>response(200,review([item({confirmed:true,value:'FIRST'})]))); await h.acceptARRDownloadTask(task,{sync:false}); assert.equal(h.arrDataReviewCanFinalize(),true); const row=h.row('1:BLOCK_CODE','SECOND'); h.trackARRDataReviewDraft(row.input); await h.finalizeARRDataReview(); assert.equal(h.arrDataReviewCanFinalize(),false); assert.equal(h.calls.length,1); row.input.value='FIRST'; h.trackARRDataReviewDraft(row.input); assert.equal(h.arrDataReviewCanFinalize(),true); }); test('lost save response re-reads durable confirmation rather than resubmitting the item',async()=>{ let current=review([item()]); const h=harness((url,options)=>{ if(options.method==='POST') { current=review([item({value:'VERIFIED',confirmed:true})],2); throw new TypeError('lost response'); } return response(200,current); }); await h.acceptARRDownloadTask(task,{sync:false}); const row=h.row('1:BLOCK_CODE','VERIFIED'); h.trackARRDataReviewDraft(row.input); await h.saveARRDataReviewItem(row.button); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); assert.equal(h.state.arrDataReview.revision,2); assert.equal(h.arrDataReviewCanFinalize(),true); }); test('finalize uses the same request and a lost response reconciles processing without a new download',async()=>{ let finalized=false; const h=harness((url,options)=>{ if(options.method==='POST') { assert.equal(url,`/api/arr-downloads/${requestId}/data-review/finalize`); assert.deepEqual(JSON.parse(options.body),{revision:7}); finalized=true; throw new TypeError('lost finalize response'); } if(url===`/api/arr-downloads/${requestId}`) return response(200,{...task,status:'processing'}); return response(200,review([item({confirmed:true,value:''})],7)); }); await h.acceptARRDownloadTask(task,{sync:false}); await h.finalizeARRDataReview(); assert.equal(finalized,true); assert.equal(h.state.arrDownloadTask.status,'processing'); assert.equal(h.state.arrDownloadIntent.request_id,requestId); assert.equal(h.state.arrDataReview,null); assert.equal(h.state.arrDataReviewFinalizing,false); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); assert.equal(h.calls.some(call=>call.url==='/api/arr-downloads'),false); }); test('an uncertain frozen finalize stays locked when the task and review cannot be reconciled',async()=>{ let finalizing=false; const h=harness((url,options)=>{ if(options.method==='POST') {finalizing=true;throw new TypeError('offline');} if(finalizing) throw new TypeError('offline'); return response(200,review([item({confirmed:true,value:''})])); }); await h.acceptARRDownloadTask(task,{sync:false}); await h.finalizeARRDataReview(); assert.equal(h.state.arrDataReviewFinalizing,true); assert.equal(h.state.arrDataReviewDisconnected,true); assert.equal(h.element('#arr-data-review-finalize').disabled,true); await h.submit(); assert.equal(h.calls.filter(call=>call.method==='POST').length,1); });