import { describe, expect, it } from "vitest"; import fs from "fs"; import path from "path"; import { extractMailInstructions, splitBodySegments, } from "@/services/parse/split-instructions"; import { isNonInstructionSegment } from "@/services/parse/instruction-lexicon"; const SUBJECT = "Fw: \u8f6c\u53d1\uff1a\u65b0\u589e\u9884\u62a5136\uff1a\u65b0\u8fb0\u6cfd+WHLC027G597465+WHSU8127240+\u6d1b\u6749\u77f6+40HQ+EDT2026.04-25 ETA2026.05-15\u8239\u540d\u822a\u6b21HMM EMERALD 013E+\u63d0\u62c6\u6d3e\u7ec4\u5408\u67dc-\u4e0d\u5e26\u6258\u67b6"; function loadMail4Body(): string { const p = path.join( process.cwd(), "data", "logs", "docx_\u90ae\u4ef6_\u90ae\u4ef64_4\u90ae\u7bb1.pdf.txt", ); if (fs.existsSync(p)) return fs.readFileSync(p, "utf8"); return [ "Dear,", "\u539f\u7bb1\u53f7YT2604021091=FBA199R49LD6-MDW2=76\u4ef6\u64cd\u4f5c\u6307\u4ee4", "\u6362\u6807\u540e\u5355\u53f7\uff1aFBA19HW52S0L-2G1MCB6X-HIA1=76\u4ef6", "1\uff1a\u8986\u76d6\u8d34FBA\u6807\u7b7e", "\u5728 2026-05-25 13:45:03 \uff0c \"\u65b0\u8fb0\u6cfd\" \u5199\u9053\uff1a", "Dear,", "WHSU8127240= \u539f\u7bb1\u53f7 \u64cd\u4f5c\u6307\u4ee4 \u6362\u6807", "\u5728 2026-05-09 18:00:16 \uff0c \"luo\" \u5199\u9053\uff1a", "\u597d\u7684", "Original:", "Dear,", "WHLC027G597465+WHSU8127240-- \u6b64\u67dc\u5b50\u9884\u62a5\u5df2\u548c\u4f60\u4eec\u8001\u677f\u6c9f\u901a\u8fc7 \u6309\u7167\u6b63\u5e38\u9884\u62a5\u63a5\u53d7\u54c8\uff0c\u5e2e\u5fd9\u5b89\u6392\u6d3e\u5355", "\u5728 2026-05-09 10:52:25 \uff0c \"luo\" \u5199\u9053\uff1a", "\u62b1\u6b49 \u4ed3\u5e93\u4ea7\u80fd\u6709\u9650 \u8be5\u67dc\u5b50\u6211\u4eec\u76ee\u524d\u65e0\u6cd5\u63a5\u6536", "Original:", "Dear,", "\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5\uff1a \u65b0\u8fb0\u6cfd +WHLC027G597465+WHSU8127240+DO\u4e5f\u540c\u6b65\u4e0a\u4f20\u81f3\u9644\u4ef6", ].join("\n"); } describe("mail4 time-round split + customer_instruction", () => { it("skips ACK and \u65e0\u6cd5\u63a5\u6536", () => { expect(isNonInstructionSegment("\u597d\u7684\nluo")).toBe(true); expect( isNonInstructionSegment( "\u62b1\u6b49 \u4ed3\u5e93\u4ea7\u80fd\u6709\u9650 \u8be5\u67dc\u5b50\u6211\u4eec\u76ee\u524d\u65e0\u6cd5\u63a5\u6536 \u8bf7\u60a8\u8c05\u89e3", ), ).toBe(true); }); it("splits by time headers", () => { const body = loadMail4Body(); const segs = splitBodySegments(body); expect(segs.length).toBeGreaterThanOrEqual(4); }); it("latest is work_order; accept-forecast is customer_instruction; no empty DO", () => { const units = extractMailInstructions({ subject: SUBJECT, body: loadMail4Body(), filenames: [ "YT2604021091=76\u4ef6\u6362\u6807\u8d70HIA1\u5361\u6d3e\u51fa\u5e93\u64cd\u4f5c\u6307\u4ee4.xlsx", ], }); const current = units.filter((u) => u.isCurrent); expect(current.some((u) => u.uiKind === "work_order")).toBe(true); expect(current.every((u) => u.uiKind !== "forecast")).toBe(true); expect( units.some( (u) => u.uiKind === "customer_instruction" && /按照正常预报接受|安排派单/.test(u.segmentText), ), ).toBe(true); expect( units.some( (u) => u.uiKind === "customer_instruction" && u.keywords.includes("仓库确认"), ), ).toBe(true); expect( units.some( (u) => u.uiKind === "customer_instruction" && u.keywords.includes("无法接收"), ), ).toBe(true); // 历史正文「请查收新增预报」可保留只读预报;主题不得单独造当前预报 expect( units.some((u) => u.uiKind === "forecast" && u.source === "body_segment"), ).toBe(true); expect(units.some((u) => u.source === "subject")).toBe(false); // 正文提到 DO 但无 DO.pdf → 不出空的上传DO expect(units.some((u) => u.uiKind === "do_upload")).toBe(false); }); it("QQ shell + unchanged forecast subject → current is work_order only", () => { const body = [ "Fw: \u8f6c\u53d1\uff1a\u65b0\u589e\u9884\u62a5136\uff1a\u65b0\u8fb0\u6cfd+WHLC027G597465+WHSU8127240", "\u53d1\u4ef6\u4eba75440098<75440098@qq.com>", "\u5927\u5c0f 1.4M", "\u53d1\u81ea\u6211\u7684iPhone", "------------------ \u539f\u59cb\u90ae\u4ef6 ------------------", "---- \u8f6c\u53d1\u7684\u90ae\u4ef6 ----", "Dear,", "\u539f\u7bb1\u53f7YT2604021091=76\u4ef6\u64cd\u4f5c\u6307\u4ee4", "\u6362\u6807\u540e\u5355\u53f7\uff1aFBA19HW52S0L=76\u4ef6", "1\uff1a\u8986\u76d6\u8d34FBA\u6807\u7b7e", "2\uff1a\u8d34\u597d\u62cd\u7167\u56de\u4f20", "\u5728 2026-05-09 10:52:25 \uff0c \"luo\" \u5199\u9053\uff1a", "Dear,", "\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5\uff1a \u65b0\u8fb0\u6cfd +WHLC027G597465", ].join("\n"); const units = extractMailInstructions({ subject: SUBJECT, body, filenames: [], }); const current = units.filter((u) => u.isCurrent); expect(current.length).toBeGreaterThan(0); expect(current.every((u) => u.uiKind === "work_order")).toBe(true); expect(units.some((u) => u.isCurrent && u.uiKind === "forecast")).toBe( false, ); }); it("with DO.pdf still emits do_upload", () => { const units = extractMailInstructions({ subject: SUBJECT, body: loadMail4Body(), filenames: ["WHSU8127240-DO.pdf"], }); expect(units.some((u) => u.uiKind === "do_upload")).toBe(true); }); it("mail85 gold: time rounds kept; ACK/reject/accept shown; ==== not a cut", () => { const body = [ "Dear,", "\u539f\u7bb1\u53f7YT2604021091=76\u4ef6\u64cd\u4f5c\u6307\u4ee4", "\u6362\u6807\u540e\u5355\u53f7\uff1aFBA19HW52S0L=76\u4ef6", "1\uff1a\u8986\u76d6\u8d34FBA\u6807\u7b7e", "\u5728 2026-05-25 13:45:03 \uff0c \"\u65b0\u8fb0\u6cfd\" \u5199\u9053\uff1a", "Dear,", "WHSU8127240= \u539f\u7bb1\u53f7 \u6362\u6807 833\u4ef6", "\u5728 2026-05-20 15:55:44 \uff0c \"\u65b0\u8fb0\u6cfd\" \u5199\u9053\uff1a", "Dear,", "\u6362\u6807 167\u4ef6\u64cd\u4f5c\u6307\u4ee4", "\u5728 2026-05-18 17:17:17 \uff0c \"\u65b0\u8fb0\u6cfd\" \u5199\u9053\uff1a", "Dear,", "\u6362\u6807 334\u4ef6\u64cd\u4f5c\u6307\u4ee4", "FBA19DCCNYX0=167\u4ef6", "============================", "FBA19DC7FMPX=28\u4ef6", "\u5728 2026-05-09 18:00:16 \uff0c \"luo\" \u5199\u9053\uff1a", "\u597d\u7684", "Original:", "Dear,", "\u6309\u7167\u6b63\u5e38\u9884\u62a5\u63a5\u53d7\u54c8\uff0c\u5e2e\u5fd9\u5b89\u6392\u6d3e\u5355", "\u5728 2026-05-09 10:52:25 \uff0c \"luo\" \u5199\u9053\uff1a", "\u62b1\u6b49 \u4ed3\u5e93\u4ea7\u80fd\u6709\u9650 \u8be5\u67dc\u5b50\u6211\u4eec\u76ee\u524d\u65e0\u6cd5\u63a5\u6536", "Original:", "Dear,", "\u8bf7\u67e5\u6536\u65b0\u589e\u9884\u62a5\uff1a \u65b0\u8fb0\u6cfd +WHLC027G597465", ].join("\n"); const units = extractMailInstructions({ subject: SUBJECT, body, filenames: [], }); const wo = units.filter((u) => u.uiKind === "work_order"); expect(wo.length).toBe(4); expect(wo.filter((u) => /334/.test(u.segmentText)).length).toBe(1); expect(units.some((u) => u.roundAt?.includes("2026-05-25"))).toBe(true); expect(units.some((u) => u.roundAt?.includes("2026-05-20"))).toBe(true); expect(units.some((u) => u.roundAt?.includes("2026-05-18"))).toBe(true); expect( units.some( (u) => u.uiKind === "customer_instruction" && /好的/.test(u.segmentText), ), ).toBe(true); expect( units.some( (u) => u.uiKind === "customer_instruction" && /无法接收/.test(u.segmentText), ), ).toBe(true); expect( units.some( (u) => u.uiKind === "customer_instruction" && /安排派单|正常预报接受/.test(u.segmentText), ), ).toBe(true); expect(units.some((u) => u.uiKind === "forecast")).toBe(true); }); it("soft \u66f4\u65b0\u6d3e\u9001\u5355 maps to customer_instruction not work_order", () => { const units = extractMailInstructions({ subject: "MATU2745683", body: "\u66f4\u65b0\u6d3e\u9001\u5355\uff0c\u8bf7\u67e5\u6536", filenames: [], }); expect(units.some((u) => u.uiKind === "customer_instruction")).toBe(true); expect(units.some((u) => u.uiKind === "work_order")).toBe(false); }); });