变更问题修复

This commit is contained in:
andy committed 2026-09-14 16:34:36 +08:00
1 parent 6de4422733
commit 9625110f44
62 files changed
+18116 -95

No files matched your search

+76 -2
View File
@@ -6,6 +6,7 @@ import { createRequire } from 'node:module';
import { validateStandardOperation, validateProgramOperation } from '../LianSyn-platform/external-agent-client.mjs';
import Ajv from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import { loadPageActionRealm } from './page-action-test-realm.mjs';
const plans = createRequire(import.meta.url)('../chrome-extension/ltjt-order-assistant/operation-plans.js');
const ajv = new Ajv({ strict: false }); addFormats(ajv);
const parsedSchema = ajv.compile(JSON.parse(readFileSync(new URL('../schemas/agent_operation.schema.json', import.meta.url))));
@@ -18,7 +19,7 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l
assistant.testOperationBlockers = operationBlockers; assistant.testExpected = arrangementExpectedFields;
assistant.testResource = arrangementResourceProof; assistant.testNative = applyNativeSettlementSelection;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture(type, { index = type === 'guide' ? 0 : 1, duplicate = false, zeroQuantity = false, onSelection = () => {}, rows } = {}) {
function fixture(type, { index = type === 'guide' ? 0 : 1, duplicate = false, zeroQuantity = false, onSelection = () => {}, rows, omitDateMatcher = false } = {}) {
let now = 10000, selections = 0;
const fields = new Map(), form = { elements: [], id: 'InfoForm1', isConnected: true };
const document = { querySelector: () => form, querySelectorAll: () => [], readyState: 'complete' };
@@ -56,7 +57,7 @@ function fixture(type, { index = type === 'guide' ? 0 : 1, duplicate = false, ze
const sandbox = { window, document, Event: window.Event, URL, URLSearchParams, TextEncoder,
Date: class extends Date { static now() { return now; } }, setTimeout(cb,ms) { now += ms; cb(); } };
window.setTimeout = sandbox.setTimeout;
vm.runInNewContext(inpage,sandbox); vm.runInNewContext(adapter,sandbox);
loadPageActionRealm(window, sandbox, { 'inpage.js': inpage, 'lifecycle-adapters.js': adapter }, omitDateMatcher ? ['operation-plans.js'] : []);
const changes = { [type === 'guide' ? 'resource' : 'supplier']: { name: '测试新资源', keyword: '测试新资源' }, ...(['vehicle','transport'].includes(type) ? { quantity: 2 } : {}) };
const selection = type === 'guide' ? undefined : type === 'vehicle' ? { start_date: '2026-09-15', end_date: '2026-09-16' } : { date: '2026-09-15' };
const input = { action: `arrangement_${type}`, order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { identifier: 'LW-TEST-UPDATE', kind: 'shared_plan' }, arrangement: { mode: 'update', ...(selection ? { selection } : {}), changes } } };
@@ -66,6 +67,20 @@ function fixture(type, { index = type === 'guide' ? 0 : 1, duplicate = false, ze
return { api, form, fields, input, selections: () => selections };
}
const types = ['guide','vehicle','transport','other'];
test('dated updates report missing MAIN-world dependency without changing native fields; guide stays independent', () => {
for (const type of types) {
const f = fixture(type, { omitDateMatcher: true });
const before = [...f.fields].map(([key, control]) => [key, control.value]);
const result = f.api.testResolve(f.form, f.input);
if (type === 'guide') assert.equal(result.ok, true, JSON.stringify(result));
else {
assert.equal(result.ok, false);
assert.deepEqual(Array.from(result.blockers), ['arrangement_date_matcher_unavailable']);
}
assert.equal(f.selections(), 0);
assert.deepEqual([...f.fields].map(([key, control]) => [key, control.value]), before);
}
});
for (const type of types) test(`${type} update resolves the original record and preserves dates and unspecified fields`, async () => {
for (const index of type === 'guide' ? [0] : [0,1,10]) {
const f = fixture(type,{index});
@@ -395,3 +410,62 @@ test('native transport selection rechecks a filter even after preflight and neve
assert.deepEqual([...f.fields.values()].map(c => c.value), before);
}
});
for (const type of ['vehicle','transport','other']) test(`${type} month/day dates pass parsed contracts and resolve to a full-year execution snapshot`, () => {
const f = fixture(type);
f.input.data.arrangement.selection = type === 'vehicle' ? { start_date:'09-15',end_date:'09-16' } : {date:'09-15', ...(type === 'other' ? {filing_date:'09-14'} : {})};
assert.equal(parsedSchema(f.input), true, JSON.stringify(parsedSchema.errors));
assert.deepEqual(validateProgramOperation(f.input), []);
assert.equal(plans.validateDispatchOperation(f.input).blockers.length, 0);
const resolved = f.api.testResolve(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify(resolved));
assert.equal(executionSchema(resolved.operation), true, JSON.stringify(executionSchema.errors));
assert.equal(resolved.operation.data.arrangement[type === 'vehicle' ? 'start_date' : 'date'], '2026-09-15');
assert.equal(resolved.operation.data.arrangement.selection, undefined);
f.fields.get('riqi0').value = '2027-9-15'; f.fields.get('riqis0').value = '2027-9-16';
assert.equal(f.api.testResolve(f.form, f.input).ok, false, 'two years are ambiguous');
f.input.data.arrangement.selection[type === 'vehicle' ? 'start_date' : 'date'] = '2026-09-15';
assert.equal(f.api.testResolve(f.form, f.input).ok, true);
if (type === 'other') {
f.input.data.arrangement.selection.filing_date = '2027-09-14';
assert.equal(f.api.testResolve(f.form, f.input).ok, false, 'explicit filing year must match');
}
});
test('parsed date-selector validators agree on invalid calendars while execution still requires a full year', () => {
for (const invalid of ['02-30','13-01','2026-02-29','9-15','bad']) {
const f = fixture('transport'); f.input.data.arrangement.selection.date = invalid;
assert.equal(parsedSchema(f.input), false, invalid);
assert.ok(validateProgramOperation(f.input).length, invalid);
assert.ok(plans.validateDispatchOperation(f.input).blockers.length, invalid);
}
assert.equal(plans.matchesOriginalArrangementDate('2028-02-29', '02-29'), true);
assert.equal(plans.matchesOriginalArrangementDate('2027-02-29', '02-29'), false);
const f = fixture('transport'); const resolved = f.api.testResolve(f.form, f.input).operation;
resolved.data.arrangement.date = '09-15';
assert.equal(executionSchema(resolved), false);
assert.ok(validateStandardOperation(resolved).length);
});
test('ambiguous original dates are surfaced in the route failure message without native ids', () => {
const source = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
const start = source.indexOf('function lifecycleRouteFailure(');
const end = source.indexOf('\nasync function executeLifecycleOperation', start);
const failure = vm.runInNewContext(`(${source.slice(start,end).trim()})`);
const unavailable = failure({action:'arrangement_hotel'}, {blockers:['arrangement_date_matcher_unavailable']});
assert.equal(unavailable.errorCode, 'arrangement_date_matcher_unavailable');
assert.match(unavailable.message, /插件日期匹配组件未加载/);
assert.doesNotMatch(unavailable.message, /没有找到|核对原安排/);
const absent = failure({action:'arrangement_hotel'}, {blockers:['arrangement_update_target_candidate_count:0']});
assert.equal(absent.errorCode, 'arrangement_update_target_not_unique');
assert.match(absent.message, /没有找到/);
const result = failure({action:'arrangement_hotel'}, {blockers:['arrangement_update_target_candidate_count:2'], preflight:{resource_resolution:{candidates:[
{date:'2026-09-04',end_date:'2026-09-05',resource:'原酒店',item:'TWN'},
{date:'2027-09-04',end_date:'2027-09-05',resource:'原酒店',item:'TWN'}
]}}});
assert.match(result.message,/2026-09-04 至 2026-09-05/);
assert.match(result.message,/2027-09-04 至 2027-09-05/);
assert.match(result.message,/完整年月日重新提交/);
assert.match(result.message,/未写入 ERP/);
assert.match(inpage,/resource_resolution: arrangementResolution.evidence/);
});
+73 -4
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import { loadPageActionRealm } from './page-action-test-realm.mjs';
const inpage = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8')
.replace('inspectArrangementLookupReadiness(form,', 'testResolveHotel: resolveArrangementExecutionOperation,\n inspectArrangementLookupReadiness(form,');
@@ -14,7 +14,7 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l
assistant.testResource = arrangementResourceProof;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture({ duplicate = false, slotIndex = 1, onSelection = () => {} } = {}) {
function fixture({ duplicate = false, slotIndex = 1, onSelection = () => {}, omitDateMatcher = false } = {}) {
let now = 10000;
let nativeSelections = 0;
const form = { elements: [], id: 'InfoForm1' };
@@ -60,8 +60,7 @@ function fixture({ duplicate = false, slotIndex = 1, onSelection = () => {} } =
setTimeout(callback, ms) { now += ms; callback(); }
};
window.setTimeout = sandbox.setTimeout;
vm.runInNewContext(inpage, sandbox);
vm.runInNewContext(adapter, sandbox);
loadPageActionRealm(window, sandbox, { 'inpage.js': inpage, 'lifecycle-adapters.js': adapter }, omitDateMatcher ? ['operation-plans.js'] : []);
const api = window.LTJTOrderAssistant;
const input = {
action: 'arrangement_hotel', data: {
@@ -106,6 +105,34 @@ test('hotel replacement resolves a dated existing row and uses native linked sel
}
});
test('fresh ERP MAIN world resolves the reported 2027 hotel update using the actual injection list', async () => {
const f = fixture();
f.fields.get('riqi1').value = '2027-9-12';
f.fields.get('riqis1').value = '2027-9-13';
f.input.data.existing_refs.identifier = 'LLW-270911A-A';
f.input.data.arrangement.selection = { start_date: '09-12', end_date: '09-13' };
f.input.data.arrangement.changes.room_count = 10;
const resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify(resolved));
assert.equal(resolved.operation.data.arrangement.start_date, '2027-09-12');
assert.equal(resolved.operation.data.arrangement.end_date, '2027-09-13');
const result = await f.api.testApply(f.form, resolved.operation);
assert.equal(result.blockers.length, 0, JSON.stringify(result));
assert.equal(f.fields.get('jianshu1').value, '10');
assert.equal(f.fields.get('riqi1').value, '2027-9-12');
assert.equal(f.fields.get('riqis1').value, '2027-9-13');
});
test('missing page date dependency is diagnosed before matching and never reported as zero ERP records', () => {
const f = fixture({ omitDateMatcher: true });
const before = [...f.fields].map(([key, control]) => [key, control.value]);
const resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, false);
assert.deepEqual(Array.from(resolved.blockers), ['arrangement_date_matcher_unavailable']);
assert.equal(f.nativeSelections(), 0);
assert.deepEqual([...f.fields].map(([key, control]) => [key, control.value]), before);
});
test('hotel replacement stops on ambiguous or missing dated rows before selecting a hotel', () => {
const duplicate = fixture({ duplicate: true });
const result = duplicate.api.testResolveHotel(duplicate.form, duplicate.input);
@@ -135,3 +162,45 @@ test('hotel replacement rejects native callbacks that change another row or chan
assert.ok(result.blockers.length, field);
}
});
test('month/day hotel selector freezes the unique actual ERP year and preserves native dates on replacement', async () => {
for (const year of [2026, 2027]) {
const f = fixture();
f.fields.get('riqi1').value = `${year}-9-4`;
f.fields.get('riqis1').value = `${year}-9-5`;
f.input.data.arrangement.selection = { start_date: '09-04', end_date: '09-05' };
const resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify(resolved));
assert.equal(resolved.operation.data.arrangement.start_date, `${year}-09-04`);
assert.equal(resolved.operation.data.arrangement.end_date, `${year}-09-05`);
assert.equal(resolved.operation.data.arrangement.selection, undefined);
const applied = await f.api.testApply(f.form, resolved.operation);
assert.equal(applied.blockers.length, 0, JSON.stringify(applied));
assert.equal(f.fields.get('riqi1').value, `${year}-9-4`);
assert.equal(f.fields.get('riqis1').value, `${year}-9-5`);
}
});
test('cross-year duplicate month/day rows remain ambiguous and report original dates before any mutation', () => {
const f = fixture();
for (const [index, year] of [[0,2026],[1,2027]]) {
f.fields.get(`riqi${index}`).value = `${year}-12-31`;
f.fields.get(`riqis${index}`).value = `${year+1}-1-2`;
}
f.input.data.arrangement.selection = { start_date: '12-31', end_date: '01-02' };
let resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, false);
assert.match(resolved.blockers.join(), /target_candidate_count:2/);
assert.equal(resolved.evidence.candidates[0].date, '2026-12-31');
assert.equal(resolved.evidence.candidates[1].end_date, '2028-01-02');
assert.equal(f.nativeSelections(), 0);
f.input.data.arrangement.selection.start_date = '2027-12-31';
resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, true);
assert.equal(resolved.operation.data.arrangement.target.slot_index, 1);
f.input.data.arrangement.selection.end_date = '2027-01-02';
assert.equal(f.api.testResolveHotel(f.form, f.input).ok, false, 'explicit wrong year must not fall back to month/day');
f.input.data.arrangement.selection = { start_date: '02-30', end_date: '03-01' };
f.fields.get('riqi1').value = '2026-02-30';
assert.equal(f.api.testResolveHotel(f.form, f.input).ok, false, 'invalid actual dates never match');
});
+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\.180'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.182'/);
});
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.180 keeps reload dormant unless its durable execution state is fully idle', async () => {
test('extension 0.5.182 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.180');
assert.equal(mapping.current_extension_version, '0.5.182');
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.180');
assert.match(inpage, /version: '0\.5\.180'/);
assert.match(teamBatchInpage, /version: '0\.5\.180'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.180'/);
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.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);
+27
View File
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
const extension = new URL('../chrome-extension/ltjt-order-assistant/', import.meta.url);
const background = readFileSync(new URL('background.js', extension), 'utf8');
const lists = background.slice(background.indexOf('const DEFAULT_PAGE_ACTION_FILES ='), background.indexOf('let storageMutationQueue ='));
const selector = background.slice(background.indexOf('function pageActionFiles('), background.indexOf('function queueOperationTimingDetails('));
// The service worker and ERP MAIN world have separate globals. Read the real
// dispatch list so adding a helper to importScripts alone cannot satisfy tests.
export function loadPageActionRealm(window, globals, overrides = {}, omitFiles = []) {
const worker = vm.createContext({});
vm.runInContext(`${lists}\n${selector}`, worker);
const files = vm.runInContext("pageActionFiles('prepareLifecycleOperation')", worker);
assert.ok(files.includes('inpage.js') && files.includes('lifecycle-adapters.js'));
const realm = Object.assign(window, globals);
realm.window = realm;
realm.self = realm;
const context = vm.createContext(realm);
assert.equal(realm.LTJTOperationPlans, undefined, 'a fresh page must not inherit worker exports');
for (const file of files) {
if (omitFiles.includes(file)) continue;
vm.runInContext(overrides[file] ?? readFileSync(new URL(file, extension), 'utf8'), context, { filename: file });
}
return realm;
}
+1 -1
View File
@@ -304,7 +304,7 @@ test('single-source and generated-output boundaries remain explicit', async () =
const diagnoseServer = await readFile(path.join(ROOT, 'infra/diagnose-server.sh'), 'utf8');
const gitignore = await readFile(path.join(ROOT, '.gitignore'), 'utf8');
assert.match(template, /^交付版本:0\.5\.132\s*$/mu);
assert.match(template, /^交付版本:0\.5\.133\s*$/mu);
assert.match(template, /^适用范围:当前 23 条业务路由,按 19 项运营场景分组;执行能力以生命周期发布门槛为准。\s*$/mu);
assert.match(template, /^### 5\. 散拼团多个新增子单\s*$/mu);
assert.doesNotMatch(template, /^事实基线:/mu);