From a338d6b7e27244f720a5041bd86855c6848eaa23 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Tue, 19 May 2026 00:07:31 +0700 Subject: [PATCH] fix: update image URLs to use httpbin.org for testing and add new test cases for attachment analysis --- tests/moderation/llmModerationClient.test.ts | 721 ++++++++++++++++++- 1 file changed, 709 insertions(+), 12 deletions(-) diff --git a/tests/moderation/llmModerationClient.test.ts b/tests/moderation/llmModerationClient.test.ts index 46c1244..a0afe46 100644 --- a/tests/moderation/llmModerationClient.test.ts +++ b/tests/moderation/llmModerationClient.test.ts @@ -426,7 +426,7 @@ describe("runModerationAnalysis", () => { }; global.fetch = vi.fn().mockImplementation((url: string) => { - if (url.includes("picser.tech") || url.includes("discord.com")) { + if (url === "https://httpbin.org/image/png") { return Promise.resolve({ ok: true, arrayBuffer: async () => { @@ -455,8 +455,8 @@ describe("runModerationAnalysis", () => { filename: "test.png", size: 500, type: "image/png", - discord_url: "https://discord.com/attachment.png", - uploaded_url: "https://picser.tech/test.png", + discord_url: "https://httpbin.org/image/png", + uploaded_url: "https://httpbin.org/image/png", upload_status: "uploaded" as const, upload_error: null, created_at: Date.now(), @@ -477,7 +477,7 @@ describe("runModerationAnalysis", () => { expect(fetchCalls.length).toBe(2); // Verify 1st call (image download) - expect(fetchCalls[0][0]).toBe("https://picser.tech/test.png"); + expect(fetchCalls[0][0]).toBe("https://httpbin.org/image/png"); // Verify 2nd call (chat completions API) const [, completionsOptions] = fetchCalls[1]; @@ -522,7 +522,7 @@ describe("runModerationAnalysis", () => { }; global.fetch = vi.fn().mockImplementation((url: string) => { - if (url.includes("picser.tech") || url.includes("discord.com")) { + if (url.startsWith("https://httpbin.org/image/")) { return Promise.resolve({ ok: true, arrayBuffer: async () => { @@ -555,8 +555,8 @@ describe("runModerationAnalysis", () => { filename: `${id}.png`, size: 500, type: "image/png", - discord_url: `https://discord.com/${id}.png`, - uploaded_url: `https://picser.tech/${id}.png`, + discord_url: `https://httpbin.org/image/png?source=${id}`, + uploaded_url: `https://httpbin.org/image/png?source=${id}`, upload_status: "uploaded" as const, upload_error: null, created_at: createdAt, @@ -592,10 +592,707 @@ describe("runModerationAnalysis", () => { // Excluded: c2 (200), c1 (100) const downloadedUrls = fetchCalls.slice(0, 8).map((call: any) => call[0]); - expect(downloadedUrls).toContain("https://picser.tech/t3.png"); - expect(downloadedUrls).toContain("https://picser.tech/t2.png"); - expect(downloadedUrls).toContain("https://picser.tech/t1.png"); - expect(downloadedUrls).toContain("https://picser.tech/c7.png"); - expect(downloadedUrls).not.toContain("https://picser.tech/c1.png"); + expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t3"); + expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t2"); + expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t1"); + expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=c7"); + expect(downloadedUrls).not.toContain("https://httpbin.org/image/png?source=c1"); + }); + + it("sends verified real PNG and JPEG attachments with realistic Indonesian text", async () => { + const mockResponse = { + choices: [ + { + message: { + content: JSON.stringify({ + results: [ + { + message_id: "m1", + status: "warn", + flags: ["harassment"], + score: 0.65, + analysis: "Teks dan gambar perlu ditinjau moderator.", + }, + ], + }), + }, + }, + ], + }; + + const imageBytes = Buffer.from("realistic-image-bytes"); + global.fetch = vi.fn().mockImplementation((url: string) => { + if ( + url === "https://httpbin.org/image/png" || + url === "https://httpbin.org/image/jpeg" + ) { + return Promise.resolve({ + ok: true, + arrayBuffer: async () => + imageBytes.buffer.slice( + imageBytes.byteOffset, + imageBytes.byteOffset + imageBytes.byteLength, + ), + }); + } + + return Promise.resolve({ + ok: true, + text: async () => JSON.stringify(mockResponse), + json: async () => mockResponse, + }); + }); + + const attachments = [ + { + id: "png1", + message_id: "m1", + guild_id: "guild123", + channel_id: "channel123", + thread_id: null, + user_id: "user123", + filename: "bukti-chat.png", + size: 8090, + type: "image/png", + discord_url: "https://httpbin.org/image/png", + uploaded_url: "https://httpbin.org/image/png", + upload_status: "uploaded" as const, + upload_error: null, + created_at: Date.now(), + uploaded_at: Date.now(), + }, + { + id: "jpeg1", + message_id: "m1", + guild_id: "guild123", + channel_id: "channel123", + thread_id: null, + user_id: "user123", + filename: "screenshot.jpeg", + size: 35588, + type: "image/jpeg", + discord_url: "https://httpbin.org/image/jpeg", + uploaded_url: "https://httpbin.org/image/jpeg", + upload_status: "uploaded" as const, + upload_error: null, + created_at: Date.now() + 1, + uploaded_at: Date.now() + 1, + }, + ]; + + const result = await runModerationAnalysis({ + targets: [ + createMessageRecord({ + id: "m1", + username: "asep", + content: + "tolong cek gambar ini, dia kirim link mencurigakan https://example.invalid/login dan maksa orang klik", + }), + ], + contextText: + "Sebelumnya user lain bilang link itu mirip phishing dan screenshot memperlihatkan halaman login palsu.", + attachments, + }); + + expect(result.results[0].status).toBe("warn"); + + const fetchCalls = (global.fetch as any).mock.calls; + expect(fetchCalls[0][0]).toBe("https://httpbin.org/image/jpeg"); + expect(fetchCalls[1][0]).toBe("https://httpbin.org/image/png"); + + const requestBody = JSON.parse(fetchCalls[2][1].body); + const contentParts = requestBody.messages[0].content; + expect(contentParts.filter((part: any) => part.type === "image_url")).toHaveLength(2); + expect(contentParts[0].image_url.url).toContain("data:image/jpeg;base64,"); + expect(contentParts[2].image_url.url).toContain("data:image/png;base64,"); + expect(contentParts.at(-1).text).toContain("https://example.invalid/login"); + expect(contentParts.at(-1).text).toContain("Sebelumnya user lain bilang"); + }); + + it("falls back to discord_url when uploaded_url is not ready", async () => { + const mockResponse = { + choices: [ + { + message: { + content: JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "Image available through Discord URL fallback.", + }, + ], + }), + }, + }, + ], + }; + + global.fetch = vi.fn().mockImplementation((url: string) => { + if (url === "https://httpbin.org/image/png") { + const buffer = Buffer.from("png-bytes"); + return Promise.resolve({ + ok: true, + arrayBuffer: async () => + buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), + }); + } + + return Promise.resolve({ + ok: true, + text: async () => JSON.stringify(mockResponse), + json: async () => mockResponse, + }); + }); + + await runModerationAnalysis({ + targets: [createMessageRecord({ id: "m1", content: "gambar belum selesai upload ke picser" })], + contextText: "test context", + attachments: [ + { + id: "pending-image", + message_id: "m1", + guild_id: "guild123", + channel_id: "channel123", + thread_id: null, + user_id: "user123", + filename: "pending.png", + size: 8090, + type: "image/png", + discord_url: "https://httpbin.org/image/png", + uploaded_url: null, + upload_status: "pending" as const, + upload_error: null, + created_at: Date.now(), + uploaded_at: null, + }, + ], + }); + + expect((global.fetch as any).mock.calls[0][0]).toBe("https://httpbin.org/image/png"); + }); + + it("keeps analyzing text when an image URL returns non-OK", async () => { + const mockResponse = { + choices: [ + { + message: { + content: JSON.stringify({ + results: [ + { + message_id: "m1", + status: "warn", + flags: ["suspicious_link"], + score: 0.6, + analysis: "Image fetch failed, text still indicates suspicious link.", + }, + ], + }), + }, + }, + ], + }; + + global.fetch = vi.fn().mockImplementation((url: string) => { + if (url === "https://httpbin.org/image/png") { + return Promise.resolve({ ok: false, status: 503 }); + } + + return Promise.resolve({ + ok: true, + text: async () => JSON.stringify(mockResponse), + json: async () => mockResponse, + }); + }); + + const result = await runModerationAnalysis({ + targets: [ + createMessageRecord({ + id: "m1", + content: "cek bonus gratis di https://example.invalid/claim sekarang", + }), + ], + contextText: "Pesan ini dikirim berulang setelah user lain menolak klik link.", + attachments: [ + { + id: "bad-image", + message_id: "m1", + guild_id: "guild123", + channel_id: "channel123", + thread_id: null, + user_id: "user123", + filename: "broken.png", + size: 8090, + type: "image/png", + discord_url: "https://httpbin.org/image/png", + uploaded_url: "https://httpbin.org/image/png", + upload_status: "uploaded" as const, + upload_error: null, + created_at: Date.now(), + uploaded_at: Date.now(), + }, + ], + }); + + expect(result.results[0].status).toBe("warn"); + + const requestBody = JSON.parse((global.fetch as any).mock.calls[1][1].body); + expect(requestBody.messages[0].content).toHaveLength(1); + expect(requestBody.messages[0].content[0].text).toContain("https://example.invalid/claim"); + }); + + describe("Edge Cases & Real-World Scenarios", () => { + it("handles single result object without results wrapper", () => { + const content = JSON.stringify({ + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "OK", + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].messageId).toBe("m1"); + expect(result[0].status).toBe("clean"); + }); + + it("handles alternative array key 'result' instead of 'results'", () => { + const content = JSON.stringify({ + result: [ + { + message_id: "m1", + status: "flagged", + flags: ["spam"], + score: 0.8, + analysis: "Spam detected", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].status).toBe("flagged"); + }); + + it("handles alternative array key 'data' instead of 'results'", () => { + const content = JSON.stringify({ + data: [ + { + message_id: "m1", + status: "warn", + flags: ["hate_speech"], + score: 0.6, + analysis: "Potential hate speech", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].status).toBe("warn"); + }); + + it("handles alternative array key 'messages' instead of 'results'", () => { + const content = JSON.stringify({ + messages: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.05, + analysis: "No violations", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].status).toBe("clean"); + }); + + it("handles text with links and URLs", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "Link shared, no violations", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].status).toBe("clean"); + }); + + it("handles very long message content", () => { + const longContent = "a".repeat(10000); + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: `Analyzed ${longContent.length} character message`, + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].analysis).toContain("10000"); + }); + + it("handles multiple messages with mixed statuses", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "OK", + }, + { + message_id: "m2", + status: "warn", + flags: ["spam"], + score: 0.5, + analysis: "Potential spam", + }, + { + message_id: "m3", + status: "flagged", + flags: ["hate_speech", "abuse"], + score: 0.95, + analysis: "Severe violations", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1", "m2", "m3"]); + expect(result).toHaveLength(3); + expect(result[0].status).toBe("clean"); + expect(result[1].status).toBe("warn"); + expect(result[2].status).toBe("flagged"); + expect(result[2].flags).toContain("hate_speech"); + expect(result[2].flags).toContain("abuse"); + }); + + it("handles LLM response with thinking blocks and markdown", () => { + const content = ` + + Let me analyze these messages carefully. + Message 1 seems clean. + Message 2 has some concerning language. + + + Based on my analysis: + + \`\`\`json + { + "results": [ + { + "message_id": "m1", + "status": "clean", + "flags": [], + "score": 0.1, + "analysis": "No violations detected" + }, + { + "message_id": "m2", + "status": "warn", + "flags": ["inappropriate"], + "score": 0.6, + "analysis": "Contains inappropriate language" + } + ] + } + \`\`\` + + These are my findings. + `; + + const result = parseModerationResponse(content, ["m1", "m2"]); + expect(result).toHaveLength(2); + expect(result[0].status).toBe("clean"); + expect(result[1].status).toBe("warn"); + }); + + it("handles response with extra fields and nested objects", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "flagged", + flags: ["violence"], + score: 0.9, + analysis: "Contains violent content", + metadata: { + confidence: 0.95, + model_version: "v2", + processing_time_ms: 150, + }, + extra_field: "ignored", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].status).toBe("flagged"); + expect(result[0].score).toBe(0.9); + }); + + it("handles scientific notation in score", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 1e-1, + analysis: "Very low risk", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].score).toBe(0.1); + }); + + it("handles empty flags array and null analysis", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0, + analysis: null, + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].flags).toEqual([]); + expect(result[0].analysis).toBe(""); + }); + + it("handles missing optional fields with defaults", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + score: 0.1, + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].flags).toEqual([]); + expect(result[0].score).toBe(0.1); + expect(result[0].analysis).toBe(""); + }); + + it("handles Unicode and emoji in analysis text", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "warn", + flags: ["inappropriate"], + score: 0.7, + analysis: "Contains inappropriate emoji 🚫 and symbols ⚠️", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].analysis).toContain("🚫"); + expect(result[0].analysis).toContain("⚠️"); + }); + + it("handles response with only one array in deeply nested object", () => { + const content = JSON.stringify({ + metadata: { version: 1 }, + analysis_results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "OK", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result).toHaveLength(1); + expect(result[0].messageId).toBe("m1"); + }); + }); + + describe("Real-World Text Samples", () => { + it("analyzes message with URL", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.15, + analysis: "Message contains URL but no violations", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].status).toBe("clean"); + }); + + it("analyzes message with mentions and hashtags", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.05, + analysis: "Mentions and hashtags are acceptable", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].status).toBe("clean"); + }); + + it("analyzes message with code snippets", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "Code snippet detected, no violations", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].status).toBe("clean"); + }); + + it("analyzes message with mixed languages", () => { + const content = JSON.stringify({ + results: [ + { + message_id: "m1", + status: "clean", + flags: [], + score: 0.1, + analysis: "Mixed language content analyzed successfully", + }, + ], + }); + + const result = parseModerationResponse(content, ["m1"]); + expect(result[0].status).toBe("clean"); + }); + }); + + describe("Failure Scenarios", () => { + it("throws on completely invalid JSON", () => { + expect(() => + parseModerationResponse("not json at all", ["m1"]), + ).toThrow(); + }); + + it("throws on empty object", () => { + expect(() => parseModerationResponse(JSON.stringify({}), ["m1"])).toThrow( + /missing.*results/i, + ); + }); + + it("throws on array without results wrapper and no message_id", () => { + expect(() => + parseModerationResponse( + JSON.stringify([ + { + status: "clean", + flags: [], + score: 0.1, + analysis: "OK", + }, + ]), + ["m1"], + ), + ).toThrow(); + }); + + it("throws on mismatched message IDs", () => { + expect(() => + parseModerationResponse( + JSON.stringify({ + results: [ + { + message_id: "m999", + status: "clean", + flags: [], + score: 0.1, + analysis: "OK", + }, + ], + }), + ["m1"], + ), + ).toThrow(/unknown.*message_id/i); + }); + + it("throws on invalid status value", () => { + expect(() => + parseModerationResponse( + JSON.stringify({ + results: [ + { + message_id: "m1", + status: "invalid_status", + flags: [], + score: 0.5, + analysis: "OK", + }, + ], + }), + ["m1"], + ), + ).toThrow(/invalid status/i); + }); + + it("throws on non-finite score", () => { + const content = `{ + "results": [ + { + "message_id": "m1", + "status": "clean", + "flags": [], + "score": "NaN", + "analysis": "OK" + } + ] + }`; + + expect(() => + parseModerationResponse(content, ["m1"]), + ).toThrow(/finite/i); + }); }); });