变更问题修复
This commit is contained in:
1 parent
6de4422733
commit
9625110f44
62 files changed
+18116
-95
No files matched your search
@@ -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/);
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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'\)\)/);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user