import type { AdapterContext } from "@rakazo/adapter-kit"; import { describe, expect, it, vi } from "vitest"; import { BuiltinMemoryProvider, builtinMemoryRequiresDeploymentOwner, MAX_MEMORY_ITEM_CHARS, prepareBuiltinMemoryConnection, splitMemoryText, } from "./builtin-memory-provider.js"; import { EmulatorEmbeddingProvider, emulatedEmbedding, OpenAiCompatibleEmbeddingServerEmulator, } from "./embedding-emulator.js"; const context: AdapterContext = { operationId: "op-1", traceId: "trace-1", spaceId: "space-1", userId: "user-1", botId: "bot-1", signal: new AbortController().signal, }; function item(id: string, content: string, model = "emulator") { return { id, content, embedding: emulatedEmbedding(content), embeddingModel: model, createdAt: new Date("2026-10-01T10:00:00.000Z"), }; } function fakeDb(items: Array> = []) { return { semanticMemoryItem: { findMany: vi.fn(async () => items), createMany: vi.fn(async (args: { data: unknown[] }) => ({ count: args.data.length })), deleteMany: vi.fn(async () => ({ count: 1 })), }, }; } function providerFor(db: ReturnType) { return new BuiltinMemoryProvider(db as never, new EmulatorEmbeddingProvider()); } describe("builtinMemoryRequiresDeploymentOwner", () => { it.each([ ["http://127.0.0.1:8080/v1", true], ["http://localhost:8080", true], ["http://192.168.1.20:8080/v1", true], ["http://100.64.1.2:8080/v1", true], ["https://embeddings.example.com/v1", false], ["not a url", false], ])("classifies %s", (baseUrl, expected) => { expect(builtinMemoryRequiresDeploymentOwner({ baseUrl, model: "m" })).toBe(expected); }); it("does not require the owner before an endpoint is chosen", () => { expect(builtinMemoryRequiresDeploymentOwner({})).toBe(false); }); }); describe("prepareBuiltinMemoryConnection", () => { it("probes the endpoint and stores the normalized connection with the key apart", async () => { const server = new OpenAiCompatibleEmbeddingServerEmulator({ apiKey: "sk-local-key" }); const prepared = await prepareBuiltinMemoryConnection( { baseUrl: " http://127.0.0.1:8080/ ", model: " nomic-embed-text " }, { apiKey: "sk-local-key" }, server.fetch, ); expect(prepared).toEqual({ settings: { baseUrl: "http://127.0.0.1:8080/v1", model: "nomic-embed-text" }, credentials: { apiKey: "sk-local-key" }, }); expect(server.requests).toHaveLength(1); }); it("connects keyless servers with empty credentials", async () => { const prepared = await prepareBuiltinMemoryConnection( { baseUrl: "http://127.0.0.1:8080/v1", model: "bge-m3" }, {}, new OpenAiCompatibleEmbeddingServerEmulator().fetch, ); expect(prepared.credentials).toEqual({}); }); it("fails before storing anything when the endpoint does not embed", async () => { const server = new OpenAiCompatibleEmbeddingServerEmulator(); server.failNextStatus = 404; await expect( prepareBuiltinMemoryConnection( { baseUrl: "http://127.0.0.1:8080/v1", model: "chat-only" }, {}, server.fetch, ), ).rejects.toThrow("Embeddings server returned 404"); await expect(prepareBuiltinMemoryConnection({ model: "m" }, {})).rejects.toThrow( /baseUrl is required/, ); await expect( prepareBuiltinMemoryConnection({ baseUrl: "http://127.0.0.1:8080" }, {}), ).rejects.toThrow(/model is required/); }); }); describe("splitMemoryText", () => { it("keeps short memories whole", () => { expect(splitMemoryText(" Prefers tea. ")).toEqual(["Prefers tea."]); }); it("splits long text at sentence ends into bounded items", () => { const sentence = "The project review moved to Thursday afternoon. "; const items = splitMemoryText(sentence.repeat(60)); expect(items.length).toBeGreaterThan(1); for (const part of items) { expect(part.length).toBeLessThanOrEqual(MAX_MEMORY_ITEM_CHARS); expect(part.endsWith(".")).toBe(true); } expect(items.join(" ")).toBe(sentence.repeat(60).trim()); }); it("never splits a surrogate pair", () => { const items = splitMemoryText(`${"a".repeat(9)}😀${"b".repeat(5)}`, 10); expect(items[0]).toBe("a".repeat(9)); expect(items[1]?.startsWith("😀")).toBe(true); }); }); describe("BuiltinMemoryProvider", () => { it("recalls only the bot's durable items in isolated scope", async () => { const db = fakeDb(); await providerFor(db).recall( { query: "jazz", scope: "isolated", botId: "bot-1", limit: 5 }, context, ); expect(db.semanticMemoryItem.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { spaceId: "space-1", embeddingModel: "emulator", OR: [{ generation: null, botId: "bot-1" }], }, }), ); }); it("adds the Space's shared items and the current history generation when asked", async () => { const db = fakeDb(); await providerFor(db).recall( { query: "jazz", scope: "shared", botId: "bot-1", historyGeneration: 3, limit: 5 }, context, ); expect(db.semanticMemoryItem.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { spaceId: "space-1", embeddingModel: "emulator", OR: [ { generation: null, OR: [{ botId: "bot-1" }, { shared: true }] }, { botId: "bot-1", generation: 3 }, ], }, }), ); }); it("ranks by similarity, drops unrelated and repeated items, and honors the limit", async () => { const db = fakeDb([ item("deadline", "Project deadline is Friday"), item("jazz-1", "User likes jazz records"), item("jazz-2", "User likes jazz records"), item("jazz-3", "Listens to jazz"), ]); const result = await providerFor(db).recall( { query: "jazz records", scope: "isolated", botId: "bot-1", limit: 5 }, context, ); expect(result.ok).toBe(true); const value = result.ok ? result.value : []; expect(value.map((entry) => entry.id)).toEqual(["jazz-1", "jazz-3"]); expect(value[0]).toMatchObject({ memory: "User likes jazz records", updatedAt: "2026-10-01T10:00:00.000Z", }); expect(value[0]!.score).toBeGreaterThan(value[1]!.score); const limited = await providerFor(db).recall( { query: "jazz records", scope: "isolated", botId: "bot-1", limit: 1 }, context, ); expect(limited.ok && limited.value.map((entry) => entry.id)).toEqual(["jazz-1"]); }); it("embeds only the opening of a long prompt used as the query", async () => { const embeddings = new EmulatorEmbeddingProvider(); const embed = vi.spyOn(embeddings, "embed"); const provider = new BuiltinMemoryProvider(fakeDb() as never, embeddings); await provider.recall( { query: "Please review this document. ".repeat(200), scope: "isolated", botId: "bot-1", limit: 5, }, context, ); const [texts] = embed.mock.calls[0]!; expect(texts).toHaveLength(1); expect(texts[0]!.length).toBeLessThanOrEqual(MAX_MEMORY_ITEM_CHARS); }); it("answers an empty query without embedding or reading", async () => { const db = fakeDb(); await expect( providerFor(db).recall({ query: " ", scope: "isolated", botId: "bot-1", limit: 5 }, context), ).resolves.toEqual({ ok: true, value: [] }); expect(db.semanticMemoryItem.findMany).not.toHaveBeenCalled(); }); it("reports embedding failures instead of throwing into the run", async () => { const provider = new BuiltinMemoryProvider(fakeDb() as never, { describe: () => new EmulatorEmbeddingProvider().describe(), embed: async () => { throw new Error("Embeddings server returned 503"); }, }); await expect( provider.recall({ query: "jazz", scope: "isolated", botId: "bot-1", limit: 5 }, context), ).resolves.toEqual({ ok: false, error: "Built-in memory could not recall: Embeddings server returned 503", }); await expect( provider.save( { content: "x", scope: "isolated", botId: "bot-1", source: { kind: "durable" } }, context, ), ).resolves.toMatchObject({ ok: false }); }); it("saves shared durable memories with their embedding", async () => { const db = fakeDb(); await expect( providerFor(db).save( { content: "Prefers tea over coffee.", scope: "shared", botId: "bot-1", source: { kind: "durable" }, }, context, ), ).resolves.toEqual({ ok: true, value: undefined }); expect(db.semanticMemoryItem.createMany).toHaveBeenCalledWith({ data: [ { spaceId: "space-1", botId: "bot-1", shared: true, generation: null, content: "Prefers tea over coffee.", embedding: emulatedEmbedding("Prefers tea over coffee."), embeddingModel: "emulator", }, ], }); }); it("splits a long history summary into private items of its generation", async () => { const db = fakeDb(); const summary = "The user planned the launch with the team. ".repeat(60); await providerFor(db).save( { content: summary, scope: "shared", botId: "bot-1", source: { kind: "history", generation: 2 }, }, context, ); const data = db.semanticMemoryItem.createMany.mock.calls[0]![0].data as Array<{ shared: boolean; generation: number | null; content: string; }>; expect(data.length).toBeGreaterThan(1); expect(data.every((row) => !row.shared && row.generation === 2)).toBe(true); expect(data.map((row) => row.content).join(" ")).toBe(summary.trim()); }); it("refuses empty and oversized memories", async () => { const provider = providerFor(fakeDb()); const request = { scope: "isolated" as const, botId: "bot-1", source: { kind: "durable" as const }, }; await expect(provider.save({ ...request, content: " " }, context)).resolves.toMatchObject({ ok: false, }); await expect( provider.save({ ...request, content: "x".repeat(40_001) }, context), ).resolves.toMatchObject({ ok: false, error: "Memory content is too long." }); }); it("purges history generations of one bot in the Space", async () => { const db = fakeDb(); const provider = providerFor(db); await provider.purgeHistory({ botId: "bot-1", generations: [] }, context); expect(db.semanticMemoryItem.deleteMany).not.toHaveBeenCalled(); await provider.purgeHistory({ botId: "bot-1", generations: [1, 1, 2] }, context); expect(db.semanticMemoryItem.deleteMany).toHaveBeenCalledWith({ where: { spaceId: "space-1", botId: "bot-1", generation: { in: [1, 2] } }, }); }); it("forgets only items the bot can recall, and never without a bot", async () => { const db = fakeDb(); const provider = providerFor(db); await expect(provider.forget({ id: "item-1", reason: "outdated" }, context)).resolves.toEqual({ ok: true, value: { id: "item-1", expired: true, reason: "outdated" }, }); expect(db.semanticMemoryItem.deleteMany).toHaveBeenCalledWith({ where: { id: "item-1", spaceId: "space-1", OR: [{ botId: "bot-1" }, { shared: true }] }, }); db.semanticMemoryItem.deleteMany.mockClear(); await expect( provider.forget({ id: "item-1" }, { ...context, botId: undefined }), ).resolves.toMatchObject({ ok: false }); expect(db.semanticMemoryItem.deleteMany).not.toHaveBeenCalled(); }); });