fix: update image URLs to use httpbin.org for testing and add new test cases for attachment analysis

This commit is contained in:
MythEclipse
2026-05-19 00:07:31 +07:00
parent 0ffb0f213a
commit a338d6b7e2
+709 -12
View File
@@ -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 = `
<thinking>
Let me analyze these messages carefully.
Message 1 seems clean.
Message 2 has some concerning language.
</thinking>
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);
});
});
});