132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildConversationContext,
|
|
estimateTokens,
|
|
formatMessageForPrompt,
|
|
} from "../../src/moderation/conversationContext";
|
|
import type { MessageRecord } from "../../src/moderation/types";
|
|
|
|
function message(
|
|
id: string,
|
|
content: string,
|
|
created_at: number,
|
|
): MessageRecord {
|
|
return {
|
|
id,
|
|
guild_id: "g1",
|
|
channel_id: "c1",
|
|
thread_id: null,
|
|
user_id: `u-${id}`,
|
|
username: `user-${id}`,
|
|
avatar_url: null,
|
|
content,
|
|
edited_content: null,
|
|
created_at,
|
|
edited_at: null,
|
|
deleted_at: null,
|
|
type: "text",
|
|
metadata: null,
|
|
ai_status: "pending",
|
|
};
|
|
}
|
|
|
|
describe("buildConversationContext", () => {
|
|
it("returns only context lines (not targets) in chronological order", async () => {
|
|
const lines = await buildConversationContext({
|
|
contextBefore: [message("a", "hello", 1)],
|
|
targets: [message("b", "bad?", 2)],
|
|
maxTokens: 1000,
|
|
});
|
|
|
|
// Only context lines are returned
|
|
expect(lines).toHaveLength(1);
|
|
expect(lines[0]).toContain("[context] id=a");
|
|
expect(lines[0]).toContain("user=user-a: hello");
|
|
});
|
|
|
|
it("formats target messages using edited content when present", async () => {
|
|
const target = message("b", "original", 2);
|
|
target.edited_content = "edited";
|
|
|
|
const line = await formatMessageForPrompt(target, "target");
|
|
expect(line).toContain("edited");
|
|
expect(line).not.toContain("original");
|
|
});
|
|
|
|
it("empty targets and no context returns empty array", async () => {
|
|
const lines = await buildConversationContext({
|
|
contextBefore: [],
|
|
targets: [],
|
|
maxTokens: 1000,
|
|
});
|
|
expect(lines).toEqual([]);
|
|
});
|
|
|
|
it("returns context lines when targets are empty", async () => {
|
|
const lines = await buildConversationContext({
|
|
contextBefore: [message("a", "hello", 1)],
|
|
targets: [],
|
|
maxTokens: 1000,
|
|
});
|
|
expect(lines).toHaveLength(1);
|
|
expect(lines[0]).toContain("[context]");
|
|
});
|
|
|
|
it("excludes context when target token budget consumes all available space", async () => {
|
|
// Create targets that exceed budget
|
|
const longContent = "x".repeat(500); // ~125 tokens
|
|
const targets = [
|
|
message("t1", longContent, 1),
|
|
message("t2", longContent, 2),
|
|
message("t3", longContent, 3),
|
|
];
|
|
|
|
const lines = await buildConversationContext({
|
|
contextBefore: [message("c1", "context", 0)],
|
|
targets,
|
|
maxTokens: 200, // Targets alone consume ~375 tokens, no room for context
|
|
});
|
|
|
|
// Context should be excluded due to budget
|
|
expect(lines).toHaveLength(0);
|
|
});
|
|
|
|
it("most recent context is kept when context budget is tight", async () => {
|
|
const contextBefore = [
|
|
message(
|
|
"c1",
|
|
"oldest context with some extra words to make it longer",
|
|
1000,
|
|
),
|
|
message(
|
|
"c2",
|
|
"middle context with some extra words to make it longer",
|
|
2000,
|
|
),
|
|
message(
|
|
"c3",
|
|
"newest context with some extra words to make it longer",
|
|
3000,
|
|
),
|
|
];
|
|
|
|
const lines = await buildConversationContext({
|
|
contextBefore,
|
|
targets: [message("t1", "target message", 4000)],
|
|
maxTokens: 300, // Target ~80 tokens, c3 ~100 tokens, fits c3
|
|
});
|
|
|
|
// Should include newest context (c3) but not oldest (c1)
|
|
expect(lines.some((line) => line.includes("id=c3"))).toBe(true);
|
|
expect(lines.some((line) => line.includes("id=c1"))).toBe(false);
|
|
|
|
// Target lines should NOT be in the result (only context)
|
|
expect(lines.some((line) => line.includes("[target]"))).toBe(false);
|
|
});
|
|
|
|
it("estimateTokens provides reasonable estimates", () => {
|
|
expect(estimateTokens("hello")).toBeGreaterThan(0);
|
|
expect(estimateTokens("x".repeat(300))).toBeGreaterThan(100);
|
|
});
|
|
});
|