Fix production ARR reads and add audited source field review

This commit is contained in:
Wyndham ARR committed 2026-10-08 17:01:54 +08:00
1 parent 2417b1a41c
commit fe8735aac2
26 files changed
+2417 -157

No files matched your search

+68 -5
View File
@@ -9,7 +9,7 @@ const {chromium} = require(process.env.ARR_TEST_PLAYWRIGHT_MODULE || 'playwright
const root = path.resolve(__dirname, '../..');
let fixture, browser, base;
before(async () => {
fixture = spawn(path.join(root, '.venv/bin/python'), ['-u', '-c', `
fixture = spawn(process.env.ARR_TEST_PYTHON || path.join(root, '.venv/bin/python'), ['-u', '-c', `
from http.server import ThreadingHTTPServer
from arr_web.preview import ReadOnlyPreview
from arr_web.server import handler_for
@@ -30,12 +30,12 @@ after(async () => {
if (browser) await browser.close();
if (fixture && fixture.exitCode === null) { fixture.kill(); await once(fixture, 'exit'); }
});
const config = (extra={}) => ({ready:true, default_date:'2026-09-15', latest_task:null, ...extra});
const config = (extra={}) => ({context_id:'synthetic-browser',ready:true, default_date:'2026-09-15', latest_task:null, ...extra});
const fulfill = (route, data) => route.fulfill({json:{ok:true, api_version:'1', data}});
async function setup(t, handler, saved=null, beforeGoto=null) {
const context = await browser.newContext();
t.after(() => context.close());
if (saved) await context.addInitScript(saved => localStorage.setItem('arr:last-download:operator', JSON.stringify(saved)), saved);
if (saved) await context.addInitScript(saved => localStorage.setItem('arr:last-download:v2:synthetic-browser:operator', JSON.stringify(saved)), saved);
const page = await context.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
@@ -107,14 +107,14 @@ test('initial disconnect preserves the saved task and ignores a different latest
queried.push(request.url().split('/').at(-1));
return fulfill(route, interrupted);
}, savedIntent);
await page.waitForFunction(() => document.querySelector('#arr-download-status').textContent.includes('原任务'));
await page.waitForFunction(() => document.querySelector('#arr-download-status').textContent.includes('重新连接'));
assert(await page.locator('#arr-download-date').isEnabled());
await page.clock.runFor(11000);
await page.waitForFunction(() => document.querySelector('#arr-download-status').textContent.includes('任务中断'));
assert.equal(await page.inputValue('#arr-download-date'), savedIntent.report_date);
assert.deepEqual(queried, [savedIntent.request_id]);
assert.equal(writes, 0);
const retained=await page.evaluate(() => JSON.parse(localStorage.getItem('arr:last-download:operator')));
const retained=await page.evaluate(() => JSON.parse(localStorage.getItem('arr:last-download:v2:synthetic-browser:operator')));
assert.deepEqual(retained, savedIntent);
});
@@ -211,3 +211,66 @@ test('online before session initialization cannot consume another storage key or
await page.waitForFunction(() => !document.querySelector('#arr-download-button').disabled);
assert.equal(reads, 1);
});
test('legacy and another instance cannot restore a terminal task into the default date', async t => {
const queried=[];
const old={...interrupted,status:'failed'};
const page=await setup(t, route=>{
queried.push(new URL(route.request().url()).pathname);
return fulfill(route,config({default_date:'2026-10-07',latest_task:old}));
},null,page=>page.addInitScript(old=>{
localStorage.setItem('arr:last-download:operator',JSON.stringify(old));
localStorage.setItem('arr:last-download:v2:sandbox:operator',JSON.stringify(old));
},old));
await page.waitForFunction(()=>!document.querySelector('#arr-download-button').disabled);
assert.equal(await page.inputValue('#arr-download-date'),'2026-10-07');
assert.deepEqual(queried,['/api/arr-downloads']);
});
test('field review saves each explicit value and reconciles a lost finalize response on the same request', async t => {
const task={...savedIntent,report_date:'2026-10-07',status:'needs_data_review',job_id:null,can_retry:false};
const items=[
{item_id:'1:ADULTS',source_sequence:1,confirmation_no:'SYNTHETIC-001',room_no:'SIM-001',company_name:'Synthetic Company',rate_code:'SYNTHETIC',field:'ADULTS',field_label:'成人数',reason_code:'MISSING',can_be_empty:false,value:null,confirmed:false},
{item_id:'1:PRODUCTS',source_sequence:1,confirmation_no:'SYNTHETIC-001',room_no:'SIM-001',company_name:'Synthetic Company',rate_code:'SYNTHETIC',field:'PRODUCTS',field_label:'包价代码',reason_code:'AMBIGUOUS',can_be_empty:true,value:['GUESSED'],confirmed:false},
];
let revision=1, finalized=false;
const writes=[];
const snapshot=()=>({request_id:task.request_id,report_date:task.report_date,status:'editing',revision,items,pending_count:items.filter(item=>!item.confirmed).length,total_count:items.length,can_finalize:items.every(item=>item.confirmed)});
const page=await setup(t,route=>{
const request=route.request(), pathname=new URL(request.url()).pathname;
if(request.method()==='POST') {
const body=request.postDataJSON();
writes.push({path:pathname,body});
assert.equal(body.revision,revision);
if(pathname.endsWith('/finalize')) {finalized=true;return route.abort('failed');}
const id=decodeURIComponent(pathname.split('/').at(-1));
const item=items.find(item=>item.item_id===id);
assert(item,'the item URL must identify the source field');
item.value=body.value;
item.confirmed=true;
revision++;
return fulfill(route,{revision});
}
if(pathname==='/api/arr-downloads') return fulfill(route,config({latest_task:task}));
if(pathname.endsWith('/data-review')) return fulfill(route,snapshot());
return fulfill(route,finalized ? {...task,status:'processing'} : task);
});
await page.locator('#arr-data-review-panel').waitFor({state:'visible'});
assert(await page.locator('#arr-download-data-review').isVisible());
assert(await page.locator('#arr-data-review-finalize').isDisabled());
const adults=page.locator('[data-arr-data-review-item-id="1:ADULTS"]');
await adults.locator('input').fill('0');
await adults.locator('[data-arr-data-review-save]').click();
await page.waitForFunction(()=>document.querySelectorAll('#arr-data-review-body .is-complete').length===1);
assert(await page.locator('#arr-data-review-finalize').isDisabled());
const products=page.locator('[data-arr-data-review-item-id="1:PRODUCTS"]');
assert.equal(await products.locator('textarea').inputValue(),'','ambiguous values must not be guessed');
await products.locator('[data-arr-data-review-empty]').click();
await page.waitForFunction(()=>!document.querySelector('#arr-data-review-finalize').disabled);
await page.locator('#arr-data-review-finalize').click();
await page.waitForFunction(()=>document.querySelector('#arr-download-status').textContent.includes('正在处理并校验'));
assert.equal(writes.length,3);
assert.deepEqual(writes.map(write=>write.body),[{revision:1,value:'0'},{revision:2,value:[]},{revision:3}]);
assert(writes.every(write=>write.path.startsWith(`/api/arr-downloads/${task.request_id}/data-review/`)));
assert(await page.locator('#arr-data-review-panel').isHidden());
});