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.

209 lines
8.4 KiB

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\" <a@b.com> \u5199\u9053\uff1a",
"Dear,",
"WHSU8127240= \u539f\u7bb1\u53f7 \u64cd\u4f5c\u6307\u4ee4 \u6362\u6807",
"\u5728 2026-05-09 18:00:16 \uff0c \"luo\" <luo@x.com> \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\" <luo@x.com> \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\" <luo@x.com> \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\" <a@b.com> \u5199\u9053\uff1a",
"Dear,",
"WHSU8127240= \u539f\u7bb1\u53f7 \u6362\u6807 833\u4ef6",
"\u5728 2026-05-20 15:55:44 \uff0c \"\u65b0\u8fb0\u6cfd\" <a@b.com> \u5199\u9053\uff1a",
"Dear,",
"\u6362\u6807 167\u4ef6\u64cd\u4f5c\u6307\u4ee4",
"\u5728 2026-05-18 17:17:17 \uff0c \"\u65b0\u8fb0\u6cfd\" <a@b.com> \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\" <luo@x.com> \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\" <luo@x.com> \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);
});
});