When inbuxa-server's AI spam classification is on, it records the model's answer in an X-Spam-LLM header: a tag (LLM_<category>[_<confidence>]) and, in parentheses, the model's explanation. The full message now asks for it, and where it's there: - the message details show "Language model's opinion" beside the spam filter's own working, with category, confidence and explanation; - a message in Junk carries a banner saying the same. Both say it's one of several signals the spam filter weighed, never the reason on its own, as the server's spec requires. The explanation is model output and is only ever rendered as text. Nothing shows without the header, so a server without the feature, or with it off, looks as before. Translations: two new strings, "Language model's opinion" and "One of several signals the spam filter weighed", in all eight catalogues (16 entries). Category and confidence come from the server and aren't translated.
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { LLM_HEADER_PROP, llmOpinion, parseLlmOpinion } from "@/lib/llmOpinion";
|
|
|
|
/*
|
|
* The header as inbuxa-server writes it (crates/features/src/ai/answer.rs):
|
|
* `X-Spam-LLM: <TAG>`, optionally followed by the explanation in one pair of
|
|
* parentheses, folded at 78 columns.
|
|
*/
|
|
describe("parseLlmOpinion", () => {
|
|
it("reads category, confidence and explanation", () => {
|
|
expect(parseLlmOpinion("LLM_UNSOLICITED_HIGH (Promotes a product the reader never asked about)")).toEqual({
|
|
tag: "LLM_UNSOLICITED_HIGH",
|
|
category: "Unsolicited",
|
|
confidence: "High",
|
|
explanation: "Promotes a product the reader never asked about",
|
|
});
|
|
});
|
|
|
|
it("reads a tag with no confidence and no explanation", () => {
|
|
expect(parseLlmOpinion("LLM_LEGITIMATE")).toEqual({
|
|
tag: "LLM_LEGITIMATE",
|
|
category: "Legitimate",
|
|
confidence: null,
|
|
explanation: null,
|
|
});
|
|
});
|
|
|
|
it("keeps an operator's multi-word category whole", () => {
|
|
const o = parseLlmOpinion("LLM_COLD_OUTREACH_MEDIUM");
|
|
expect(o?.category).toBe("Cold outreach");
|
|
expect(o?.confidence).toBe("Medium");
|
|
// An unknown last word is part of the category, not a confidence.
|
|
expect(parseLlmOpinion("LLM_COLD_OUTREACH")?.category).toBe("Cold outreach");
|
|
expect(parseLlmOpinion("LLM_COLD_OUTREACH")?.confidence).toBeNull();
|
|
});
|
|
|
|
it("unfolds a folded header and keeps inner parentheses", () => {
|
|
const o = parseLlmOpinion("LLM_HARMFUL_LOW (Asks for a password\r\n (urgently) via a link)");
|
|
expect(o?.explanation).toBe("Asks for a password (urgently) via a link");
|
|
});
|
|
|
|
it("returns null for anything that isn't the server's tag", () => {
|
|
for (const raw of [null, undefined, "", " ", "Yes, score=6.7", "LLM_", "llm_unsolicited_high", "X LLM_SPAM"]) {
|
|
expect(parseLlmOpinion(raw), String(raw)).toBeNull();
|
|
}
|
|
});
|
|
|
|
it("reads the JMAP property a full message carries", () => {
|
|
expect(llmOpinion({ [LLM_HEADER_PROP]: "LLM_LEGITIMATE_HIGH" })?.category).toBe("Legitimate");
|
|
expect(llmOpinion({})).toBeNull();
|
|
});
|
|
});
|