|
|
import { describe, expect, it } from "vitest";
|
|
|
import { matchSenderCustomer } from "@/services/customer/resolve-sender";
|
|
|
import {
|
|
|
isRecentlyCreated,
|
|
|
TIMEOUT_VERIFY_RECENT_MS,
|
|
|
} from "@/services/import/compensation";
|
|
|
import { isPastRetention } from "@/services/retention/cleanup";
|
|
|
import {
|
|
|
assertTransition,
|
|
|
canTransition,
|
|
|
} from "@/services/state-machine";
|
|
|
import { classifyMail } from "@/services/parse/classify";
|
|
|
import { mapChannel } from "@/services/parse/channel-map";
|
|
|
import { isValidContainerNo } from "@/utils/iso6346";
|
|
|
|
|
|
/**
|
|
|
* Resilience A-* 可测采样(docx/异常场景与系统韧性设计 §12)
|
|
|
* 依赖 DB/HTTP 的链路见 force-import UI + worker compensation/retention。
|
|
|
*/
|
|
|
describe("resilience A-*", () => {
|
|
|
describe("A-1 input", () => {
|
|
|
it("A-1.1 empty subject continues classify", () => {
|
|
|
const r = classifyMail({ subject: "", body: "转仓", filenames: [] });
|
|
|
// 无主题不阻断;正文「转仓」现行口径为 TRANSFER
|
|
|
expect(["TRANSFER", "WORK_ORDER"]).toContain(r.mail_type);
|
|
|
});
|
|
|
|
|
|
it("A-1.6 invalid container format", () => {
|
|
|
expect(isValidContainerNo("BAD")).toBe(false);
|
|
|
});
|
|
|
|
|
|
it("A-1.9 unmapped channel still returns transporter", () => {
|
|
|
const m = mapChannel("神秘渠道XYZ");
|
|
|
expect(m.unmapped).toBe(true);
|
|
|
expect(m.transporter).toBeTruthy();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("A-3 timeout verify window", () => {
|
|
|
it("A-3.x recent createDate within 10min is recently created", () => {
|
|
|
const now = Date.now();
|
|
|
expect(
|
|
|
isRecentlyCreated(new Date(now - 60_000).toISOString(), now),
|
|
|
).toBe(true);
|
|
|
expect(
|
|
|
isRecentlyCreated(
|
|
|
new Date(now - TIMEOUT_VERIFY_RECENT_MS - 1).toISOString(),
|
|
|
now,
|
|
|
),
|
|
|
).toBe(false);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("A-5 / A-6 state machine", () => {
|
|
|
it("A-5.8 reparse allowed from PARSE_FAILED", () => {
|
|
|
expect(canTransition("PARSE_FAILED", "PARSING")).toBe(true);
|
|
|
expect(canTransition("SUCCESS", "PARSING")).toBe(false);
|
|
|
});
|
|
|
|
|
|
it("A-6.6 illegal transition rejected", () => {
|
|
|
expect(canTransition("SUCCESS", "PENDING_CONFIRM")).toBe(false);
|
|
|
expect(() => assertTransition("IMPORTING", "PARSED")).toThrow();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("retention + sender_customer_map", () => {
|
|
|
it("DATA_RETENTION cutoff marks old mail as past", () => {
|
|
|
const now = new Date("2026-07-22T00:00:00Z");
|
|
|
const old = new Date("2026-06-01T00:00:00Z");
|
|
|
expect(isPastRetention(old, old, 30, now)).toBe(true);
|
|
|
expect(isPastRetention(now, now, 30, now)).toBe(false);
|
|
|
});
|
|
|
|
|
|
it("sender map EMAIL beats DOMAIN", () => {
|
|
|
const hit = matchSenderCustomer(
|
|
|
{ fromAddr: "demo-customer@example.com" },
|
|
|
[
|
|
|
{
|
|
|
matchType: "DOMAIN",
|
|
|
matchValue: "example.com",
|
|
|
customerCode: "DOM",
|
|
|
},
|
|
|
{
|
|
|
matchType: "EMAIL",
|
|
|
matchValue: "demo-customer@example.com",
|
|
|
customerCode: "MAIL",
|
|
|
},
|
|
|
],
|
|
|
);
|
|
|
expect(hit?.customerCode).toBe("MAIL");
|
|
|
});
|
|
|
|
|
|
it("sender map DOMAIN match", () => {
|
|
|
const hit = matchSenderCustomer(
|
|
|
{ fromAddr: "ops@partner.example.com" },
|
|
|
[
|
|
|
{
|
|
|
matchType: "DOMAIN",
|
|
|
matchValue: "partner.example.com",
|
|
|
customerCode: "PARTNER01",
|
|
|
},
|
|
|
],
|
|
|
);
|
|
|
expect(hit?.customerCode).toBe("PARTNER01");
|
|
|
});
|
|
|
});
|
|
|
});
|