99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
const extract = vi.fn();
|
|
vi.mock("./attachment-extractor", () => ({
|
|
extractAttachmentText: (...a: unknown[]) => extract(...a),
|
|
}));
|
|
|
|
import { buildUserTurn } from "./giellie-attachments";
|
|
|
|
const file = (filename: string, contentType: string, bytes = Buffer.from("x")) => ({
|
|
filename,
|
|
contentType,
|
|
bytes,
|
|
});
|
|
|
|
beforeEach(() => extract.mockReset());
|
|
|
|
describe("buildUserTurn", () => {
|
|
it("returns just the text when there are no files", async () => {
|
|
const t = await buildUserTurn("hello", []);
|
|
expect(t.stored).toEqual([{ type: "text", text: "hello" }]);
|
|
expect(t.live).toEqual([{ type: "text", text: "hello" }]);
|
|
});
|
|
|
|
it("inlines extracted text and stores a chip", async () => {
|
|
extract.mockResolvedValue({
|
|
filename: "list.csv",
|
|
contentType: "text/csv",
|
|
kind: "csv",
|
|
text: "sku,price\nA1,10",
|
|
thin: false,
|
|
});
|
|
const t = await buildUserTurn("add these", [file("list.csv", "text/csv")]);
|
|
expect(t.stored[0]).toEqual({ type: "attachment", filename: "list.csv", kind: "file" });
|
|
expect(t.stored[1]).toMatchObject({ type: "text" });
|
|
expect((t.stored[1] as unknown as { text: string }).text).toContain("--- File: list.csv");
|
|
expect((t.stored[1] as unknown as { text: string }).text).toContain("A1,10");
|
|
expect(t.stored.at(-1)).toEqual({ type: "text", text: "add these" });
|
|
expect(t.live).toEqual(t.stored);
|
|
});
|
|
|
|
it("sends images as base64 in live blocks only", async () => {
|
|
const bytes = Buffer.from("fakejpeg");
|
|
extract.mockResolvedValue({
|
|
filename: "p.jpg",
|
|
contentType: "image/jpeg",
|
|
kind: "image",
|
|
text: null,
|
|
bytes,
|
|
});
|
|
const t = await buildUserTurn("", [file("p.jpg", "image/jpeg", bytes)]);
|
|
expect(t.stored).toContainEqual({ type: "attachment", filename: "p.jpg", kind: "image" });
|
|
expect(JSON.stringify(t.stored)).not.toContain(bytes.toString("base64"));
|
|
const img = t.live.find((b) => b.type === "image") as unknown as {
|
|
source: { type: string; media_type: string; data: string };
|
|
};
|
|
expect(img.source).toEqual({ type: "base64", media_type: "image/jpeg", data: bytes.toString("base64") });
|
|
// Empty user text still yields a text block (API requires one).
|
|
expect(t.live.at(-1)).toMatchObject({ type: "text" });
|
|
expect((t.live.at(-1) as unknown as { text: string }).text.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("notes unreadable files instead of failing", async () => {
|
|
extract.mockResolvedValue({
|
|
filename: "a.bin",
|
|
contentType: "application/octet-stream",
|
|
kind: "unknown",
|
|
text: null,
|
|
bytes: Buffer.from("x"),
|
|
});
|
|
const t = await buildUserTurn("see file", [file("a.bin", "application/octet-stream")]);
|
|
const all = JSON.stringify(t.stored);
|
|
expect(all).toContain("Could not read");
|
|
expect(all).toContain("a.bin");
|
|
});
|
|
|
|
it("truncates very long extracted text", async () => {
|
|
extract.mockResolvedValue({
|
|
filename: "big.csv",
|
|
contentType: "text/csv",
|
|
kind: "csv",
|
|
text: "a".repeat(300_000),
|
|
thin: false,
|
|
});
|
|
const t = await buildUserTurn("x", [file("big.csv", "text/csv")]);
|
|
const block = t.stored.find((b) => b.type === "text" && String(b.text).startsWith("--- File")) as unknown as {
|
|
text: string;
|
|
};
|
|
expect(block.text.length).toBeLessThan(210_000);
|
|
expect(block.text).toContain("truncated");
|
|
});
|
|
|
|
it("rejects too many files and oversized files", async () => {
|
|
const many = Array.from({ length: 11 }, (_, i) => file(`f${i}.txt`, "text/plain"));
|
|
await expect(buildUserTurn("x", many)).rejects.toMatchObject({ status: 400 });
|
|
const big = file("huge.pdf", "application/pdf", Buffer.alloc(10 * 1024 * 1024 + 1));
|
|
await expect(buildUserTurn("x", [big])).rejects.toMatchObject({ status: 413 });
|
|
});
|
|
});
|