TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import { describe, it, expect, afterAll, beforeAll } from "vitest";2import { execSync } from "node:child_process";34// Loads .env (DATABASE_URL) without printing anything.5import "../../scripts/load-env";67const hasDb = (() => {8 try {9 execSync(`psql "${process.env.DATABASE_URL ?? "postgres://localhost:5432/polyllm"}" -Atc "select 1"`, { stdio: "ignore" });10 return true;11 } catch {12 return false;13 }14})();1516/**17 * Exercises the real SQL of workstream G against the local database (needs drizzle/0003_search_index applied):18 * full-text search with filters, ILIKE fallback, cursor pagination, selective share, share listing/revocation, exports.19 */20describe.skipIf(!hasDb)("integration: search + share + export", () => {21 const uid = `it_g_${Date.now()}`;22 let convA = "";23 let convB = "";24 let projectId = "";25 let msgIds: string[] = [];2627 beforeAll(async () => {28 const { getDb, users, conversations, messages, projects, folders } = await import("@/db");29 const db = getDb();30 await db.insert(users).values({ id: uid, name: "Search IT", email: `${uid}@polyllm.test`, emailVerified: true });31 projectId = `${uid}_prj`;32 await db.insert(projects).values({ id: projectId, userId: uid, name: "Quarterly Research" });33 const folderId = `${uid}_fld`;34 await db.insert(folders).values({ id: folderId, userId: uid, name: "Clients" });35 convA = `${uid}_cnv_a`;36 convB = `${uid}_cnv_b`;37 const old = new Date("2026-01-15T10:00:00Z");38 const recent = new Date("2026-09-10T10:00:00Z");39 await db.insert(conversations).values([40 { id: convA, userId: uid, title: "Gemini rate limits explained", modelKey: "gemini/gemini-3-flash", provider: "gemini", projectId, pinned: true, messageCount: 3, lastMessageAt: recent, updatedAt: recent, createdAt: recent },41 { id: convB, userId: uid, title: "Old Claude brainstorm", modelKey: "anthropic/claude-sonnet-5", provider: "anthropic", folderId, archived: true, messageCount: 1, lastMessageAt: old, updatedAt: old, createdAt: old },42 ]);43 msgIds = [`${uid}_m1`, `${uid}_m2`, `${uid}_m3`, `${uid}_m4`];44 await db.insert(messages).values([45 { id: msgIds[0], conversationId: convA, userId: uid, role: "user", content: "Why do I get 429 errors with the Gemini free tier?", parts: [{ type: "text", text: "Why do I get 429 errors with the Gemini free tier?" }], createdAt: new Date("2026-09-10T10:00:00Z") },46 { id: msgIds[1], conversationId: convA, userId: uid, role: "assistant", modelKey: "gemini/gemini-3-flash", provider: "gemini", content: "Free-tier keys have **tiny per-model daily quotas**. Reconfiguring the project does not help.", parts: [{ type: "text", text: "Free-tier keys have tiny quotas." }], createdAt: new Date("2026-09-10T10:00:05Z") },47 { id: msgIds[2], conversationId: convA, userId: uid, role: "assistant", modelKey: "gemini/gemini-3-flash", provider: "gemini", content: "This one is inactive and must never show up: zebrafish", parts: [], active: false, createdAt: new Date("2026-09-10T10:00:06Z") },48 { id: msgIds[3], conversationId: convB, userId: uid, role: "assistant", modelKey: "anthropic/claude-sonnet-5", provider: "anthropic", content: "Brainstorm: quotas for a research budget.", parts: [], createdAt: old },49 ]);50 });5152 afterAll(async () => {53 const { getDb, users, closeDb } = await import("@/db");54 const { eq } = await import("drizzle-orm");55 await getDb().delete(users).where(eq(users.id, uid)); // cascades to everything else56 await closeDb();57 });5859 it("finds titles and message content with full-text search and highlights terms", async () => {60 const { searchAll } = await import("@/lib/search/service");61 const res = await searchAll(uid, "quotas", { limit: 10 });62 expect(res.engine).toBe("fts");63 expect(res.messages.map((m) => m.id).sort()).toEqual([msgIds[1], msgIds[3]].sort());64 expect(res.messages.every((m) => !m.id.includes("_m3"))).toBe(true); // inactive excluded65 expect(res.messages.find((m) => m.id === msgIds[1])?.snippet).toContain("quotas");66 expect(res.query.terms).toEqual(["quotas"]);67 const byTitle = await searchAll(uid, "rate limits", { limit: 10 });68 expect(byTitle.conversations.map((c) => c.id)).toEqual([convA]);69 expect(byTitle.conversations[0].pinned).toBe(true);70 expect(byTitle.conversations[0].shared).toBe(false);71 });7273 it("supports prefix matching and falls back to ILIKE for word fragments", async () => {74 const { searchAll } = await import("@/lib/search/service");75 const prefix = await searchAll(uid, "quot", { limit: 10 });76 expect(prefix.messages.length).toBe(2);77 const fragment = await searchAll(uid, "configur", { limit: 10 }); // inside "Reconfiguring"78 expect(fragment.engine).toBe("ilike");79 expect(fragment.messages.map((m) => m.id)).toEqual([msgIds[1]]);80 });8182 it("applies filters: project, folder, provider, model, role, dates and flags", async () => {83 const { searchAll } = await import("@/lib/search/service");84 expect((await searchAll(uid, "quotas project:quarterly")).messages.map((m) => m.id)).toEqual([msgIds[1]]);85 expect((await searchAll(uid, "quotas folder:clients")).messages.map((m) => m.id)).toEqual([msgIds[3]]);86 expect((await searchAll(uid, "quotas provider:anthropic")).messages.map((m) => m.id)).toEqual([msgIds[3]]);87 expect((await searchAll(uid, "quotas model:gemini-3")).messages.map((m) => m.id)).toEqual([msgIds[1]]);88 expect((await searchAll(uid, "gemini role:user")).messages.map((m) => m.id)).toEqual([msgIds[0]]);89 expect((await searchAll(uid, "gemini role:user")).conversations).toEqual([]); // role hides the conversation group90 expect((await searchAll(uid, "quotas after:2026-06-01")).messages.map((m) => m.id)).toEqual([msgIds[1]]);91 expect((await searchAll(uid, "quotas before:2026-06-01")).messages.map((m) => m.id)).toEqual([msgIds[3]]);92 expect((await searchAll(uid, "is:pinned")).conversations.map((c) => c.id)).toEqual([convA]);93 expect((await searchAll(uid, "is:archived")).conversations.map((c) => c.id)).toEqual([convB]);94 expect((await searchAll(uid, "quotas project:does-not-exist")).messages).toEqual([]);95 expect((await searchAll(uid, "role:assistant after:2026-09-01")).messages.map((m) => m.id)).toEqual([msgIds[1]]);96 });9798 it("paginates messages with an opaque cursor", async () => {99 const { searchAll } = await import("@/lib/search/service");100 const p1 = await searchAll(uid, "quotas", { limit: 1 });101 expect(p1.messages.length).toBe(1);102 expect(p1.nextCursor).toBeTruthy();103 const p2 = await searchAll(uid, "quotas", { limit: 1, cursor: p1.nextCursor });104 expect(p2.messages.length).toBe(1);105 expect(p2.messages[0].id).not.toBe(p1.messages[0].id);106 expect(p2.models).toEqual([]); // secondary groups only on the first page107 });108109 it("shares whole conversations (upsert) and selected messages (new link), lists and revokes", async () => {110 const { shareConversation, listShares, revokeShareById, getPublicShare, readShareMeta } = await import("@/lib/conversations/service");111 const { searchAll } = await import("@/lib/search/service");112 const full1 = await shareConversation(uid, convA);113 const full2 = await shareConversation(uid, convA);114 expect(full1.id).toBe(full2.id); // same URL refreshed115 expect(full1.partial).toBe(false);116 expect(full1.messageCount).toBe(2); // inactive message excluded117118 const partial = await shareConversation(uid, convA, { messageIds: [msgIds[1]] });119 expect(partial.id).not.toBe(full1.id);120 expect(partial.partial).toBe(true);121 expect(partial.messageCount).toBe(1);122123 const list = await listShares(uid);124 expect(list.map((s) => s.id).sort()).toEqual([full1.id, partial.id].sort());125 expect(list.find((s) => s.id === partial.id)?.partial).toBe(true);126127 const pub = await getPublicShare(partial.id, { peek: true });128 expect(pub).not.toBeNull();129 expect(readShareMeta(pub!.snapshot)?.selectedCount).toBe(1);130 expect((pub!.snapshot as { role?: string }[]).filter((m) => m.role).length).toBe(1);131132 expect((await searchAll(uid, "is:shared")).conversations.map((c) => c.id)).toEqual([convA]);133134 await revokeShareById(uid, partial.id);135 expect((await listShares(uid)).map((s) => s.id)).toEqual([full1.id]);136 expect(await getPublicShare(partial.id, { peek: true })).toBeNull();137 await expect(revokeShareById(uid, partial.id)).rejects.toThrow();138 });139140 it("exports txt and html", async () => {141 const { exportConversation } = await import("@/lib/conversations/service");142 const txt = await exportConversation(uid, convA, "txt");143 expect(txt.contentType).toContain("text/plain");144 expect(txt.body).toContain("GEMINI RATE LIMITS EXPLAINED");145 expect(txt.body).toContain("Assistant · gemini/gemini-3-flash");146 expect(txt.body).not.toContain("zebrafish");147 const html = await exportConversation(uid, convA, "html", { print: true, appUrl: "https://www.polyllm.io" });148 expect(html.contentType).toContain("text/html");149 expect(html.body).toContain("<strong>tiny per-model daily quotas</strong>");150 expect(html.body).toContain("window.print()");151 expect(html.filename).toBe("gemini-rate-limits-explained.html");152 });153});154