import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import test from 'node:test'; import vm from 'node:vm'; import { validateProgramOperation } from '../LianSyn-platform/external-agent-client.mjs'; const require = createRequire(import.meta.url); const plans = require('../chrome-extension/ltjt-order-assistant/operation-plans.js'); const headers = plans.PASSENGER_HEADERS.join('\t'); const row = (sequence, phone = '13800000001', remark = '') => [ sequence, `合成游客${sequence}`, 'SYNTHETIC GUEST', '男', '1990-01-01', '合成城市', '护照', `TEST-PASS-${sequence}`, '合成城市', '2025-01-01', '2035-01-01', phone, remark ].join('\t'); const operation = (text, rowCount = 1, kind = 'independent_order') => ({ action: 'passenger_list_import', order_nature: 'formal', submit_mode: 'dry_run', data: { existing_refs: { kind, identifier: 'SYNTH-ORDER-001' }, passenger_list: { operation: 'import', row_count: rowCount, text } } }); test('passenger TSV keeps trailing empty columns through server and plugin dispatch', () => { for (const kind of ['independent_order', 'shared_child_order']) { for (const phone of ['13800000001', '']) { const text = [headers, ...Array.from({ length: 19 }, (_, i) => row(i + 1, phone))].join('\n'); for (const input of [text, `\r\n${text.replace(/\n/g, '\r\n')}\r\n\r\n`]) { for (const mode of ['import', 'full_replace']) { const candidate = operation(input, 19, kind); Object.assign(candidate.data.passenger_list, mode === 'full_replace' ? { operation: mode, confirmed: true } : { operation: mode }); assert.deepEqual(validateProgramOperation(candidate), []); assert.deepEqual(plans.validateDispatchOperation(candidate).blockers, []); assert.equal(plans.buildPassengerTsv(candidate.data.passenger_list), text); assert.equal(candidate.data.passenger_list.text, input); } } } } }); test('passenger TSV still rejects missing or surplus columns and keeps leader validation', () => { for (const malformed of [row(1).slice(0, -1), `${row(1)}\t`]) { const candidate = operation(`${headers}\n${malformed}`); assert.match(validateProgramOperation(candidate).join('\n'), /完整 13 列/); assert.match(plans.validateDispatchOperation(candidate).blockers.join('\n'), /13 列/); } const candidate = operation(`${headers}\n${row(1, '13800000001', '领队')}\n${row(2, '')}`, 2); assert.match(validateProgramOperation(candidate).join('\n'), /leader_contact/); assert.match(plans.validateDispatchOperation(candidate).blockers.join('\n'), /leader_contact/); candidate.data.passenger_list.leader_contact = { sequence: 1, name: '合成游客1', phone: '13800000001' }; assert.deepEqual(validateProgramOperation(candidate), []); assert.deepEqual(plans.validateDispatchOperation(candidate).blockers, []); candidate.data.passenger_list.leader_contact.phone = '13800000002'; assert.match(validateProgramOperation(candidate).join('\n'), /完全一致/); assert.match(plans.validateDispatchOperation(candidate).blockers.join('\n'), /完全一致/); }); test('passenger merge accepts empty final fields without repairing malformed column counts', async () => { const source = await readFile(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8'); const sandbox = { window: { LTJTOrderAssistant: {} } }; vm.runInNewContext(source, sandbox); const merge = sandbox.window.LTJTOrderAssistant.planPassengerMerge; const blankRows = Array.from({ length: 19 }, (_, i) => Object.fromEntries(plans.PASSENGER_HEADERS.map(header => [ header, header === '序号' ? i + 1 : header === '证件类型' ? '护照' : '' ]))); for (const phone of ['13800000001', '']) { const text = [headers, ...Array.from({ length: 19 }, (_, i) => row(i + 1, phone))].join('\n'); for (const input of [text, `\r\n${text.replace(/\n/g, '\r\n')}\r\n`]) { const result = merge(blankRows, { operation: 'first_import', row_count: 19, text: input }, 19); assert.deepEqual([...result.blockers], []); assert.equal(result.merged_tsv, text); assert.equal(result.input_rows[18]['电话'], phone); assert.equal(result.input_rows[18]['备注'], ''); assert.equal(result.changed_sequences.length, 19); } } for (const malformed of [row(1).slice(0, -1), `${row(1)}\t`]) { const result = merge(blankRows, { operation: 'first_import', row_count: 1, text: `${headers}\n${malformed}` }, 19); assert.ok(result.blockers.includes('passenger_input_row_columns_incomplete')); } });