You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
5.9 KiB
161 lines
5.9 KiB
import { describe, expect, it } from "vitest";
|
|
import { classifyMail } from "@/services/parse/classify";
|
|
import { parsePackingListBuffer } from "@/services/parse/packing-list";
|
|
import { parseMailSource } from "@/services/imap/snapshot";
|
|
import { decidePull } from "@/services/imap/pull-filter";
|
|
import { resolveNoSpreadsheetOutcome } from "@/services/parse/pipeline";
|
|
import { resolveParsedMailBody } from "@/utils/mail-body-text";
|
|
import {
|
|
fixtureFilenames,
|
|
loadAllEmailFixtures,
|
|
loadEmailFixture,
|
|
} from "@/services/test/email-fixture-library";
|
|
|
|
function spreadsheetRank(filename: string): number {
|
|
const f = filename || "";
|
|
if (/数据模版|提拆派|卡派资料|卡派清单|装箱清单|packing/i.test(f)) return 0;
|
|
if (/换标|覆盖贴|贴标|操作指令/i.test(f)) return 2;
|
|
return 1;
|
|
}
|
|
|
|
function pickSpreadsheet(filenames: string[]): string | null {
|
|
const sheets = filenames
|
|
.filter((f) => /\.(xlsx|xls|csv|zip)$/i.test(f))
|
|
.slice()
|
|
.sort((a, b) => spreadsheetRank(a) - spreadsheetRank(b) || a.localeCompare(b));
|
|
return sheets[0] || null;
|
|
}
|
|
|
|
describe("email fixture catalog", () => {
|
|
it("covers >=15 fixtures and all required categories", async () => {
|
|
const all = await loadAllEmailFixtures();
|
|
expect(all.length).toBeGreaterThanOrEqual(15);
|
|
const cats = new Set(all.map((f) => f.spec.category));
|
|
expect(cats.has("normal")).toBe(true);
|
|
expect(cats.has("boundary")).toBe(true);
|
|
expect(cats.has("abnormal")).toBe(true);
|
|
expect(cats.has("duplicate")).toBe(true);
|
|
expect(cats.has("multi_attachment")).toBe(true);
|
|
for (const f of all) {
|
|
expect(f.expected.mail_type).toBeTruthy();
|
|
expect(f.expected.status).toBeTruthy();
|
|
expect(f.spec.messageId).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it("classifyMail matches expected.mail_type (except pull-skipped unknown)", async () => {
|
|
const all = await loadAllEmailFixtures();
|
|
for (const f of all) {
|
|
if (f.expected.pull === false) {
|
|
const d = decidePull({
|
|
fromAddr: f.spec.from,
|
|
subject: f.spec.subject,
|
|
body: f.spec.text,
|
|
filenames: fixtureFilenames(f.spec),
|
|
rules: [],
|
|
});
|
|
expect(d.pull, f.id).toBe(false);
|
|
expect(f.expected.mail_type).toBe("UNKNOWN");
|
|
continue;
|
|
}
|
|
const evidence = classifyMail({
|
|
subject: f.spec.subject,
|
|
body: f.spec.text,
|
|
filenames: fixtureFilenames(f.spec),
|
|
});
|
|
expect(evidence.mail_type, f.id).toBe(f.expected.mail_type);
|
|
}
|
|
});
|
|
|
|
it("packing fixtures: parsePackingListBuffer === expected shipments", async () => {
|
|
const ids = [
|
|
"01-normal-forecast",
|
|
"13-illegal-date",
|
|
"12-missing-required",
|
|
];
|
|
for (const id of ids) {
|
|
const f = await loadEmailFixture(id);
|
|
const xlsx = f.attachments.find((a) => a.filename.endsWith(".xlsx"));
|
|
expect(xlsx, id).toBeTruthy();
|
|
const parsed = await parsePackingListBuffer(xlsx!.content, {
|
|
enableIsoCheck: true,
|
|
});
|
|
if (f.expected.missing_columns) {
|
|
expect(parsed.errorCode, id).toBe("PARSE_FAILED");
|
|
expect(parsed.lineage.missing_columns, id).toBeTruthy();
|
|
continue;
|
|
}
|
|
expect(parsed.errorCode, id).toBeUndefined();
|
|
if (f.expected.shipment_count != null) {
|
|
expect(parsed.shipments.length, id).toBe(f.expected.shipment_count);
|
|
}
|
|
if (f.expected.shipments) {
|
|
for (let i = 0; i < f.expected.shipments.length; i++) {
|
|
const exp = f.expected.shipments[i];
|
|
const row = parsed.shipments[i];
|
|
if (exp.F_FBACode != null) expect(row.F_FBACode, id).toBe(exp.F_FBACode);
|
|
if (exp.F_Transporter != null) {
|
|
expect(row.F_Transporter, id).toBe(exp.F_Transporter);
|
|
}
|
|
if (exp.F_CTNS != null) expect(row.F_CTNS, id).toBe(exp.F_CTNS);
|
|
if (exp.row_status) expect(row.row_status, id).toBe(exp.row_status);
|
|
if (exp.F_Expected_DeliveryDateB === null) {
|
|
expect(row.F_Expected_DeliveryDateB ?? null, id).toBeNull();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("07-gbk-body: mailparser decodes 新增预报", async () => {
|
|
const f = await loadEmailFixture("07-gbk-body");
|
|
const parsed = await parseMailSource(f.eml);
|
|
const body = resolveParsedMailBody(parsed);
|
|
expect(body).toMatch(/新增预报/);
|
|
expect(parsed.subject).toMatch(/WHSU8127240/);
|
|
});
|
|
|
|
it("08-oversized-attachment exceeds 20MB", async () => {
|
|
const f = await loadEmailFixture("08-oversized-attachment");
|
|
expect(f.attachments[0].content.length).toBeGreaterThan(20 * 1024 * 1024);
|
|
expect(f.expected.last_error).toBe("ATTACHMENT_TOO_LARGE");
|
|
});
|
|
|
|
it("09/11 no-spreadsheet outcomes match expected status", async () => {
|
|
const noAtt = await loadEmailFixture("09-no-attachment");
|
|
expect(
|
|
resolveNoSpreadsheetOutcome({
|
|
mailType: noAtt.expected.mail_type,
|
|
hasAttachments: false,
|
|
}),
|
|
).toEqual({
|
|
status: noAtt.expected.status,
|
|
lastError: noAtt.expected.last_error,
|
|
});
|
|
const wrong = await loadEmailFixture("11-wrong-format");
|
|
expect(
|
|
resolveNoSpreadsheetOutcome({
|
|
mailType: wrong.expected.mail_type,
|
|
hasAttachments: true,
|
|
}),
|
|
).toEqual({
|
|
status: wrong.expected.status,
|
|
lastError: wrong.expected.last_error,
|
|
});
|
|
});
|
|
|
|
it("14-duplicate-pull shares Message-ID with 01", async () => {
|
|
const a = await loadEmailFixture("01-normal-forecast");
|
|
const b = await loadEmailFixture("14-duplicate-pull");
|
|
expect(b.spec.messageId).toBe(a.spec.messageId);
|
|
expect(b.expected.idempotent_with).toBe("01-normal-forecast");
|
|
});
|
|
|
|
it("15-multi-attachment classifies WORK_ORDER but still selects 数据模版 over 换标 xlsx", async () => {
|
|
const f = await loadEmailFixture("15-multi-attachment");
|
|
const names = fixtureFilenames(f.spec);
|
|
expect(names.length).toBeGreaterThan(2);
|
|
expect(pickSpreadsheet(names)).toBe(f.expected.selected_attachment);
|
|
});
|
|
});
|