WFDASH-WEBAPP/src/lib/server/giellie-attachments.test.ts
Dave 8500a08482 feat(giellie): system prompt and attachment ingestion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 23:42:35 +02:00

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 });
});
});