修复一个偶发的问题
This commit is contained in:
1 parent
8d55e930a2
commit
0282c30a2a
20 files changed
+440
-39
No files matched your search
@@ -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/);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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'\)\)/);
|
||||
|
||||
Reference in new issue
Block a user