修复一个偶发的问题

This commit is contained in:
andy committed 2026-09-14 18:24:25 +08:00
1 parent 8d55e930a2
commit 0282c30a2a
20 files changed
+440 -39

No files matched your search

+16 -2
View File
@@ -15,10 +15,11 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l
teamSummaryRequery = async (operation, subpage) => subpage;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {} } = {}) {
function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [] } = {}) {
let now = 10000;
let posts = 0;
let selections = 0;
let linkageTicks = 0;
let savedForm;
const form = { elements: [], id: 'InfoForm1', isConnected: true };
const fields = new Map();
@@ -73,7 +74,10 @@ function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () =>
return savedForm.elements.find((control) => control.name === name) || null;
} }, text: 'LW-TEST-HOTEL', response: { ok: true, status: 200 } });
const sandbox = { window, document, location, Event: window.Event, URL, URLSearchParams, TextEncoder,
Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) { now += ms; callback(); } };
Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) {
now += selections ? linkageDelays[linkageTicks++] ?? ms : ms;
callback();
} };
window.setTimeout = sandbox.setTimeout;
vm.runInNewContext(inpage, sandbox);
vm.runInNewContext(adapter, sandbox);
@@ -86,6 +90,16 @@ function fixture(type, { occupied = 1, rows = occupied + 2, onSelection = () =>
}
for (const type of ['vehicle', 'transport', 'other']) {
test(`${type} create recovers from a delayed linkage sample but never saves while sampling stays delayed`, async () => {
for (const [linkageDelays, completed] of [[[6000], true], [Array(6).fill(6000), false]]) {
const f = fixture(type, { linkageDelays });
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, completed ? 'lifecycle_completed' : 'lifecycle_apply_blocked', JSON.stringify(result));
assert.equal(f.posts(), completed ? 1 : 0); assert.equal(f.selections(), 1);
assert.equal(result.preflight.arrangement_linkage.reason, completed ? 'stable' : 'sampling_delayed');
}
});
test(`${type} create preserves old rows and binds native selection, dates and quantity to empty rows 0/1/10`, async () => {
for (const occupied of [0, 1, 10]) {
const f = fixture(type, { occupied });
@@ -0,0 +1,112 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
const source = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8')
.replace('assistant.preflightLifecycleOperation = preflightLifecycleOperation;',
'assistant.testWaitForStable = waitForStable; assistant.preflightLifecycleOperation = preflightLifecycleOperation;');
const background = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
function fixture({ delays = [], onTick = () => {} } = {}) {
let now = 0;
let ticks = 0;
const control = { name: 'jine1', id: 'jine1', type: 'text', value: 'private-value', isConnected: true };
const document = { querySelectorAll: () => [], readyState: 'complete' };
const window = { document, LTJTOrderAssistant: {}, jQuery: { active: 0 }, performance: { now: () => now },
setTimeout(callback, ms) { now += delays[ticks] ?? ms; onTick({ now, ticks: ++ticks, control, form, window }); callback(); } };
window.parent = window; document.defaultView = window;
const form = { elements: [control], isConnected: true, ownerDocument: document };
vm.runInNewContext(source, { window, Date: class extends Date { static now() { return now; } }, URL, URLSearchParams });
const diagnostic = {};
return { control, form, window, diagnostic, now: () => now,
run: () => window.LTJTOrderAssistant.testWaitForStable(form, ['jine1', 'optional1'], 6000, diagnostic) };
}
test('ordinary linkage requires multiple observations over 1.5 seconds and redacts values', async () => {
const f = fixture();
assert.equal(await f.run(), true);
assert.equal(f.now(), 1500);
assert.ok(f.diagnostic.sample_count >= 3);
assert.equal(f.diagnostic.ready, true);
assert.equal(f.diagnostic.reason, 'stable');
assert.equal(JSON.stringify(f.diagnostic).includes('private-value'), false);
});
test('one delayed callback restarts observation instead of failing or counting the blind interval', async () => {
const f = fixture({ delays: [6000] });
assert.equal(await f.run(), true);
assert.equal(f.now(), 7500);
assert.equal(f.diagnostic.delayed_sample_count, 1);
assert.equal(f.diagnostic.max_sample_gap_ms, 6000);
assert.equal(f.diagnostic.stable_ms, 1500);
});
test('one-second sampling still proves stability; persistent long gaps fail boundedly', async () => {
const slower = fixture({ delays: [1000, 1000] });
assert.equal(await slower.run(), true);
assert.equal(slower.now(), 2000);
const paused = fixture({ delays: Array(6).fill(6000) });
assert.equal(await paused.run(), false);
assert.equal(paused.now(), 30000);
assert.equal(paused.diagnostic.reason, 'sampling_delayed');
assert.equal(paused.diagnostic.stable_ms, 0);
});
test('continuous value changes fail and report field names rather than their values', async () => {
const f = fixture({ onTick: ({ control, ticks }) => { control.value = `secret-${ticks}`; } });
assert.equal(await f.run(), false);
assert.equal(f.now(), 6000);
assert.equal(f.diagnostic.reason, 'fields_changing');
assert.deepEqual([...f.diagnostic.changed_fields], ['jine1']);
assert.equal(JSON.stringify(f.diagnostic).includes('secret-'), false);
});
test('the last allowed sample is checked; a late change restarts stability', async () => {
const f = fixture({ onTick: ({ now, control }) => { if (now <= 4500) control.value = String(now); } });
assert.equal(await f.run(), true);
assert.equal(f.now(), 6000);
const late = fixture({ delays: [6000], onTick: ({ now, control }) => { if (now === 7250) control.value = 'late'; } });
assert.equal(await late.run(), true);
assert.equal(late.now(), 8750);
});
test('detached form, missing/replaced controls and pending native Ajax cannot pass', async () => {
for (const mutate of [
({ form }) => { form.isConnected = false; },
({ form }) => { form.elements = []; },
({ form, control }) => { form.elements = [{ ...control }]; },
({ control }) => { control.isConnected = false; }
]) {
const f = fixture({ onTick: mutate });
assert.equal(await f.run(), false);
assert.match(f.diagnostic.reason, /form_detached|control_unavailable/);
}
const busy = fixture(); busy.window.jQuery.active = 1;
assert.equal(await busy.run(), false);
assert.equal(busy.diagnostic.reason, 'native_busy');
const finishes = fixture({ onTick: ({ now, window }) => { if (now >= 1000) window.jQuery.active = 0; } });
finishes.window.jQuery.active = 1;
assert.equal(await finishes.run(), true);
assert.equal(finishes.now(), 2500);
const late = fixture({ onTick: ({ now, window }) => { if (now >= 5500) window.jQuery.active = 0; } });
late.window.jQuery.active = 1;
assert.equal(await late.run(), false);
assert.equal(late.diagnostic.reason, 'native_busy', 'late Ajax completion is not evidence of oscillating field values');
assert.equal(late.diagnostic.native_busy, false);
assert.ok(late.diagnostic.native_busy_sample_count > 0);
});
test('background reports the observed failure reason without classifying all failures as changing fields', () => {
const start = background.indexOf('function arrangementLinkageFailureMessage(');
const end = background.indexOf('\nfunction lifecycleLiveBusinessFailure(', start);
const message = vm.runInNewContext(`${background.slice(start, end)}; arrangementLinkageFailureMessage;`);
for (const [reason, expected] of [['sampling_delayed', '检测持续延迟'], ['fields_changing', '联动字段在观察期间未稳定'],
['native_busy', '联动请求'], ['form_detached', '关闭或刷新'], ['control_unavailable', '控件']]) {
const result = message({ blockers: ['arrangement_create_linked_fields_not_stable'], preflight: { arrangement_linkage: { reason } } });
assert.ok(result.includes(expected)); assert.ok(result.includes('未提交 ERP 保存'));
}
assert.equal(message({ blockers: ['other_error'] }), '');
assert.equal(message({ blockers: ['arrangement_create_linked_fields_not_stable'] }), '');
assert.match(background, /failure_message: linkageFailureMessage/);
});
+36 -2
View File
@@ -15,10 +15,11 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l
teamSummaryRequery = async (operation, subpage) => subpage;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {} } = {}) {
function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [], onLinkageTick = () => {} } = {}) {
let now = 10000;
let posts = 0;
let selections = 0;
let linkageTicks = 0;
let savedForm;
const form = { elements: [], id: 'InfoForm1', isConnected: true };
const fields = new Map();
@@ -69,7 +70,11 @@ function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, on
return savedForm.elements.find((control) => control.name === name) || null;
} }, text: 'LW-TEST-HOTEL', response: { ok: true, status: 200 } });
const sandbox = { window, document, location, Event: window.Event, URL, URLSearchParams, TextEncoder,
Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) { now += ms; callback(); } };
Date: class extends Date { static now() { return now; } }, setTimeout(callback, ms) {
now += selections ? linkageDelays[linkageTicks] ?? ms : ms;
if (selections) onLinkageTick(fields, ++linkageTicks);
callback();
} };
window.setTimeout = sandbox.setTimeout;
vm.runInNewContext(inpage, sandbox);
vm.runInNewContext(adapter, sandbox);
@@ -101,6 +106,35 @@ test('hotel create binds the first empty row through preflight, native linkage,
}
});
test('6/12-room hotel create recovers after delayed sampling, with only one native selection and POST', async () => {
for (const roomCount of [6, 12]) for (const linkageDelays of [[], [6000]]) {
const f = fixture({ linkageDelays });
f.operation.data.arrangement.room_count = roomCount;
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, 'lifecycle_completed', JSON.stringify(result));
assert.equal(f.fields.get('jianshu1').value, String(roomCount));
assert.equal(f.posts(), 1); assert.equal(f.selections(), 1);
assert.equal(result.preflight.arrangement_linkage.elapsed_ms, linkageDelays.length ? 7500 : 1500);
assert.equal(result.preflight.arrangement_linkage.ready, true);
}
});
test('unstable or persistently delayed hotel linkage records diagnostics and never saves', async () => {
const changing = fixture({ onLinkageTick(fields, tick) { fields.get('jine1').value = String(tick); } });
const paused = fixture({ linkageDelays: Array(6).fill(6000) });
for (const [f, reason] of [[changing, 'fields_changing'], [paused, 'sampling_delayed']]) {
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, 'lifecycle_apply_blocked', JSON.stringify(result));
assert.equal(result.write_attempted, false); assert.equal(result.no_erp_write, true);
assert.equal(f.posts(), 0); assert.equal(f.selections(), 1);
assert.equal(f.fields.get('zt1').value, '', 'do not continue filling after failed observation');
assert.equal(result.preflight.arrangement_linkage.reason, reason);
assert.ok(result.blockers.includes('arrangement_create_linked_fields_not_stable'));
assert.ok(result.blockers.some(value => value.startsWith(`arrangement_linkage_wait:${reason}:`)));
assert.equal(JSON.stringify(result.preflight.arrangement_linkage).includes('测试新酒店'), false);
}
});
test('hotel create matches the new record after ERP sorts rows and uses persisted evidence on read-only reconciliation', async () => {
const f = fixture({ sort: true });
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
+7 -7
View File
@@ -1374,7 +1374,7 @@ test('passenger explicit server success is terminal without a post-save row requ
assert.doesNotMatch(passengerBranch, /passengerRequery|verifyLifecycleOperation/);
assert.doesNotMatch(background, /attemptAutomaticPassengerReconciliation/);
assert.match(background, /名单已取得 ERP 明确成功响应,按业务规则确认录入成功/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.182'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.183'/);
});
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
@@ -1584,7 +1584,7 @@ test('lifecycle execution keeps the MV3 worker alive until the durable task sett
assert.match(background, /arrangement_resource_candidate_data_missing/);
});
test('extension 0.5.182 keeps reload dormant unless its durable execution state is fully idle', async () => {
test('extension 0.5.183 keeps reload dormant unless its durable execution state is fully idle', async () => {
const [background, bridge] = await Promise.all([
readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'),
readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8')
@@ -2035,7 +2035,7 @@ test('delete guard distinguishes independent, shared child, and shared parent ro
for (const operation of operations) assert.equal(plans.validateOperation(operation).ok, true, plans.validateOperation(operation).blockers.join('; '));
const mapping = JSON.parse(await readFile(new URL('../mappings/lifecycle.mapping.json', import.meta.url), 'utf8'));
assert.equal(mapping.current_extension_version, '0.5.182');
assert.equal(mapping.current_extension_version, '0.5.183');
assert.equal(
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
'ertrenshu'
@@ -2419,10 +2419,10 @@ test('schema and browser adapters contain the v2 safety fields and no confirm ov
assert.match(inpage, /`ys_danweiid\$\{index\}`, resolvedCustomerId/);
assert.doesNotMatch(inpage, /product_customer_source_region|sourceRegionCheck|source_reference/);
assert.doesNotMatch(teamBatchInpage, /product_customer_source_region|sourceRegionCheck/);
assert.equal(extensionManifest.version, '0.5.182');
assert.match(inpage, /version: '0\.5\.182'/);
assert.match(teamBatchInpage, /version: '0\.5\.182'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.182'/);
assert.equal(extensionManifest.version, '0.5.183');
assert.match(inpage, /version: '0\.5\.183'/);
assert.match(teamBatchInpage, /version: '0\.5\.183'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.183'/);
assert.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);