feat(config): add AI analysis tuning parameters and update related logic
This commit is contained in:
@@ -28,6 +28,39 @@ describe("loadConfig", () => {
|
||||
expect(config.WEBSERVER_PORT).toBe(4000);
|
||||
expect(config.RECORDINGS_DIR).toBe("./recordings");
|
||||
expect(config.NODE_ENV).toBe("test");
|
||||
expect(config.TELE_UPLOAD_URL).toBe(
|
||||
"https://upload.asepharyana.tech/api/upload",
|
||||
);
|
||||
expect(config.AI_ANALYSIS_DEBOUNCE_MS).toBe(500);
|
||||
expect(config.AI_ANALYSIS_RECOVERY_INTERVAL_MS).toBe(15000);
|
||||
expect(config.AI_ANALYSIS_ERROR_COOLDOWN_MS).toBe(30000);
|
||||
expect(config.AI_ANALYSIS_MAX_BATCH_SIZE).toBe(25);
|
||||
expect(config.AI_ANALYSIS_MAX_CONTEXT_TOKENS).toBe(8000);
|
||||
expect(config.AI_ANALYSIS_CONTEXT_MESSAGE_LIMIT).toBe(20);
|
||||
});
|
||||
|
||||
it("coerces AI analysis tuning values", async () => {
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
DISCORD_TOKEN: "token",
|
||||
AI_ANALYSIS_DEBOUNCE_MS: "750",
|
||||
AI_ANALYSIS_RECOVERY_INTERVAL_MS: "20000",
|
||||
AI_ANALYSIS_ERROR_COOLDOWN_MS: "45000",
|
||||
AI_ANALYSIS_MAX_BATCH_SIZE: "40",
|
||||
AI_ANALYSIS_MAX_CONTEXT_TOKENS: "12000",
|
||||
AI_ANALYSIS_CONTEXT_MESSAGE_LIMIT: "35",
|
||||
NODE_ENV: "test",
|
||||
};
|
||||
|
||||
const { loadConfig } = await import("../src/config");
|
||||
const config = loadConfig(process.env);
|
||||
|
||||
expect(config.AI_ANALYSIS_DEBOUNCE_MS).toBe(750);
|
||||
expect(config.AI_ANALYSIS_RECOVERY_INTERVAL_MS).toBe(20000);
|
||||
expect(config.AI_ANALYSIS_ERROR_COOLDOWN_MS).toBe(45000);
|
||||
expect(config.AI_ANALYSIS_MAX_BATCH_SIZE).toBe(40);
|
||||
expect(config.AI_ANALYSIS_MAX_CONTEXT_TOKENS).toBe(12000);
|
||||
expect(config.AI_ANALYSIS_CONTEXT_MESSAGE_LIMIT).toBe(35);
|
||||
});
|
||||
|
||||
it("derives split text and voice guild defaults from legacy config", async () => {
|
||||
|
||||
@@ -10,44 +10,31 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("attachmentUploader", () => {
|
||||
it("parses picser upload response correctly", async () => {
|
||||
const { parseUploadResponse } = await import(
|
||||
"../../src/moderation/attachmentUploader"
|
||||
it("parses tele upload response correctly", async () => {
|
||||
const { parseTeleUploadResponse } = await import(
|
||||
"../../src/uploader/teleUpload"
|
||||
);
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
filename: "uploads/abc123.jpg",
|
||||
urls: {
|
||||
raw_commit:
|
||||
"https://raw.githubusercontent.com/user/repo/commit/uploads/abc123.jpg",
|
||||
},
|
||||
size: 102400,
|
||||
type: "image/jpeg",
|
||||
};
|
||||
const result = parseTeleUploadResponse({
|
||||
download_url: "https://upload.asepharyana.tech/d/abc123.jpg",
|
||||
public_id: "abc123",
|
||||
file_name: "abc123.jpg",
|
||||
size_bytes: 102400,
|
||||
});
|
||||
|
||||
const result = parseUploadResponse(response);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.url).toBe(
|
||||
"https://raw.githubusercontent.com/user/repo/commit/uploads/abc123.jpg",
|
||||
);
|
||||
expect(result.filename).toBe("uploads/abc123.jpg");
|
||||
expect(result.url).toBe("https://upload.asepharyana.tech/d/abc123.jpg");
|
||||
expect(result.publicId).toBe("abc123");
|
||||
expect(result.filename).toBe("abc123.jpg");
|
||||
expect(result.sizeBytes).toBe(102400);
|
||||
});
|
||||
|
||||
it("handles upload response with missing raw_commit", async () => {
|
||||
const { parseUploadResponse } = await import(
|
||||
"../../src/moderation/attachmentUploader"
|
||||
it("handles upload response with missing download_url", async () => {
|
||||
const { parseTeleUploadResponse } = await import(
|
||||
"../../src/uploader/teleUpload"
|
||||
);
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
filename: "uploads/abc123.jpg",
|
||||
urls: {},
|
||||
size: 102400,
|
||||
type: "image/jpeg",
|
||||
};
|
||||
|
||||
expect(() => parseUploadResponse(response)).toThrow();
|
||||
expect(() => parseTeleUploadResponse({ download_url: "" })).toThrow(
|
||||
/download_url/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -420,6 +420,46 @@ describe("runModerationAnalysis", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends text-only analysis without dummy image", async () => {
|
||||
const mockResponse = {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({
|
||||
results: [
|
||||
{
|
||||
message_id: "m1",
|
||||
status: "clean",
|
||||
flags: [],
|
||||
score: 0.1,
|
||||
analysis: "OK",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
text: async () => JSON.stringify(mockResponse),
|
||||
json: async () => mockResponse,
|
||||
});
|
||||
|
||||
await runModerationAnalysis({
|
||||
targets: [createMessageRecord()],
|
||||
contextText: "test context",
|
||||
});
|
||||
|
||||
const requestBody = JSON.parse((global.fetch as any).mock.calls[0][1].body);
|
||||
expect(requestBody.messages[0].role).toBe("system");
|
||||
expect(requestBody.messages[1].role).toBe("user");
|
||||
expect(typeof requestBody.messages[1].content).toBe("string");
|
||||
expect(requestBody.messages[1].content).toContain("test context");
|
||||
expect(requestBody.messages[1].content).not.toContain("data:image/png");
|
||||
});
|
||||
|
||||
it("throws on non-ok HTTP response", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
@@ -435,6 +475,42 @@ describe("runModerationAnalysis", () => {
|
||||
).rejects.toThrow(/LLM API error 500/);
|
||||
});
|
||||
|
||||
it("parses first JSON object when provider appends extra JSON", async () => {
|
||||
const mockResponse = {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({
|
||||
results: [
|
||||
{
|
||||
message_id: "m1",
|
||||
status: "clean",
|
||||
flags: [],
|
||||
score: 0.1,
|
||||
analysis: "OK",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
text: async () =>
|
||||
`${JSON.stringify(mockResponse)}\n{"usage":{"tokens":12}}`,
|
||||
});
|
||||
|
||||
const result = await runModerationAnalysis({
|
||||
targets: [createMessageRecord()],
|
||||
contextText: "test context",
|
||||
});
|
||||
|
||||
expect(result.results).toHaveLength(1);
|
||||
expect(result.results[0].messageId).toBe("m1");
|
||||
});
|
||||
|
||||
it("throws on missing choices in response", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -544,7 +620,8 @@ describe("runModerationAnalysis", () => {
|
||||
const [, completionsOptions] = fetchCalls[1];
|
||||
const body = JSON.parse(completionsOptions.body);
|
||||
|
||||
const userMessage = body.messages[0];
|
||||
expect(body.messages[0].role).toBe("system");
|
||||
const userMessage = body.messages[1];
|
||||
expect(userMessage.role).toBe("user");
|
||||
expect(Array.isArray(userMessage.content)).toBe(true);
|
||||
expect(userMessage.content[0].type).toBe("image_url");
|
||||
@@ -556,7 +633,8 @@ describe("runModerationAnalysis", () => {
|
||||
"Image Attachment for Message ID: m1",
|
||||
);
|
||||
expect(userMessage.content[2].type).toBe("text");
|
||||
expect(userMessage.content[2].text).toContain(
|
||||
expect(userMessage.content[2].text).toContain("test context");
|
||||
expect(body.messages[0].content).toContain(
|
||||
"You are a content moderation assistant.",
|
||||
);
|
||||
});
|
||||
@@ -764,7 +842,7 @@ describe("runModerationAnalysis", () => {
|
||||
expect(fetchCalls[1][0]).toBe("https://httpbin.org/image/png");
|
||||
|
||||
const requestBody = JSON.parse(fetchCalls[2][1].body);
|
||||
const contentParts = requestBody.messages[0].content;
|
||||
const contentParts = requestBody.messages[1].content;
|
||||
expect(
|
||||
contentParts.filter((part: any) => part.type === "image_url"),
|
||||
).toHaveLength(2);
|
||||
@@ -819,7 +897,7 @@ describe("runModerationAnalysis", () => {
|
||||
targets: [
|
||||
createMessageRecord({
|
||||
id: "m1",
|
||||
content: "gambar belum selesai upload ke picser",
|
||||
content: "gambar belum selesai upload ke tele",
|
||||
}),
|
||||
],
|
||||
contextText: "test context",
|
||||
@@ -916,8 +994,8 @@ 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[1].content).toHaveLength(1);
|
||||
expect(requestBody.messages[1].content[0].text).toContain(
|
||||
"https://example.invalid/claim",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -617,7 +617,7 @@ describe("message query integration tests", () => {
|
||||
size: 1024,
|
||||
type: "image/png",
|
||||
discord_url: "https://discord.com/test1.png",
|
||||
uploaded_url: "https://picser.tech/test1.png",
|
||||
uploaded_url: "https://upload.asepharyana.tech/test1.png",
|
||||
upload_status: "uploaded" as const,
|
||||
upload_error: null,
|
||||
created_at: Date.now(),
|
||||
@@ -635,7 +635,7 @@ describe("message query integration tests", () => {
|
||||
size: 2048,
|
||||
type: "image/png",
|
||||
discord_url: "https://discord.com/test2.png",
|
||||
uploaded_url: "https://picser.tech/test2.png",
|
||||
uploaded_url: "https://upload.asepharyana.tech/test2.png",
|
||||
upload_status: "uploaded" as const,
|
||||
upload_error: null,
|
||||
created_at: Date.now(),
|
||||
|
||||
Reference in New Issue
Block a user