SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
9.0 KB · 154 lines typescript
Raw Blame History
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