feat: support multiple image uploads for AI event generation 🖼️
- Updated OpenRouter integration to accept an array of image URLs - Updated ImagePicker to use the `multiple` attribute natively - Added `appendImagesDeduped` for handling client-side image deduplication - Enhanced clipboard pasting to extract multiple images at once - Rendered multiple images in a horizontal thumbnail strip in the AIToolbar - Added tests to cover multi-image logic and AI request mapping
This commit is contained in:
96
tests/ai-event-route.test.ts
Normal file
96
tests/ai-event-route.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { buildMultimodalMessages } from "@/lib/ai-event-messages";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// buildMultimodalMessages – behavioral tests
|
||||
//
|
||||
// Public behavior under test: given a system prompt, an optional text prompt,
|
||||
// and an array of base64 image strings, returns a well-formed messages array
|
||||
// for the OpenRouter chat API.
|
||||
//
|
||||
// We test WHAT the function produces (message structure), not HOW it does it.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SYSTEM_PROMPT = "You are an assistant…";
|
||||
const FAKE_PNG = "data:image/png;base64,abc123";
|
||||
const FAKE_JPEG = "data:image/jpeg;base64,def456";
|
||||
|
||||
describe("buildMultimodalMessages – message structure", () => {
|
||||
test("first message is always the system prompt", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "hello", [FAKE_PNG]);
|
||||
expect(messages[0].role).toBe("system");
|
||||
expect((messages[0].content as string)).toBe(SYSTEM_PROMPT);
|
||||
});
|
||||
|
||||
test("second message is the user message", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "hello", [FAKE_PNG]);
|
||||
expect(messages[1].role).toBe("user");
|
||||
});
|
||||
|
||||
test("user message content array starts with the text part", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "any prompt", [FAKE_PNG]);
|
||||
const userContent = messages[1].content as Array<{ type: string }>;
|
||||
expect(userContent[0].type).toBe("text");
|
||||
});
|
||||
|
||||
test("user message content array includes one image_url part per image", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "prompt", [FAKE_PNG, FAKE_JPEG]);
|
||||
const userContent = messages[1].content as Array<{ type: string }>;
|
||||
const imageparts = userContent.filter((p) => p.type === "image_url");
|
||||
expect(imageparts).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("each image_url part carries the correct base64 URL", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, undefined, [FAKE_PNG, FAKE_JPEG]);
|
||||
const userContent = messages[1].content as Array<{
|
||||
type: string;
|
||||
imageUrl?: { url: string };
|
||||
}>;
|
||||
const imageParts = userContent.filter((p) => p.type === "image_url");
|
||||
expect(imageParts[0].imageUrl?.url).toBe(FAKE_PNG);
|
||||
expect(imageParts[1].imageUrl?.url).toBe(FAKE_JPEG);
|
||||
});
|
||||
|
||||
test("text part uses a fallback when prompt is undefined", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, undefined, [FAKE_PNG]);
|
||||
const userContent = messages[1].content as Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
}>;
|
||||
const textPart = userContent.find((p) => p.type === "text");
|
||||
expect(typeof textPart?.text).toBe("string");
|
||||
expect(textPart?.text?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("text part carries the provided prompt text when given", () => {
|
||||
const prompt = "Extract all events from these flyers";
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, prompt, [FAKE_PNG]);
|
||||
const userContent = messages[1].content as Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
}>;
|
||||
const textPart = userContent.find((p) => p.type === "text");
|
||||
expect(textPart?.text).toBe(prompt);
|
||||
});
|
||||
|
||||
test("produces exactly 2 messages (system + user)", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "x", [FAKE_PNG]);
|
||||
expect(messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("single image produces content array of length 2 (1 text + 1 image)", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "x", [FAKE_PNG]);
|
||||
const userContent = messages[1].content as unknown[];
|
||||
expect(userContent).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("three images produce content array of length 4 (1 text + 3 images)", () => {
|
||||
const messages = buildMultimodalMessages(SYSTEM_PROMPT, "x", [
|
||||
FAKE_PNG,
|
||||
FAKE_JPEG,
|
||||
FAKE_PNG,
|
||||
]);
|
||||
const userContent = messages[1].content as unknown[];
|
||||
expect(userContent).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user