feat(moderation): enhance JSON extraction and validation in moderation analysis

This commit is contained in:
MythEclipse
2026-05-21 04:23:01 +07:00
parent f851ea0fa9
commit 834b19b1ae
3 changed files with 306 additions and 117 deletions
+126 -3
View File
@@ -6,7 +6,17 @@ import {
import type { MessageRecord } from "../../src/moderation/types";
vi.mock("../../src/retry", () => ({
retryWithBackoff: vi.fn((fn) => fn()),
retryWithBackoff: vi.fn(async (fn) => {
let lastError: unknown;
for (let attempt = 0; attempt < 4; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
}
}
throw lastError;
}),
}));
/**
@@ -122,6 +132,49 @@ describe("parseModerationResponse", () => {
expect(result[0].messageId).toBe("m1");
});
it("handles trailing JSON after first moderation object", () => {
const moderationJson = JSON.stringify({
results: [
{
message_id: "m1",
status: "clean",
flags: [],
score: 0.1,
analysis: "OK",
},
],
});
const trailingLogJson = JSON.stringify({ msg: "Retry attempt" });
const result = parseModerationResponse(
`${moderationJson}\n${trailingLogJson}`,
["m1"],
);
expect(result).toHaveLength(1);
expect(result[0].messageId).toBe("m1");
});
it("handles braces inside string values", () => {
const result = parseModerationResponse(
JSON.stringify({
results: [
{
message_id: "m1",
status: "clean",
flags: [],
score: 0.1,
analysis: "Contains literal braces: {not json}",
},
],
}),
["m1"],
);
expect(result).toHaveLength(1);
expect(result[0].analysis).toBe("Contains literal braces: {not json}");
});
it("handles nested fields in results", () => {
const content = JSON.stringify({
results: [
@@ -554,6 +607,77 @@ describe("runModerationAnalysis", () => {
expect(result.results[0].messageId).toBe("m1");
});
it("includes previous validation error in retry prompt", async () => {
const invalidResponse = {
choices: [
{
message: {
content: JSON.stringify({
results: [
{
message_id: "m1",
status: "bad",
flags: [],
score: 0.1,
analysis: "Invalid",
},
],
}),
},
},
],
};
const validResponse = {
choices: [
{
message: {
content: JSON.stringify({
results: [
{
message_id: "m1",
status: "clean",
flags: [],
score: 0.1,
analysis: "OK",
},
],
}),
},
},
],
};
global.fetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify(invalidResponse),
})
.mockResolvedValueOnce({
ok: true,
text: async () => JSON.stringify(validResponse),
});
const result = await runModerationAnalysis({
targets: [createMessageRecord()],
contextText: "test context",
});
const secondRequestBody = JSON.parse(
(global.fetch as any).mock.calls[1][1].body,
);
expect(secondRequestBody.messages[0].content).toContain(
"Previous response failed validation",
);
expect(secondRequestBody.messages[0].content).toContain(
"Invalid status: bad",
);
expect(secondRequestBody.messages[0].content).toContain(
"Retry with corrected output",
);
expect(result.results[0].status).toBe("clean");
});
it("throws on missing choices in response", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
@@ -1039,8 +1163,7 @@ describe("runModerationAnalysis", () => {
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(
expect(requestBody.messages[0].content).toContain(
"https://example.invalid/claim",
);
});