修改备注没有带入的问题

This commit is contained in:
andy
2026-09-29 15:43:15 +08:00
parent c2c71d35af
commit 2f0e6582d5
25 changed files with 5104 additions and 30 deletions

View File

@@ -2,6 +2,12 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import { webcrypto } from 'node:crypto';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const plans = require('../chrome-extension/ltjt-order-assistant/operation-plans.js');
const hotelBatch = require('../chrome-extension/ltjt-order-assistant/hotel-batch.js');
const inpage = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8')
@@ -15,7 +21,7 @@ const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/l
teamSummaryRequery = async (operation, subpage) => subpage;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [], onLinkageTick = () => {} } = {}) {
function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, onValidate = () => {}, sort = false, onPost = () => {}, linkageDelays = [], onLinkageTick = () => {}, onFieldEvent = () => {}, crypto = webcrypto } = {}) {
let now = 10000;
let posts = 0;
let selections = 0;
@@ -25,13 +31,13 @@ function fixture({ occupied = 1, rows = occupied + 2, onSelection = () => {}, on
const fields = new Map();
const document = { querySelector: () => form, querySelectorAll: () => [], readyState: 'complete', body: { innerText: '' } };
const location = { href: 'https://erp.invalid/System/Business/teams_jiudian.asp?tdID=123', pathname: '/System/Business/teams_jiudian.asp' };
const window = { document, Event: class {}, location, testValidate: () => onValidate(fields), getComputedStyle: () => ({ display: 'block', visibility: 'visible', opacity: '1' }) };
const window = { document, crypto, Event: class {}, location, testValidate: () => onValidate(fields), getComputedStyle: () => ({ display: 'block', visibility: 'visible', opacity: '1' }) };
window.parent = window;
document.defaultView = window;
form.ownerDocument = document;
const add = (name, value) => {
const control = { name, id: name, value: String(value), type: name === 'SubmitButton' ? 'submit' : 'text', tagName: 'INPUT', disabled: false,
ownerDocument: document, dispatchEvent() {},
ownerDocument: document, dispatchEvent() { onFieldEvent(fields, name); },
getAttribute: (key) => key.toLowerCase() === 'data' ? '270◆测试新酒店◆TWN H◆CNY◆120' : key.toLowerCase() === 'setval' ? name : null };
fields.set(name, control); form.elements.push(control);
};
@@ -204,3 +210,100 @@ test('requery cannot report an old row, duplicate new rows, changed existing row
assert.equal(f.posts(), 1);
}
});
test('omitted hotel remarks survive selection/date callbacks, save and read-only requery', async () => {
for (const omitted of [undefined, '']) for (const crypto of [webcrypto, null]) {
const f = fixture({ sort: true, crypto,
onSelection(fields, index) { fields.get(`beizhu${index}`).value = '6月21号起 按照包房12间\n有房损另计'; },
onFieldEvent(fields, name) { if (/^(danwei|riqi|riqis|jianshu)1$/.test(name)) fields.get('beizhu1').value = ''; }
});
if (omitted === undefined) delete f.operation.data.arrangement.remark;
const before = JSON.stringify(f.operation);
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, 'lifecycle_completed', JSON.stringify(result));
assert.equal(f.fields.get('beizhu1').value, '6月21号起 按照包房12间\n有房损另计');
assert.equal(f.fields.get('beizhu0').value, '保留备注0');
assert.equal(JSON.stringify(f.operation), before);
assert.equal(result.preflight.hotel_create.version, 2);
const checked = result.requery.checks.find(check => check.field === 'beizhu0');
assert.equal(checked.actual, f.fields.get('beizhu1').value);
assert.equal(checked.matched, true);
const verified = await f.api.verifyLifecycleOperation(f.operation, { hotel_create: result.preflight.hotel_create });
assert.equal(verified.status, 'lifecycle_requery_matched', JSON.stringify(verified));
assert.equal(f.posts(), 1);
}
});
test('explicit hotel remark overrides the native default, while missing native notes remain empty', async () => {
const f = fixture({ onSelection(fields, index) { fields.get(`beizhu${index}`).value = '酒店默认备注'; } });
f.operation.data.arrangement.remark = '用户备注:晚到保留';
const result = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(result.status, 'lifecycle_completed', JSON.stringify(result));
assert.equal(f.fields.get('beizhu1').value, '用户备注:晚到保留');
assert.equal(result.preflight.hotel_create.version, 1);
const empty = fixture();
const blank = await empty.api.liveSubmitLifecycleOperation(empty.operation);
assert.equal(blank.status, 'lifecycle_completed', JSON.stringify(blank));
assert.equal(empty.fields.get('beizhu1').value, '');
// Old executions stored v1 and intentionally wrote the omitted note as ''.
const oldProof = { ...blank.preflight.hotel_create, version: 1 };
delete oldProof.native_remark_sha256;
assert.equal((await empty.api.verifyLifecycleOperation(empty.operation, { hotel_create: oldProof })).status, 'lifecycle_requery_matched');
});
test('native remark changes at validation block POST; missing or changed saved evidence cannot report success', async () => {
const selection = (fields, index) => { fields.get(`beizhu${index}`).value = '酒店默认备注'; };
const f = fixture({ onSelection: selection, onValidate(fields) { fields.get('beizhu1').value = ''; } });
const blocked = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(blocked.status, 'lifecycle_apply_blocked', JSON.stringify(blocked));
assert.equal(f.posts(), 0);
const changed = fixture({ onSelection: selection, onPost(form) { form.elements.find(control => control.name === 'beizhu1').value = ''; } });
const uncertain = await changed.api.liveSubmitLifecycleOperation(changed.operation);
assert.equal(uncertain.status, 'lifecycle_execution_uncertain');
assert.ok(uncertain.requery.blockers.includes('arrangement_create_native_remark_changed_after_save'));
const good = fixture({ onSelection: selection });
const result = await good.api.liveSubmitLifecycleOperation(good.operation);
const proof = structuredClone(result.preflight.hotel_create);
delete proof.native_remark_sha256;
assert.equal((await good.api.verifyLifecycleOperation(good.operation, { hotel_create: proof })).status, 'lifecycle_requery_not_matched');
good.savedForm().elements.find(control => control.name === 'beizhu1').value = 'ERP后续变动';
assert.equal((await good.api.verifyLifecycleOperation(good.operation, { hotel_create: result.preflight.hotel_create })).status, 'lifecycle_requery_not_matched');
assert.equal(good.posts(), 1);
});
test('batch orchestration preserves each hotel native or explicit remark through actual lifecycle execution', async () => {
const base = fixture().operation;
const arrangements = [5, 6, 7].map(day => ({ mode: 'create', resource: { name: '测试新酒店TWN', keyword: '测试新酒店TWN' },
start_date: `2026-09-0${day}`, end_date: `2026-09-0${day + 1}`, room_count: 12,
...(day === 6 ? { remark: '用户独立备注' } : {}) }));
const operation = { action: 'arrangement_hotel', order_nature: 'formal', submit_mode: 'dry_run',
data: { existing_refs: { identifier: 'LW-TEST-HOTEL' }, arrangements } };
const before = JSON.stringify(operation);
const results = {}, reports = [];
let final;
const context = { operationPlans: plans, hotelBatch, throwIfTaskCancelled() {},
publishHotelBatch: async (_id, _op, batch) => { final = batch; },
chrome: { storage: { local: { get: async () => ({ businessTaskResults: results }) } } },
executeLifecycleOperation: async (id, child) => {
const index = reports.length;
const f = fixture({ onSelection(fields, slot) { fields.get(`beizhu${slot}`).value = `酒店备注${index}`; },
onFieldEvent(fields, name) { if (/^(riqi|riqis|jianshu)1$/.test(name)) fields.get('beizhu1').value = ''; },
onPost(form) { form.elements.find(control => control.name === 'id1').value = String(999 + index); }
});
f.operation.data.arrangement = { ...base.data.arrangement, ...child.data.arrangement, resource: base.data.arrangement.resource };
const report = await f.api.liveSubmitLifecycleOperation(f.operation);
assert.equal(report.status, 'lifecycle_completed', JSON.stringify(report));
report.requery = { matched: report.requery.matched, subpage: report.requery };
reports.push(report);
results[id] = { status: 'completed', report, write_attempted: true, no_erp_write: false };
}
};
const background = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
vm.createContext(context);
vm.runInContext(background.slice(background.indexOf('async function executeHotelBatch('), background.indexOf('async function runAutoTask(')), context);
await context.executeHotelBatch('TASK', operation);
assert.equal(hotelBatch.inspect(final).allCompleted, true);
assert.deepEqual(reports.map(report => report.requery.subpage.checks.find(check => /^beizhu\d+$/.test(check.field)).actual),
['酒店备注0', '用户独立备注', '酒店备注2']);
assert.equal(JSON.stringify(operation), before);
});

