判断修改
This commit is contained in:
1 parent
e06afc8c33
commit
4194a200c1
65 files changed
+9617
-109
No files matched your search
@@ -133,7 +133,11 @@ const minimumLabels = {
|
||||
修改散拼母团计划收客数: ['出发日期', '目标计划收客数'],
|
||||
追加散拼子单订房说明: ['预订客户', '出发日期', '追加内容'],
|
||||
独立团信息修改: ['预订客户', '出发日期', '目标修改'],
|
||||
安排变更: ['团号', '目标变更'],
|
||||
安排酒店变更: ['团号', '入住日期', '离店日期', '酒店搜索', '间数'],
|
||||
安排导游变更: ['团号', '导游搜索'],
|
||||
安排用车变更: ['团号', '结算单位搜索', '用车日期', '数量'],
|
||||
安排大交通变更: ['团号', '结算单位搜索', '交通日期', '数量'],
|
||||
'安排其他/备案变更': ['团号', '结算单位搜索', '业务日期'],
|
||||
取消订单: ['预订客户', '出发日期'],
|
||||
恢复订单: ['预订客户', '出发日期'],
|
||||
导出团队文件: ['预订客户', '出发日期', '文件类型']
|
||||
@@ -169,6 +173,17 @@ const fieldLines = (block) =>
|
||||
.slice(1)
|
||||
.filter((line) => line.includes(':'));
|
||||
|
||||
const arrangementUpdateDirectives = ['安排酒店变更', '安排导游变更', '安排用车变更', '安排大交通变更', '安排其他/备案变更'];
|
||||
const catalogPairs = operationSections.flatMap((section) => {
|
||||
const directives = section.directive === '安排变更' ? arrangementUpdateDirectives : [section.directive];
|
||||
const blocks = textBlocks(section.body);
|
||||
return directives.map((directive) => {
|
||||
const matching = blocks.filter((block) => block.startsWith(`${directive}\n`));
|
||||
return { directive, blocks: matching, template: matching[0], example: matching[1] };
|
||||
});
|
||||
});
|
||||
const pairByDirective = new Map(catalogPairs.map((pair) => [pair.directive, pair]));
|
||||
|
||||
const assertTwoBlockContract = (document, directives, source) => {
|
||||
const blocks = textBlocks(document);
|
||||
|
||||
@@ -192,17 +207,22 @@ test('catalog has exactly 19 ordered operations with adjacent template and examp
|
||||
);
|
||||
assert.deepEqual(operationSections.map((section) => section.directive), allDirectives);
|
||||
|
||||
for (const pair of catalogPairs) {
|
||||
assert.equal(pair.blocks.length, 2, `${pair.directive}: expected template and example`);
|
||||
assert.ok(pair.template, `${pair.directive}: missing template block`);
|
||||
assert.ok(pair.example, `${pair.directive}: missing example block`);
|
||||
assert.match(pair.template, /(必填):/u);
|
||||
assert.doesNotMatch(pair.example, /(必填|选填)/u);
|
||||
}
|
||||
for (const section of operationSections) {
|
||||
const template = labeledBlock(section, '模板');
|
||||
const example = labeledBlock(section, '示例');
|
||||
assert.ok(template, `${section.directive}: missing template block`);
|
||||
assert.ok(example, `${section.directive}: missing example block`);
|
||||
assert.equal(textBlocks(section.body).length, section.directive === '安排变更' ? 7 : 2, `${section.directive}: unexpected text blocks`);
|
||||
if (section.directive === '安排变更') {
|
||||
assert.match(textBlocks(section.body)[2], /^安排酒店变更\n团号:.+\n入住日期:.+\n离店日期:.+\n酒店搜索:.+\n间数:9$/u);
|
||||
const blocks = textBlocks(section.body);
|
||||
assert.deepEqual(blocks.map((block) => block.split('\n')[0]), arrangementUpdateDirectives.flatMap((directive) => [directive, directive]));
|
||||
continue;
|
||||
}
|
||||
assert.equal(template.split(/\r?\n/, 1)[0], section.directive);
|
||||
assert.equal(example.split(/\r?\n/, 1)[0], section.directive);
|
||||
assert.equal(textBlocks(section.body).length, 2);
|
||||
assert.ok(labeledBlock(section, '模板'));
|
||||
assert.ok(labeledBlock(section, '示例'));
|
||||
assert.ok(section.body.indexOf('#### 模板') < section.body.indexOf('#### 示例'));
|
||||
}
|
||||
|
||||
@@ -331,9 +351,9 @@ test('Agent parse Schema accepts business facts and rejects execution metadata',
|
||||
});
|
||||
|
||||
test('template fields are marked and examples contain only concrete values', () => {
|
||||
for (const section of operationSections) {
|
||||
const template = labeledBlock(section, '模板');
|
||||
const example = labeledBlock(section, '示例');
|
||||
for (const section of catalogPairs) {
|
||||
const template = section.template;
|
||||
const example = section.example;
|
||||
const fields = fieldLines(template);
|
||||
|
||||
assert.ok(fields.length > 0, `${section.directive}: expected at least one field`);
|
||||
@@ -351,8 +371,8 @@ test('template fields are marked and examples contain only concrete values', ()
|
||||
test('user templates never request ERP internal identifiers and use canonical customer label', () => {
|
||||
const forbiddenLabels = /^(?:母团\s*)?(?:tid|ddid|row_id|行\s*ID|账号|运行编号|测试标记|allowlist)((?:必填|选填)):/im;
|
||||
|
||||
for (const section of operationSections) {
|
||||
const template = labeledBlock(section, '模板');
|
||||
for (const section of catalogPairs) {
|
||||
const template = section.template;
|
||||
assert.doesNotMatch(template, forbiddenLabels);
|
||||
assert.doesNotMatch(template, /^预定客户((?:必填|选填)):/m, 'template must use 预订客户');
|
||||
}
|
||||
@@ -416,7 +436,7 @@ test('the two desktop-approved examples remain exactly copyable', () => {
|
||||
|
||||
test('every operation preserves its explicit minimum-required field set', () => {
|
||||
for (const [directive, expectedLabels] of Object.entries(minimumLabels)) {
|
||||
const template = labeledBlock(sectionByDirective.get(directive), '模板');
|
||||
const template = pairByDirective.get(directive).template;
|
||||
const actualLabels = fieldLines(template)
|
||||
.filter((line) => line.includes('(必填):'))
|
||||
.map((line) => line.slice(0, line.indexOf('(必填):')));
|
||||
@@ -441,9 +461,9 @@ test('modification input asks only for targets and export stays minimal', () =>
|
||||
'修改散拼母团计划收客数',
|
||||
'追加散拼子单订房说明',
|
||||
'独立团信息修改',
|
||||
'安排变更'
|
||||
...arrangementUpdateDirectives
|
||||
]) {
|
||||
assert.doesNotMatch(labeledBlock(sectionByDirective.get(directive), '模板'), forbiddenPreviousValueField);
|
||||
assert.doesNotMatch(pairByDirective.get(directive).template, forbiddenPreviousValueField);
|
||||
}
|
||||
|
||||
assert.deepEqual(minimumLabels['导出团队文件'], ['预订客户', '出发日期', '文件类型']);
|
||||
@@ -497,7 +517,15 @@ test('independent-order updates preserve broad parser targets while arrangement
|
||||
assert.match(updatingSkill, /兼容旧三段 `人数改为8\+1(占)\+1`/);
|
||||
assert.match(updatingSkill, /pax\.child_no_bed=1/);
|
||||
assert.match(sectionByDirective.get('独立团信息修改')?.body ?? '', /未出现的类别保持原值/);
|
||||
assert.match(labeledBlock(sectionByDirective.get('安排变更'), '示例'), /离店日期改为11-05;房间数改为15间/);
|
||||
const updates = sectionByDirective.get('安排变更').body;
|
||||
const [current, legacy] = updates.split('#### 旧版酒店指令兼容说明');
|
||||
assert.ok(legacy, 'legacy hotel syntax must be separately explained');
|
||||
assert.doesNotMatch(current, /^安排变更$|^目标变更[(:]|^安排类型[(:]/mu);
|
||||
assert.match(current, /日期填写原安排日期,只用于定位,不修改日期/);
|
||||
assert.match(legacy, /只接受酒店/);
|
||||
assert.match(legacy, /会实际修改离店日期/);
|
||||
assert.match(legacy, /默认针对酒店表单第一行/);
|
||||
assert.equal(textBlocks(legacy).length, 0, 'legacy syntax must not be presented as a current copyable template');
|
||||
assert.doesNotMatch(catalog, /需求登记:/);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const testSource = adapterSource.replace(
|
||||
);
|
||||
const hotelFields = ['riqi0', 'riqis0', 'danwei0', 'danweiid0', 'fangxing0', 'jianshu0', 'zt0', 'beizhu0', 'id0', 'yf0', 'sh0', 'MaxI', 'tdID', 'ddID', 'chufari', 'SubmitButton'];
|
||||
|
||||
function fixture({ count = 154, maxI = '6', mode = 'create', tick = () => {}, proofs } = {}) {
|
||||
function fixture({ count = 76, maxI = '2', rows = Number(maxI) || 2, mode = 'create', tick = () => {}, proofs } = {}) {
|
||||
let now = 10000;
|
||||
const initialTime = now;
|
||||
const values = { MaxI: maxI, tdID: '123', ddID: '0', chufari: '2026-09-15' };
|
||||
@@ -23,8 +23,11 @@ function fixture({ count = 154, maxI = '6', mode = 'create', tick = () => {}, pr
|
||||
disabled: false, value: values[name] ?? ''
|
||||
});
|
||||
const form = { id: 'InfoForm1', isConnected: true, elements: hotelFields.map(field) };
|
||||
for (let i = hotelFields.length - 1; i < count; i += 1) form.elements.push(field(`extra${i}`));
|
||||
const state = { loading: false };
|
||||
for (let row = 1; row < rows; row += 1) {
|
||||
form.elements.push(...hotelFields.filter((name) => name.endsWith('0')).map((name) => field(`${name.slice(0, -1)}${row}`)));
|
||||
}
|
||||
for (let i = form.elements.length - 1; i < count; i += 1) form.elements.push(field(`extra${i}`));
|
||||
const state = { loading: false, currentForm: form };
|
||||
const loadingElement = {
|
||||
className: 'datagrid-mask',
|
||||
getBoundingClientRect: () => ({ width: 50, height: 50, left: 0, top: 0, right: 50, bottom: 50 }),
|
||||
@@ -32,7 +35,7 @@ function fixture({ count = 154, maxI = '6', mode = 'create', tick = () => {}, pr
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll: (selector) => selector.startsWith('.datagrid-mask') && state.loading ? [loadingElement] : [],
|
||||
querySelector: () => form,
|
||||
querySelector: () => state.currentForm,
|
||||
readyState: 'complete'
|
||||
};
|
||||
const window = {
|
||||
@@ -59,11 +62,13 @@ function fixture({ count = 154, maxI = '6', mode = 'create', tick = () => {}, pr
|
||||
};
|
||||
}
|
||||
|
||||
test('hotel create accepts the reported complete 154-field form after one stable second', async () => {
|
||||
test('hotel create accepts a complete two-row 76-field form after one stable second', async () => {
|
||||
const page = fixture();
|
||||
const result = await page.run();
|
||||
assert.equal(result.successful_control_count, 154);
|
||||
assert.equal(result.successful_control_count, 76);
|
||||
assert.equal(result.ready, true, JSON.stringify(result.blockers));
|
||||
assert.equal(result.hotel_row_count, 2);
|
||||
assert.equal(result.native_max_i, 2);
|
||||
assert.ok(page.elapsed() >= 1000 && page.elapsed() < 2000);
|
||||
});
|
||||
|
||||
@@ -74,13 +79,12 @@ test('hotel create fails closed without a form', async () => {
|
||||
assert.ok(missing.blockers.includes('lifecycle_complete_form_missing'));
|
||||
});
|
||||
|
||||
test('hotel create uses the requested 150-field boundary', async () => {
|
||||
for (const count of [149, 150, 154, 195, 442]) {
|
||||
const result = await fixture({ count }).run();
|
||||
test('hotel create accepts complete variable-size forms without a serialized count threshold', async () => {
|
||||
for (const [count, maxI] of [[76, '2'], [149, '5'], [150, '5'], [154, '5'], [195, '7'], [442, '16']]) {
|
||||
const result = await fixture({ count, maxI }).run();
|
||||
assert.equal(result.successful_control_count, count);
|
||||
assert.equal(result.minimum_successful_controls, 150);
|
||||
assert.equal(result.ready, count >= 150, `${count}: ${JSON.stringify(result.blockers)}`);
|
||||
if (count < 150) assert.ok(result.blockers.includes(`lifecycle_form_incomplete:${count}:150`));
|
||||
assert.equal(result.minimum_successful_controls, null);
|
||||
assert.equal(result.ready, true, `${count}: ${JSON.stringify(result.blockers)}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,7 +110,7 @@ test('hotel create waits for visible loading to disappear and then stabilize', a
|
||||
assert.ok(result.blockers.includes('lifecycle_loading_overlay_visible'));
|
||||
});
|
||||
|
||||
test('hotel create retains the existing one-second count stability gate', async () => {
|
||||
test('hotel create requires a full observed stable second, including the final timeout sample', async () => {
|
||||
const grow = (form) => form.elements.push({ name: `extra${form.elements.length}`, tagName: 'INPUT', type: 'text', value: '' });
|
||||
const settling = fixture({ tick: ({ elapsed, form }) => { if (elapsed === 750) grow(form); } });
|
||||
assert.equal((await settling.run()).ready, true);
|
||||
@@ -115,6 +119,10 @@ test('hotel create retains the existing one-second count stability gate', async
|
||||
const result = await changing.run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.ok(result.blockers.includes('lifecycle_form_not_stable'));
|
||||
assert.equal((await fixture().run(999)).ready, false);
|
||||
assert.equal((await fixture().run(1000)).ready, true);
|
||||
const lastSample = fixture({ tick: ({ elapsed, form }) => { if (elapsed === 1000) grow(form); } });
|
||||
assert.equal((await lastSample.run(1000)).ready, false);
|
||||
});
|
||||
|
||||
test('hotel update/clear and unrelated lifecycle actions retain their current count gates', async () => {
|
||||
@@ -132,7 +140,7 @@ test('hotel update/clear and unrelated lifecycle actions retain their current co
|
||||
assert.equal(result.minimum_successful_controls, 90);
|
||||
});
|
||||
|
||||
test('154-field readiness still leaves occupied slots and payment/audit state blocked', async () => {
|
||||
test('76-field readiness still leaves occupied slots and payment/audit state blocked', async () => {
|
||||
for (const [name, value, blocker] of [
|
||||
['id0', '1234', 'arrangement_slot_occupied:id0'],
|
||||
['danweiid0', '5678', 'arrangement_slot_occupied:danweiid0'],
|
||||
@@ -148,7 +156,7 @@ test('154-field readiness still leaves occupied slots and payment/audit state bl
|
||||
}
|
||||
});
|
||||
|
||||
test('full hotel preflight preserves identity, resource and ownership gates after the count check', async () => {
|
||||
test('full hotel preflight preserves identity, resource and ownership gates after structural readiness', async () => {
|
||||
for (const failure of ['', 'identity', 'resource', 'ownership', 'slot']) {
|
||||
const calls = [];
|
||||
const page = fixture({ proofs: {
|
||||
@@ -174,3 +182,81 @@ test('full hotel preflight preserves identity, resource and ownership gates afte
|
||||
assert.equal(result.write_attempted, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('hotel create rejects missing controls in later rows even on a large form', async () => {
|
||||
for (const name of hotelFields.filter((name) => name.endsWith('0')).map((name) => `${name.slice(0, -1)}1`)) {
|
||||
const page = fixture({ count: 442 });
|
||||
page.form.elements = page.form.elements.filter((control) => control.name !== name);
|
||||
const result = await page.run();
|
||||
assert.equal(result.ready, false, name);
|
||||
assert.ok(result.missing_required_fields.includes(name), name);
|
||||
}
|
||||
});
|
||||
|
||||
test('hotel create rejects invalid row markers, omitted rows and gaps', async () => {
|
||||
for (const maxI of ['', '0', '-1', '2.5', '2x', 'Infinity', '9007199254740992']) {
|
||||
const result = await fixture({ maxI, rows: 2 }).run();
|
||||
assert.equal(result.ready, false, maxI);
|
||||
assert.ok(result.blockers.includes('lifecycle_hotel_max_i_invalid'));
|
||||
}
|
||||
for (const [maxI, rows] of [['3', 2], ['1', 2], ['999999999', 2]]) {
|
||||
const result = await fixture({ maxI, rows }).run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.ok(result.blockers.includes(`lifecycle_hotel_rows_incomplete:${rows}:${maxI}`));
|
||||
}
|
||||
const gap = fixture({ maxI: '3' });
|
||||
gap.form.elements = gap.form.elements.filter((control) => !/^(riqi|riqis|danwei|danweiid|fangxing|jianshu|zt|beizhu|id|yf|sh)1$/.test(control.name));
|
||||
assert.equal((await gap.run()).ready, false);
|
||||
});
|
||||
|
||||
test('hotel create rejects disabled, duplicate and id-only required data controls', async () => {
|
||||
for (const change of [
|
||||
(control) => { control.disabled = true; },
|
||||
(control) => { control.matches = (selector) => selector === ':disabled'; },
|
||||
(control, form) => { form.elements.push({ ...control }); },
|
||||
(control) => { control.name = ''; },
|
||||
(control) => { control.type = 'button'; },
|
||||
(control) => { control.isConnected = false; }
|
||||
]) {
|
||||
const page = fixture();
|
||||
change(page.form.elements.find((control) => control.name === 'jianshu0'), page.form);
|
||||
const result = await page.run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.ok(result.unavailable_required_fields.includes('jianshu0'));
|
||||
}
|
||||
const readonly = fixture();
|
||||
readonly.form.elements.find((control) => control.name === 'fangxing0').readOnly = true;
|
||||
assert.equal((await readonly.run()).ready, true);
|
||||
});
|
||||
|
||||
test('hotel create rejects detached or replaced forms and unfinished documents', async () => {
|
||||
for (const change of [
|
||||
(page) => { page.form.isConnected = false; },
|
||||
(page) => { page.state.currentForm = { ...page.form }; },
|
||||
(page) => { page.form.ownerDocument.readyState = 'loading'; }
|
||||
]) {
|
||||
const page = fixture();
|
||||
change(page);
|
||||
assert.equal((await page.run()).ready, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('same-count control replacement and select-option changes restart stability', async () => {
|
||||
for (const change of [
|
||||
(form) => { form.elements[0] = { ...form.elements[0] }; },
|
||||
(form) => { form.elements.at(-1).id += 'changed'; },
|
||||
(form) => { form.elements.at(-1).options = [{}]; }
|
||||
]) {
|
||||
const page = fixture({ tick: ({ elapsed, form }) => { if (elapsed === 750) change(form); } });
|
||||
assert.equal((await page.run()).ready, true);
|
||||
assert.equal(page.elapsed(), 1750);
|
||||
}
|
||||
});
|
||||
|
||||
test('native pending ajax blocks hotel readiness even without a visible overlay', async () => {
|
||||
const page = fixture();
|
||||
page.form.ownerDocument.defaultView.jQuery = { active: 1 };
|
||||
const result = await page.run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.equal(result.native_ajax_active, true);
|
||||
});
|
||||
@@ -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\.177'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.178'/);
|
||||
});
|
||||
|
||||
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.177 keeps reload dormant unless its durable execution state is fully idle', async () => {
|
||||
test('extension 0.5.178 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.177');
|
||||
assert.equal(mapping.current_extension_version, '0.5.178');
|
||||
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.177');
|
||||
assert.match(inpage, /version: '0\.5\.177'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.177'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.177'/);
|
||||
assert.equal(extensionManifest.version, '0.5.178');
|
||||
assert.match(inpage, /version: '0\.5\.178'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.178'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.178'/);
|
||||
assert.match(inpage, /function strictIsoDate\(value\)/);
|
||||
assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/);
|
||||
assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);
|
||||
|
||||
@@ -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\.131\s*$/mu);
|
||||
assert.match(template, /^交付版本:0\.5\.132\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