修复bug
This commit is contained in:
1 parent
c65f00c941
commit
98282d127c
33 files changed
+1104
-167
No files matched your search
@@ -25,6 +25,19 @@ function loadPopupBusinessUrlMatcher(popupSource) {
|
||||
return context.isBusinessSystemUrl;
|
||||
}
|
||||
|
||||
function loadKeepaliveStateNormalizer(backgroundSource) {
|
||||
const start = backgroundSource.indexOf('function normalizeErpKeepaliveState');
|
||||
const end = backgroundSource.indexOf('async function readErpKeepaliveState', start);
|
||||
assert.ok(start >= 0 && end > start, 'background must expose the keepalive state normalizer');
|
||||
const context = {};
|
||||
vm.runInNewContext(
|
||||
`${backgroundSource.slice(start, end)}\nthis.normalizeErpKeepaliveState = normalizeErpKeepaliveState;`,
|
||||
context,
|
||||
{ filename: 'background-keepalive-state.js' }
|
||||
);
|
||||
return context.normalizeErpKeepaliveState;
|
||||
}
|
||||
|
||||
test('production control-plane origin is recognized and bridge injection is permitted', async () => {
|
||||
const [manifestText, background, popup] = await Promise.all([
|
||||
read('chrome-extension/ltjt-order-assistant/manifest.json'),
|
||||
@@ -105,6 +118,34 @@ test('ERP exception recovery refreshes the existing ERP tab once, then performs
|
||||
assert.doesNotMatch(recoveryBody, /captureAjaxSubmit|SubmitInfoForm|DoInfo/);
|
||||
});
|
||||
|
||||
test('an interrupted service worker recovery cannot leave ERP dispatch permanently blocked', async () => {
|
||||
const background = await read('chrome-extension/ltjt-order-assistant/background.js');
|
||||
const normalizeErpKeepaliveState = loadKeepaliveStateNormalizer(background);
|
||||
const persistedRunning = {
|
||||
last_status: 'running',
|
||||
recovery_status: 'running',
|
||||
recovery_attempt_at: '2026-08-29T12:00:00.000Z',
|
||||
recovery_completed_at: '',
|
||||
recovery_error: ''
|
||||
};
|
||||
|
||||
const active = normalizeErpKeepaliveState(persistedRunning, true);
|
||||
assert.equal(active.last_status, 'running');
|
||||
assert.equal(active.recovery_status, 'running');
|
||||
|
||||
const interrupted = normalizeErpKeepaliveState(persistedRunning, false);
|
||||
assert.equal(interrupted.last_status, 'interrupted');
|
||||
assert.equal(interrupted.recovery_status, 'interrupted');
|
||||
assert.equal(interrupted.recovery_error, 'erp_link_recovery_interrupted');
|
||||
|
||||
const keepaliveBody = background.slice(
|
||||
background.indexOf('async function runErpSessionKeepalive'),
|
||||
background.indexOf('async function ensureErpSessionKeepaliveAlarm')
|
||||
);
|
||||
assert.match(keepaliveBody, /success && !erpLinkRecoveryInFlight/);
|
||||
assert.match(keepaliveBody, /recovery_status: 'idle'/);
|
||||
});
|
||||
|
||||
test('keepalive setting and last probe state are visible without changing the business operation contract', async () => {
|
||||
const [background, bridge, popup, popupHtml] = await Promise.all([
|
||||
read('chrome-extension/ltjt-order-assistant/background.js'),
|
||||
|
||||
@@ -147,6 +147,166 @@ test('team-order customer restore accepts exact final values and rejects any pro
|
||||
});
|
||||
});
|
||||
|
||||
test('team-order protected-field drift matches the 0.5.156 core-field scope', async () => {
|
||||
const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
const sandbox = { window: {}, URLSearchParams };
|
||||
vm.runInNewContext(inpage, sandbox);
|
||||
const inspect = sandbox.window.LTJTOrderAssistant.inspectProtectedSubmitFieldChanges;
|
||||
const approved = 'chufa_ri=2026-11-08&darenshu=15&ys_danwei1=customer-a&ys_danweiid1=101&ys_jine0=1500&xiadanbeizhu=approved-note';
|
||||
const current = 'chufa_ri=2026-11-8&darenshu=16&ys_danwei1=customer-b&ys_danweiid1=202&ys_jine0=1600&xiadanbeizhu=current-note';
|
||||
|
||||
assert.deepEqual(Array.from(inspect(approved, current)), ['darenshu']);
|
||||
assert.match(inpage, /__ltjt_approved_form_protected_projection = postInterceptProtectedProjection/);
|
||||
});
|
||||
|
||||
test('team-order live prewrite blocker exposes only changed protected field names', async () => {
|
||||
const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
const inpage = source.replace(
|
||||
'inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,',
|
||||
'inspectProtectedSubmitProjection: protectedSubmitProjection,\n inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,'
|
||||
);
|
||||
let serialized = 'chufa_ri=2026-11-8&darenshu=16&quanrenshu=1&frenshu1=8';
|
||||
let nativeAjaxCalls = 0;
|
||||
const form = { elements: { length: 822 } };
|
||||
const jq = () => ({ serialize: () => serialized });
|
||||
jq.ajax = () => { nativeAjaxCalls += 1; };
|
||||
jq.ajax.__ltjt_preflight_guard = true;
|
||||
jq.ajax.__ltjt_native_ajax = () => { nativeAjaxCalls += 1; };
|
||||
jq.ajax.__ltjt_intercepted_submits = [{}];
|
||||
const pageWindow = {
|
||||
jQuery: jq,
|
||||
$: jq,
|
||||
SubmitInfoForm() {},
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
location: { href: 'https://lwlt.hisy.cc/System/Business/orders_add.asp' }
|
||||
};
|
||||
pageWindow.window = pageWindow;
|
||||
const sandbox = {
|
||||
window: pageWindow,
|
||||
document: { querySelector: (selector) => selector === '#ListForm' ? form : null },
|
||||
location: pageWindow.location,
|
||||
crypto: globalThis.crypto,
|
||||
TextEncoder,
|
||||
URLSearchParams,
|
||||
Blob,
|
||||
FormData,
|
||||
performance,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
console
|
||||
};
|
||||
vm.runInNewContext(inpage, sandbox);
|
||||
const assistant = pageWindow.LTJTOrderAssistant;
|
||||
const approved = 'chufa_ri=2026-11-8&darenshu=15&quanrenshu=1&frenshu1=8';
|
||||
const approvedProjection = assistant.inspectProtectedSubmitProjection(approved);
|
||||
const approvedHashBytes = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(approvedProjection));
|
||||
const approvedHash = Array.from(new Uint8Array(approvedHashBytes), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
jq.ajax.__ltjt_approved_form_protected_projection = approvedProjection;
|
||||
|
||||
const result = await assistant.liveSubmitApproved({
|
||||
expectedPayloadSha256: 'approved-payload',
|
||||
expectedFormProtectedFieldsSha256: approvedHash,
|
||||
expectedRequestProtectedFieldsSha256: approvedHash,
|
||||
expectedFormFieldCount: 4,
|
||||
expectedRequestFieldCount: 4
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'live_submit_blocked');
|
||||
assert.equal(result.write_attempted, false);
|
||||
assert.equal(result.no_erp_write, true);
|
||||
assert.equal(nativeAjaxCalls, 0);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(result.protected_field_drift)), {
|
||||
baseline_available: true,
|
||||
baseline_matches_approved_hash: true,
|
||||
changed_field_count: 1,
|
||||
changed_field_names: ['darenshu'],
|
||||
values_redacted: true
|
||||
});
|
||||
assert.deepEqual(Array.from(result.blockers), [
|
||||
'current protected business fields do not match the approved preflight (changed fields: darenshu)'
|
||||
]);
|
||||
});
|
||||
|
||||
test('team-order live gate allows numbered receivable drift like 0.5.156', async () => {
|
||||
const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
const inpage = source.replace(
|
||||
'inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,',
|
||||
'inspectProtectedSubmitProjection: protectedSubmitProjection,\n inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,'
|
||||
);
|
||||
const approved = 'chufa_ri=2026-11-8&darenshu=15&quanrenshu=1&frenshu1=8&ys_danwei1=customer-a&ys_danweiid1=101';
|
||||
const current = 'chufa_ri=2026-11-8&darenshu=15&quanrenshu=1&frenshu1=8&ys_danwei1=customer-b&ys_danweiid1=202';
|
||||
let serialized = current;
|
||||
let nativeAjaxCalls = 0;
|
||||
const form = { elements: { length: 822 } };
|
||||
const jq = () => ({ serialize: () => serialized });
|
||||
jq.ajax = () => { throw new Error('preflight guard should be replaced during live submit'); };
|
||||
jq.ajax.__ltjt_preflight_guard = true;
|
||||
jq.ajax.__ltjt_native_ajax = (config) => {
|
||||
nativeAjaxCalls += 1;
|
||||
config.success?.('下单成功\n团号: TEST-ORDER', 'success');
|
||||
config.complete?.({}, 'success');
|
||||
return { readyState: 4, status: 200, abort() {} };
|
||||
};
|
||||
jq.ajax.__ltjt_intercepted_submits = [{}];
|
||||
const pageWindow = {
|
||||
jQuery: jq,
|
||||
$: jq,
|
||||
SubmitInfoForm() {
|
||||
jq.ajax({
|
||||
url: '/System/DAT/orders.asp',
|
||||
type: 'POST',
|
||||
data: `Act=DoInfoJH&${serialized}`
|
||||
});
|
||||
},
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
location: { href: 'https://lwlt.hisy.cc/System/Business/orders_add.asp' },
|
||||
alert() {}
|
||||
};
|
||||
pageWindow.window = pageWindow;
|
||||
const sandbox = {
|
||||
window: pageWindow,
|
||||
document: {
|
||||
title: '计划下单/修改',
|
||||
querySelector: (selector) => selector === '#ListForm' ? form : null,
|
||||
querySelectorAll: () => []
|
||||
},
|
||||
location: pageWindow.location,
|
||||
crypto: globalThis.crypto,
|
||||
TextEncoder,
|
||||
URLSearchParams,
|
||||
Blob,
|
||||
FormData,
|
||||
performance,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
console
|
||||
};
|
||||
vm.runInNewContext(inpage, sandbox);
|
||||
const assistant = pageWindow.LTJTOrderAssistant;
|
||||
const approvedProjection = assistant.inspectProtectedSubmitProjection(approved);
|
||||
const approvedHashBytes = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(approvedProjection));
|
||||
const approvedHash = Array.from(new Uint8Array(approvedHashBytes), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
jq.ajax.__ltjt_approved_form_protected_projection = approvedProjection;
|
||||
|
||||
const result = await assistant.liveSubmitApproved({
|
||||
expectedPayloadSha256: 'approved-payload-before-native-receivable-drift',
|
||||
expectedFormProtectedFieldsSha256: approvedHash,
|
||||
expectedRequestProtectedFieldsSha256: approvedHash,
|
||||
expectedFormFieldCount: 6,
|
||||
expectedRequestFieldCount: 6
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'live_submit_completed');
|
||||
assert.equal(result.write_attempted, true);
|
||||
assert.equal(nativeAjaxCalls, 1);
|
||||
assert.deepEqual(Array.from(result.blockers), []);
|
||||
assert.equal(result.submit_safety.approved_form_protected_fields_sha256_matched, true);
|
||||
assert.equal(result.submit_safety.approved_request_protected_fields_sha256_matched, true);
|
||||
clearTimeout(jq.ajax.__ltjt_restore_timer);
|
||||
});
|
||||
|
||||
test('split-parent post-write verification retries read-only and falls back after any missed response group', async () => {
|
||||
const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
const createBody = inpage.slice(
|
||||
@@ -1136,7 +1296,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\.158'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.163'/);
|
||||
});
|
||||
|
||||
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
|
||||
@@ -1766,7 +1926,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.158');
|
||||
assert.equal(mapping.current_extension_version, '0.5.163');
|
||||
assert.equal(
|
||||
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
|
||||
'ertrenshu'
|
||||
@@ -2148,10 +2308,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.158');
|
||||
assert.match(inpage, /version: '0\.5\.158'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.158'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.158'/);
|
||||
assert.equal(extensionManifest.version, '0.5.163');
|
||||
assert.match(inpage, /version: '0\.5\.163'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.163'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.163'/);
|
||||
assert.match(inpage, /function strictIsoDate\(value\)/);
|
||||
assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/);
|
||||
assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/);
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const timing = require('../chrome-extension/ltjt-order-assistant/operation-timing.js');
|
||||
@@ -180,6 +181,8 @@ test('team-order preflight waits on ERP completion events without page-timer pol
|
||||
assert.match(preflight, /await productEffectCompletion/);
|
||||
assert.match(preflight, /preflight_lookup\.parallel_wall/);
|
||||
assert.match(preflight, /preflight_product_linkage/);
|
||||
assert.match(preflight, /await waitForJQueryAjaxIdle\(jq\)/);
|
||||
assert.match(preflight, /preflight_native_ajax_idle/);
|
||||
assert.match(preflight, /preflight_customer_restore/);
|
||||
assert.match(preflight, /dispatchNativeSideEffect: false/);
|
||||
assert.match(preflight, /customerRestoreReadiness/);
|
||||
@@ -220,6 +223,89 @@ test('team-order frame discovery targets the exact ERP iframe instead of injecti
|
||||
assert.ok(JSON.parse(manifestText).permissions.includes('webNavigation'));
|
||||
});
|
||||
|
||||
test('team-order frame waits for native customer and staff lookup data before preflight', async () => {
|
||||
const background = await readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
|
||||
const start = background.indexOf('function lightweightOrderFrameProbe()');
|
||||
const end = background.indexOf('function lightweightTeamBatchFrameProbe()', start);
|
||||
const probeSource = background.slice(start, end);
|
||||
const runProbe = (readyFields, activeAjaxCount = 0) => {
|
||||
const fields = Object.fromEntries(['zutuanshe', 'gendanren', 'xiaoshouren'].map((name) => [
|
||||
name,
|
||||
{ getAttribute: (attribute) => attribute === 'data' && readyFields.includes(name) ? 'native-options' : '' }
|
||||
]));
|
||||
const form = {
|
||||
elements: { length: 822 },
|
||||
querySelector: (selector) => fields[selector.slice(1)] || null
|
||||
};
|
||||
const sandbox = {
|
||||
performance,
|
||||
document: {
|
||||
title: '计划下单/修改',
|
||||
querySelector: (selector) => selector === '#ListForm' ? form : null
|
||||
},
|
||||
location: { href: 'https://lwlt.hisy.cc/System/Business/orders_add.asp' },
|
||||
window: { frameElement: null, jQuery: { active: activeAjaxCount } },
|
||||
URL
|
||||
};
|
||||
vm.runInNewContext(`${probeSource};globalThis.result=lightweightOrderFrameProbe();`, sandbox);
|
||||
return JSON.parse(JSON.stringify(sandbox.result));
|
||||
};
|
||||
|
||||
const loading = runProbe([]);
|
||||
assert.equal(loading.status, 'order_frame_loading');
|
||||
assert.deepEqual(loading.missing, ['zutuanshe.data', 'gendanren.data', 'xiaoshouren.data']);
|
||||
const nativeAjaxPending = runProbe(['zutuanshe', 'gendanren', 'xiaoshouren'], 2);
|
||||
assert.equal(nativeAjaxPending.status, 'order_frame_loading');
|
||||
assert.deepEqual(nativeAjaxPending.missing, ['jquery.ajax.pending:2']);
|
||||
assert.equal(runProbe(['zutuanshe', 'gendanren', 'xiaoshouren']).status, 'order_frame_ready');
|
||||
});
|
||||
|
||||
test('team-order preflight waits for the native jQuery Ajax completion event', async () => {
|
||||
const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
const inpage = source.replace(
|
||||
'inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,',
|
||||
'inspectWaitForJQueryAjaxIdle: waitForJQueryAjaxIdle,\n inspectProtectedSubmitFieldChanges: protectedSubmitFieldChanges,'
|
||||
);
|
||||
const handlers = new Map();
|
||||
const eventTarget = {
|
||||
one(eventName, handler) {
|
||||
handlers.set(eventName, handler);
|
||||
return this;
|
||||
},
|
||||
off(eventName) {
|
||||
handlers.delete(eventName);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
const jq = () => eventTarget;
|
||||
jq.active = 2;
|
||||
const pageWindow = { setTimeout, clearTimeout };
|
||||
pageWindow.window = pageWindow;
|
||||
vm.runInNewContext(inpage, {
|
||||
window: pageWindow,
|
||||
document: {},
|
||||
performance,
|
||||
setTimeout,
|
||||
clearTimeout
|
||||
});
|
||||
|
||||
const waiting = pageWindow.LTJTOrderAssistant.inspectWaitForJQueryAjaxIdle(jq, 1000);
|
||||
setTimeout(() => {
|
||||
jq.active = 0;
|
||||
const handler = [...handlers.entries()].find(([eventName]) => eventName.startsWith('ajaxStop'))?.[1];
|
||||
handler?.();
|
||||
}, 5);
|
||||
const result = await waiting;
|
||||
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(result)), {
|
||||
status: 'idle',
|
||||
initial_pending_ajax_count: 2,
|
||||
final_pending_ajax_count: 0,
|
||||
duration_ms: result.duration_ms
|
||||
});
|
||||
assert.ok(result.duration_ms >= 0);
|
||||
});
|
||||
|
||||
test('extension result changes push immediately while polling remains as a fallback', async () => {
|
||||
const bridge = await readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8');
|
||||
const app = await readFile(new URL('../LianSyn-platform/app.js', import.meta.url), 'utf8');
|
||||
|
||||
Reference in new issue
Block a user