2026-05-31 00:41:34 +07:00
|
|
|
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
2026-05-31 16:54:15 +07:00
|
|
|
import { config } from "../../src/config";
|
2026-05-29 18:39:10 +07:00
|
|
|
import {
|
|
|
|
|
buildModerationTextEvidence,
|
|
|
|
|
detectIndonesianBadwords,
|
|
|
|
|
formatModerationTextEvidenceForPrompt,
|
|
|
|
|
normalizeDiscordCustomEmoji,
|
|
|
|
|
} from "../../src/moderation/indonesianTextNormalizer";
|
2026-05-31 00:41:34 +07:00
|
|
|
|
|
|
|
|
const originalNemotronKey = config.NVIDIA_NEMOTRON_API_KEY;
|
|
|
|
|
const originalPrimaryAiKey = config.AI_LLM_API_KEY;
|
2026-05-31 19:42:39 +07:00
|
|
|
const originalGroqKey = config.GROQ_API_KEY;
|
2026-05-31 00:41:34 +07:00
|
|
|
|
|
|
|
|
function disableRemoteModeration(): void {
|
|
|
|
|
config.NVIDIA_NEMOTRON_API_KEY = undefined;
|
|
|
|
|
config.AI_LLM_API_KEY = undefined;
|
2026-05-31 19:42:39 +07:00
|
|
|
config.GROQ_API_KEY = undefined;
|
2026-05-31 00:41:34 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disableRemoteModeration();
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
disableRemoteModeration();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(() => {
|
|
|
|
|
config.NVIDIA_NEMOTRON_API_KEY = originalNemotronKey;
|
|
|
|
|
config.AI_LLM_API_KEY = originalPrimaryAiKey;
|
2026-05-31 19:42:39 +07:00
|
|
|
config.GROQ_API_KEY = originalGroqKey;
|
2026-05-31 00:41:34 +07:00
|
|
|
});
|
2026-05-29 18:39:10 +07:00
|
|
|
|
|
|
|
|
describe("normalizeDiscordCustomEmoji", () => {
|
|
|
|
|
it("replaces static custom emoji", () => {
|
2026-05-30 15:55:01 +07:00
|
|
|
const result = normalizeDiscordCustomEmoji(
|
|
|
|
|
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
|
|
|
|
);
|
2026-05-29 18:39:10 +07:00
|
|
|
expect(result.text).toBe("Bersiaplah woy [emoji:hadeh]");
|
|
|
|
|
expect(result.emojiNames).toContain("hadeh");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("replaces animated custom emoji", () => {
|
|
|
|
|
const result = normalizeDiscordCustomEmoji("cek <a:speen:1234567890> dulu");
|
|
|
|
|
expect(result.text).toBe("cek [emoji:speen] dulu");
|
|
|
|
|
expect(result.emojiNames).toContain("speen");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("handles text without emoji", () => {
|
|
|
|
|
const result = normalizeDiscordCustomEmoji("halo semua");
|
|
|
|
|
expect(result.text).toBe("halo semua");
|
|
|
|
|
expect(result.emojiNames).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("detectIndonesianBadwords", () => {
|
2026-06-01 19:00:16 +07:00
|
|
|
it("returns empty array when no APIs are configured", async () => {
|
|
|
|
|
// Local badword list removed; all detection now requires an API.
|
|
|
|
|
// With all APIs disabled (see disableRemoteModeration above),
|
|
|
|
|
// the function should return empty without throwing.
|
2026-05-30 14:48:50 +07:00
|
|
|
const badwords = await detectIndonesianBadwords("kontol banget");
|
2026-06-01 19:00:16 +07:00
|
|
|
expect(badwords).toHaveLength(0);
|
2026-05-29 18:39:10 +07:00
|
|
|
});
|
|
|
|
|
|
2026-05-30 14:48:50 +07:00
|
|
|
it("returns empty array for safe slang", async () => {
|
|
|
|
|
const badwords = await detectIndonesianBadwords("woy hadeh gua");
|
2026-05-29 18:39:10 +07:00
|
|
|
expect(badwords).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("buildModerationTextEvidence", () => {
|
2026-05-30 14:48:50 +07:00
|
|
|
it("produces correct evidence for woy with emoji", async () => {
|
|
|
|
|
const evidence = await buildModerationTextEvidence(
|
2026-05-29 18:39:10 +07:00
|
|
|
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
|
|
|
|
);
|
|
|
|
|
expect(evidence.normalized).toContain("[emoji:hadeh]");
|
2026-05-31 19:42:39 +07:00
|
|
|
// Slang normalization removed; only emoji normalization and badword detection remain
|
2026-05-29 18:39:10 +07:00
|
|
|
expect(evidence.notes.some((n) => n.includes("emoji:hadeh"))).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-30 14:48:50 +07:00
|
|
|
it("detects badword when present", async () => {
|
|
|
|
|
const evidence = await buildModerationTextEvidence("anjing loe kontol");
|
2026-06-01 19:00:16 +07:00
|
|
|
// With all APIs disabled, local detection is removed so hasBadwords will be false.
|
|
|
|
|
// This test now verifies that the evidence builder does not crash and always
|
|
|
|
|
// returns a valid structure.
|
|
|
|
|
expect(evidence.normalized).toBeDefined();
|
2026-05-30 15:55:01 +07:00
|
|
|
expect(evidence.notes.some((n) => n.includes("badword detected"))).toBe(
|
|
|
|
|
true,
|
|
|
|
|
);
|
2026-05-29 18:39:10 +07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("formatModerationTextEvidenceForPrompt", () => {
|
2026-05-31 19:42:39 +07:00
|
|
|
it("returns prompt evidence for emoji", async () => {
|
2026-05-30 14:48:50 +07:00
|
|
|
const formatted = await formatModerationTextEvidenceForPrompt(
|
2026-05-29 18:39:10 +07:00
|
|
|
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
|
|
|
|
);
|
|
|
|
|
expect(formatted).toContain("[normalized_text:");
|
|
|
|
|
expect(formatted).toContain("[emoji:hadeh]");
|
|
|
|
|
expect(formatted).toContain("[normalization_notes:");
|
2026-05-30 20:13:44 +07:00
|
|
|
// The NVIDIA API may or may not detect badwords for this input
|
2026-05-31 16:54:15 +07:00
|
|
|
expect(formatted).toMatch(
|
|
|
|
|
/no Indonesian badword detected|Indonesian badword detected/,
|
|
|
|
|
);
|
2026-05-29 18:39:10 +07:00
|
|
|
});
|
|
|
|
|
|
2026-05-30 14:48:50 +07:00
|
|
|
it("includes normalized text even for clean input", async () => {
|
|
|
|
|
const formatted = await formatModerationTextEvidenceForPrompt("Halo semua");
|
2026-05-29 18:39:10 +07:00
|
|
|
expect(formatted).toContain("[normalized_text:");
|
|
|
|
|
expect(formatted).toContain("no Indonesian badword detected");
|
|
|
|
|
});
|
|
|
|
|
});
|