View File

@@ -123,6 +123,19 @@ test('fresh ERP MAIN world resolves the reported 2027 hotel update using the act
assert.equal(f.fields.get('riqis1').value, '2027-9-13');
});
test('hotel replacement preserves the existing note unless the user supplies a new one', async () => {
for (const remark of [undefined, '明确的新备注']) {
const f = fixture({ onSelection(fields, slot) { fields.get(`beizhu${slot}`).value = '新酒店默认备注'; } });
if (remark !== undefined) f.input.data.arrangement.changes.remark = remark;
const resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify(resolved));
const applied = await f.api.testApply(f.form, resolved.operation);
assert.equal(applied.blockers.length, 0, JSON.stringify(applied));
assert.equal(f.fields.get('beizhu1').value, remark ?? '保留备注1');
assert.equal(f.fields.get('beizhu0').value, '保留备注0');
}
});
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]);

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\.190'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.191'/);
});
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.190 keeps reload dormant unless its durable execution state is fully idle', async () => {
test('extension 0.5.191 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')
@@ -2037,7 +2037,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.190');
assert.equal(mapping.current_extension_version, '0.5.191');
assert.equal(
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
'ertrenshu'
@@ -2421,10 +2421,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.190');
assert.match(inpage, /version: '0\.5\.190'/);
assert.match(teamBatchInpage, /version: '0\.5\.190'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.190'/);
assert.equal(extensionManifest.version, '0.5.191');
assert.match(inpage, /version: '0\.5\.191'/);
assert.match(teamBatchInpage, /version: '0\.5\.191'/);
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.191'/);
assert.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);