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