merge: integrate shared child batch creation

This commit is contained in:
inman committed 2026-09-09 17:24:25 +08:00
commit 0d20544d52
32 files changed
+958 -90

No files matched your search

+63 -7
View File
@@ -405,6 +405,62 @@ test('identifier-only native list searches receive a calendar-safe plus/minus on
assert.match(adapter, /listExpectedPresent && present && !ddidMatched/);
});
test('shared-child batch resolver expands the inclusive range and hard-matches concrete parent entry rows by product', async () => {
const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
const sandbox = { window: {} };
vm.runInNewContext(inpage, sandbox);
const assistant = sandbox.window.LTJTOrderAssistant;
const dates = Array.from(assistant.inspectInclusiveDateRange('2026-10-06', '2026-11-03'));
assert.equal(dates.length, 29);
assert.equal(dates[0], '2026-10-06');
assert.equal(dates.at(-1), '2026-11-03');
const parentLink = {
tagName: 'A',
textContent: '新增子单',
closest() { return parentRow; },
getAttribute(name) {
return name === 'onclick' ? "OPEN_update('61006','0')" : '';
}
};
const parentRow = {
id: 'tr_61006',
tagName: 'TR',
textContent: 'LW-261006-A 老挝行程--广东 2026-10-6',
getAttribute() { return ''; },
querySelectorAll(selector) { return selector === 'a' ? [parentLink] : []; }
};
const scopeDocument = { querySelectorAll: () => [parentRow] };
const matched = assistant.inspectSharedPlanBatchCandidates(scopeDocument, '2026-10-06', '老挝行程--广东');
assert.deepEqual(JSON.parse(JSON.stringify(matched.candidates)), [{
kind: 'shared_plan',
identifier: 'LW-261006-A',
parent_group_no: 'LW-261006-A',
plan_no: 'LW-261006-A',
tid: '61006',
departure_date: '2026-10-06',
row_id: 'tr_61006',
row_text: 'LW-261006-A 老挝行程--广东 2026-10-6',
internal_ref_complete: true
}]);
assert.equal(assistant.inspectSharedPlanBatchCandidates(scopeDocument, '2026-10-06', '其他产品').candidates.length, 0);
const resolutionBody = inpage.slice(
inpage.indexOf('async function resolveSharedChildBatchOperation'),
inpage.indexOf('async function resolveLifecycleOperation')
);
const executionBody = inpage.slice(
inpage.indexOf('async function createSplitChildBatchLive'),
inpage.indexOf('function normalizeExportType')
);
assert.match(resolutionBody, /for \(const departureDate of dates\)/);
assert.match(resolutionBody, /shared_child_batch_no_matching_parent/);
assert.match(resolutionBody, /batch_targets: batchTargets/);
assert.match(executionBody, /await createSplitChildLive\(childOperation\)/);
assert.match(executionBody, /status: 'not_started'/);
assert.doesNotMatch(executionBody, /retry/i);
});
test('business lookup uses customer/date as the native primary lookup and keeps optional facts supplemental', async () => {
const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
const sandbox = { window: {} };
@@ -1318,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\.169'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.170'/);
});
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
@@ -1528,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.169 keeps reload dormant unless its durable execution state is fully idle', async () => {
test('extension 0.5.170 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')
@@ -1967,7 +2023,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.169');
assert.equal(mapping.current_extension_version, '0.5.170');
assert.equal(
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
'ertrenshu'
@@ -2351,10 +2407,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.169');
assert.match(inpage, /version: '0\.5\.169'/);
assert.match(teamBatchInpage, /version: '0\.5\.169'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.169'/);
assert.equal(extensionManifest.version, '0.5.170');
assert.match(inpage, /version: '0\.5\.170'/);
assert.match(teamBatchInpage, /version: '0\.5\.170'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.170'/);
assert.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/);
+65
View File
@@ -214,6 +214,71 @@ test('shared-child leader lookup survives the strict execution gate', () => {
assert.deepEqual(validateStandardOperation(operation), []);
});
test('shared-child batch dispatch is Program-only, resolves all parents, and enters the guarded batch adapter', () => {
const parsed = {
action: 'shared_child_order_batch_create',
order_nature: 'formal',
submit_mode: 'dry_run',
data: {
customer: { name: '合成客户', keyword: '合成客户' },
product: { name: '老挝行程--广东', keyword: '老挝行程--广东' },
leader: { name: '张三', keyword: '张三' },
recurrence: { start_date: '2026-10-06', end_date: '2026-11-03' },
passenger_counts: { adult: 20, leader: 1 }
}
};
const dispatch = plans.validateDispatchOperation(parsed);
assert.equal(dispatch.ok, true, dispatch.blockers.join('; '));
assert.equal(dispatch.execution, 'browser_live_split_child_batch');
assert.equal(dispatch.requiresErpResolution, true);
assert.equal(plans.validateOperation(parsed).ok, false, 'execution gate requires ERP-resolved batch targets');
const resolved = {
...parsed,
data: {
...parsed.data,
batch_targets: [
{ parent_group_no: 'LW-261006-A', tid: '61006', departure_date: '2026-10-06' },
{ parent_group_no: 'LW-261103-A', tid: '61103', departure_date: '2026-11-03' }
],
resolution: {
stage: 'erp_readonly', target: 'batch', candidate_count: 2,
target_kind: 'shared_plan', write_attempted: false
}
}
};
const execution = plans.validateOperation(resolved);
assert.equal(execution.ok, true, execution.blockers.join('; '));
assert.equal(execution.execution, 'browser_live_split_child_batch');
assert.equal(execution.noErpWrite, false);
assert.deepEqual(validateStandardOperation(resolved), []);
});
test('shared-child batch execution rejects duplicate or out-of-range resolved parents', () => {
const operation = {
action: 'shared_child_order_batch_create',
order_nature: 'formal',
submit_mode: 'dry_run',
data: {
customer: { name: '合成客户' },
product: { name: '合成产品' },
recurrence: { start_date: '2026-10-06', end_date: '2026-11-03' },
passenger_counts: { adult: 20, leader: 1 },
batch_targets: [
{ parent_group_no: 'LW-261006-A', tid: '61006', departure_date: '2026-10-06' },
{ parent_group_no: 'LW-261006-A', tid: '61006', departure_date: '2026-11-04' }
],
resolution: {
stage: 'erp_readonly', target: 'batch', candidate_count: 2,
target_kind: 'shared_plan', write_attempted: false
}
}
};
const result = plans.validateOperation(operation);
assert.equal(result.ok, false);
assert.match(result.blockers.join('\n'), /重复母团目标|不在请求日期范围/);
});
test('blocks a marked split-child parent when the child remark loses the test marker', () => {
const result = plans.validateOperation({
action: 'shared_child_order_create',
+1
View File
@@ -153,6 +153,7 @@ test('background timing hook covers every executor through the shared result and
'verifyLifecycleOperation',
'createSplitParentLive',
'createSplitChildLive',
'createSplitChildBatchLive',
'exportConfirmationSources',
'preflightRawInstruction',
'liveSubmitApproved',