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.
mail-yubao/tests/unit/email-fixtures-catalog.test.ts

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);
});
});