导入表头修复

This commit is contained in:
andy committed 2026-09-16 17:11:16 +08:00
1 parent 8a0f02c9be
commit a3505f3921
28 files changed
+281 -45

No files matched your search

@@ -136,6 +136,102 @@ test('XLSX is deterministically normalized to the canonical 13-column TSV', asyn
].join('\n'));
});
test('an absent phone header produces blank phones without guessing unheaded values', async () => {
const baseline = await normalizeSynthetic(await syntheticWorkbook());
const expected = baseline.canonicalTsv.split('\n').map((line, index) => {
if (index === 0) return line;
const fields = line.split('\t');
fields[11] = '';
return fields.join('\t');
}).join('\n');
for (const removeColumn of [false, true]) {
const result = await normalizeSynthetic(await syntheticWorkbook(workbook => {
const sheet = workbook.getWorksheet('Sheet1')!;
if (removeColumn) sheet.spliceColumns(13, 1);
else sheet.getCell('M2').value = null; // The original number below remains unheaded.
}));
assert.equal(result.canonicalTsv, expected);
assert.equal(result.rowCount, 2);
const parsed = await parseProgramInput({
rawText: `导入散拼子单名单\n预订客户:测试客户\n出发日期:2026-09-18\n名单内容:\n${result.canonicalTsv}`,
receivedAt: new Date('2026-09-16T00:00:00Z')
});
assert.equal(parsed.status, 'agent_parse_passed');
}
});
test('phone-free rosters support reordered alias headers, merged remarks and XLS conversion', async () => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('无电话模板');
sheet.mergeCells('B1:K1');
sheet.getCell('B1').value = '脱敏团队信息';
sheet.addRow(['序号', '姓名', '英文姓名', '护照号码', '性别', '出生日期', '年龄', '出生地点', '签发地', '签发日期', '有效期至', '备注']);
for (let sequence = 1; sequence <= 8; sequence++) {
sheet.addRow([sequence, '测试游客', 'TEST', `P0000000${sequence}`, '女', '1990-01-02', 36, '测试地', '测试地', '2025-01-02', '2035-01-01', '普通备注']);
}
sheet.mergeCells('L3:L10');
sheet.getCell('M10').value = 13800000000; // Extra unheaded content must not become a phone.
const xlsx = Buffer.from(await workbook.xlsx.writeBuffer());
const expected = await normalizeSynthetic(xlsx);
assert.equal(expected.rowCount, 8);
for (const row of expected.canonicalTsv.split('\n').slice(1)) {
assert.equal(row.split('\t').length, 13);
assert.deepEqual(row.split('\t').slice(11), ['', '普通备注']);
}
assert.doesNotMatch(expected.canonicalTsv, /13800000000/);
const legacy = await normalizePassengerRosterWorkbook({
content: Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]),
fileName: 'synthetic.xls', contentType: 'application/vnd.ms-excel'
}, { convertLegacyXls: async () => xlsx });
assert.equal(legacy.canonicalTsv, expected.canonicalTsv);
});
test('phone is the only optional semantic and duplicate phone headers stay blocked', async () => {
for (const column of [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 14]) {
const content = await syntheticWorkbook(workbook => {
const sheet = workbook.getWorksheet('Sheet1')!;
sheet.getCell('M2').value = null;
sheet.getCell(2, column).value = null;
});
await expectRosterError(() => normalizeSynthetic(content), 'roster_workbook_header_not_found');
}
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getCell('O2').value = '电话';
}).then(normalizeSynthetic), 'roster_workbook_header_not_found');
});
test('present phone columns retain formula, value, hidden-data and row checks', async () => {
for (const [value, code] of [
[{ formula: '1+1', result: 2 }, 'roster_workbook_formula_not_allowed'],
[{ error: '#VALUE!' }, 'roster_workbook_invalid_cell'],
[1.5, 'roster_workbook_invalid_identifier']
] as const) {
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getCell('M3').value = value;
}).then(normalizeSynthetic), code, { row: 3, column: 13 });
}
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getColumn(13).hidden = true;
}).then(normalizeSynthetic), 'roster_workbook_hidden_data');
await expectRosterError(() => syntheticWorkbook(workbook => {
workbook.getWorksheet('Sheet1')!.getCell('M5').value = '13800000000';
}).then(normalizeSynthetic), 'roster_workbook_invalid_sequence', { row: 5, column: 1 });
});
test('a marked leader still requires a phone when the source phone column is absent', async () => {
const result = await normalizeSynthetic(await syntheticWorkbook(workbook => {
const sheet = workbook.getWorksheet('Sheet1')!;
sheet.getCell('M2').value = null;
sheet.getCell('N3').value = '领队';
}));
const parsed = await parseProgramInput({
rawText: `导入散拼子单名单\n预订客户:测试客户\n出发日期:2026-09-18\n名单内容:\n${result.canonicalTsv}`,
receivedAt: new Date('2026-09-16T00:00:00Z')
});
assert.notEqual(parsed.status, 'agent_parse_passed');
assert.match(JSON.stringify(parsed), /姓名和电话都必须填写/);
});
test('a merged title error is skipped, while header and passenger errors stay blocked', async () => {
const baseline = await normalizeSynthetic(await syntheticWorkbook());
const titleError = await normalizeSynthetic(await syntheticWorkbook(workbook => {