feat: enhance logging and moderation response handling with improved serialization and error management
This commit is contained in:
@@ -66,15 +66,13 @@ describe("parseModerationResponse", () => {
|
||||
["m1"],
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
messageId: "m1",
|
||||
status: "warn",
|
||||
flags: ["provokasi"],
|
||||
score: 0.7,
|
||||
analysis: "Perlu peringatan.",
|
||||
},
|
||||
]);
|
||||
expect(result[0]).toMatchObject({
|
||||
messageId: "m1",
|
||||
status: "warn",
|
||||
flags: ["provokasi"],
|
||||
score: 0.7,
|
||||
analysis: "Perlu peringatan.",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects deferral analysis text", () => {
|
||||
@@ -232,7 +230,7 @@ describe("parseModerationResponse", () => {
|
||||
}),
|
||||
["m1"],
|
||||
),
|
||||
).toThrow(/null or undefined/i);
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects undefined score", () => {
|
||||
@@ -250,7 +248,7 @@ describe("parseModerationResponse", () => {
|
||||
}),
|
||||
["m1"],
|
||||
),
|
||||
).toThrow(/null or undefined/i);
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects duplicate message_id", () => {
|
||||
@@ -295,7 +293,7 @@ describe("parseModerationResponse", () => {
|
||||
}),
|
||||
["m1"],
|
||||
),
|
||||
).toThrow(/invalid status/i);
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("clamps score to 0-1 range", () => {
|
||||
@@ -686,13 +684,13 @@ describe("runModerationAnalysis", () => {
|
||||
(global.fetch as any).mock.calls[1][1].body,
|
||||
);
|
||||
expect(secondRequestBody.messages[0].content).toContain(
|
||||
"Previous response failed validation",
|
||||
"RESPON SEBELUMNYA GAGAL VALIDASI",
|
||||
);
|
||||
expect(secondRequestBody.messages[0].content).toContain(
|
||||
"Invalid status: bad",
|
||||
"Invalid option",
|
||||
);
|
||||
expect(secondRequestBody.messages[0].content).toContain(
|
||||
"Retry with corrected output",
|
||||
"Coba lagi dengan output JSON yang benar",
|
||||
);
|
||||
expect(result.results[0].status).toBe("clean");
|
||||
});
|
||||
@@ -753,10 +751,14 @@ describe("runModerationAnalysis", () => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
arrayBuffer: async () => {
|
||||
const buffer = Buffer.from("fake-image-bytes");
|
||||
return buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength,
|
||||
// Minimal valid PNG bytes (8-byte signature)
|
||||
const png = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
]);
|
||||
return png.buffer.slice(
|
||||
png.byteOffset,
|
||||
png.byteOffset + png.byteLength,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -796,33 +798,25 @@ describe("runModerationAnalysis", () => {
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
|
||||
const fetchCalls = (global.fetch as any).mock.calls;
|
||||
// Should be called twice: 1st for image download, 2nd for API completions
|
||||
expect(fetchCalls.length).toBe(2);
|
||||
// 3 calls: 1st image download, 2nd separate media analysis (chat/completions), 3rd main batch
|
||||
expect(fetchCalls.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Verify 1st call (image download)
|
||||
expect(fetchCalls[0][0]).toBe("https://httpbin.org/image/png");
|
||||
|
||||
// Verify 2nd call (chat completions API)
|
||||
const [, completionsOptions] = fetchCalls[1];
|
||||
// The main batch call should be a text-only string (images analyzed separately)
|
||||
const mainBatchCall = fetchCalls[fetchCalls.length - 1];
|
||||
const [, completionsOptions] = mainBatchCall;
|
||||
const body = JSON.parse(completionsOptions.body);
|
||||
|
||||
expect(body.messages).toHaveLength(1);
|
||||
const userMessage = body.messages[0];
|
||||
expect(userMessage.role).toBe("user");
|
||||
expect(Array.isArray(userMessage.content)).toBe(true);
|
||||
expect(userMessage.content[0].type).toBe("image_url");
|
||||
expect(userMessage.content[0].image_url.url).toContain(
|
||||
"data:image/png;base64,",
|
||||
);
|
||||
expect(userMessage.content[1].type).toBe("text");
|
||||
expect(userMessage.content[1].text).toContain(
|
||||
"Image Attachment for Message ID: m1",
|
||||
);
|
||||
expect(userMessage.content[2].type).toBe("text");
|
||||
expect(userMessage.content[2].text).toContain("test context");
|
||||
expect(userMessage.content[2].text).toContain(
|
||||
"You are a content moderation assistant.",
|
||||
);
|
||||
// Now text-only: content is a string, not an array (images analyzed separately)
|
||||
expect(typeof userMessage.content).toBe("string");
|
||||
expect(userMessage.content).toContain("test context");
|
||||
// Verify the target message is present in the content
|
||||
expect(userMessage.content).toContain("id=m1");
|
||||
});
|
||||
|
||||
it("caps image attachments to 8 and prioritizes targets over context", async () => {
|
||||
@@ -938,7 +932,7 @@ describe("runModerationAnalysis", () => {
|
||||
status: "warn",
|
||||
flags: ["harassment"],
|
||||
score: 0.65,
|
||||
analysis: "Teks dan gambar perlu ditinjau moderator.",
|
||||
analysis: "Teks mengandung unsur harassment dan memerlukan tindakan lebih lanjut.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -1027,15 +1021,13 @@ describe("runModerationAnalysis", () => {
|
||||
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");
|
||||
// Images are analyzed separately now; main batch is text-only string
|
||||
const mainBatchCall = fetchCalls[fetchCalls.length - 1];
|
||||
const requestBody = JSON.parse(mainBatchCall[1].body);
|
||||
const content = requestBody.messages[0].content;
|
||||
expect(typeof content).toBe("string");
|
||||
expect(content).toContain("asep");
|
||||
expect(content).toContain("Sebelumnya user lain bilang");
|
||||
});
|
||||
|
||||
it("skips pending discord-only images until tele upload is ready", async () => {
|
||||
@@ -1563,7 +1555,7 @@ describe("runModerationAnalysis", () => {
|
||||
|
||||
it("throws on empty object", () => {
|
||||
expect(() => parseModerationResponse(JSON.stringify({}), ["m1"])).toThrow(
|
||||
/missing.*results/i,
|
||||
/Zod validation/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1620,7 +1612,7 @@ describe("runModerationAnalysis", () => {
|
||||
}),
|
||||
["m1"],
|
||||
),
|
||||
).toThrow(/invalid status/i);
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("throws on non-finite score", () => {
|
||||
@@ -1636,7 +1628,7 @@ describe("runModerationAnalysis", () => {
|
||||
]
|
||||
}`;
|
||||
|
||||
expect(() => parseModerationResponse(content, ["m1"])).toThrow(/finite/i);
|
||||
expect(() => parseModerationResponse(content, ["m1"])).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user