From 44276a157483bb9dafc30fb68f24fa21190a6ae0 Mon Sep 17 00:00:00 2001 From: GautamSharma99 Date: Mon, 13 Jul 2026 04:09:25 +0530 Subject: [PATCH] fix(tools): await conversation persistence --- packages/tools/src/vercel/index.ts | 4 +- .../persistence-lifecycle.test.ts | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 packages/tools/test/with-supermemory/persistence-lifecycle.test.ts diff --git a/packages/tools/src/vercel/index.ts b/packages/tools/src/vercel/index.ts index 7726ba2e2..71f8dd408 100644 --- a/packages/tools/src/vercel/index.ts +++ b/packages/tools/src/vercel/index.ts @@ -191,7 +191,7 @@ const wrapVercelLanguageModel = ( const assistantResponseText = extractAssistantResponseText( result.content as unknown[], ) - saveMemoryAfterResponse( + await saveMemoryAfterResponse( ctx.client, ctx.containerTag, ctx.customId, @@ -267,7 +267,7 @@ const wrapVercelLanguageModel = ( userMessage && userMessage.trim() ) { - saveMemoryAfterResponse( + await saveMemoryAfterResponse( ctx.client, ctx.containerTag, ctx.customId, diff --git a/packages/tools/test/with-supermemory/persistence-lifecycle.test.ts b/packages/tools/test/with-supermemory/persistence-lifecycle.test.ts new file mode 100644 index 000000000..440bf6538 --- /dev/null +++ b/packages/tools/test/with-supermemory/persistence-lifecycle.test.ts @@ -0,0 +1,143 @@ +import type { + LanguageModelV2, + LanguageModelV2CallOptions, + LanguageModelV2StreamPart, +} from "@ai-sdk/provider" +import { afterEach, describe, expect, it, vi } from "vitest" +import { withSupermemory } from "../../src/vercel" + +const params: LanguageModelV2CallOptions = { + prompt: [ + { + role: "user", + content: [{ type: "text", text: "Hello" }], + }, + ], +} + +const emptyProfile = { + profile: { static: [], dynamic: [] }, + searchResults: { results: [] }, +} + +function deferred() { + let resolve!: (value: T) => void + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise + }) + return { promise, resolve } +} + +function createFetchMock(conversationResponse: Promise) { + const conversationRequested = deferred() + const fetchMock = vi.fn((input: string | URL | Request) => { + const url = input.toString() + if (url.endsWith("/v4/profile")) { + return Promise.resolve(Response.json(emptyProfile)) + } + if (url.endsWith("/v4/conversations")) { + conversationRequested.resolve() + return conversationResponse + } + throw new Error(`Unexpected request: ${url}`) + }) + return { fetchMock, conversationRequested: conversationRequested.promise } +} + +function wrap(model: LanguageModelV2) { + return withSupermemory(model, { + containerTag: "user-123", + customId: "conversation-123", + apiKey: "test-key", + }) +} + +describe("Vercel persistence lifecycle", () => { + const originalFetch = globalThis.fetch + + afterEach(() => { + globalThis.fetch = originalFetch + }) + + it("waits for conversation persistence before doGenerate resolves", async () => { + const conversation = deferred() + const { fetchMock, conversationRequested } = createFetchMock( + conversation.promise, + ) + globalThis.fetch = fetchMock as unknown as typeof fetch + const model: LanguageModelV2 = { + specificationVersion: "v2", + provider: "test", + modelId: "test-model", + supportedUrls: {}, + doGenerate: vi.fn(async () => ({ + content: [{ type: "text" as const, text: "Hi" }], + finishReason: "stop" as const, + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + warnings: [], + })), + doStream: vi.fn(), + } + const wrapped = wrap(model) + let completed = false + + const generatePromise = wrapped.doGenerate(params).then((result) => { + completed = true + return result + }) + + await conversationRequested + expect(fetchMock).toHaveBeenCalledTimes(2) + expect(completed).toBe(false) + + conversation.resolve(Response.json({ id: "saved-conversation" })) + await generatePromise + expect(completed).toBe(true) + }) + + it("waits for conversation persistence before a stream finishes", async () => { + const conversation = deferred() + const { fetchMock, conversationRequested } = createFetchMock( + conversation.promise, + ) + globalThis.fetch = fetchMock as unknown as typeof fetch + const upstream = new ReadableStream({ + start(controller) { + controller.enqueue({ type: "text-delta", id: "text-1", delta: "Hi" }) + controller.close() + }, + }) + const model: LanguageModelV2 = { + specificationVersion: "v2", + provider: "test", + modelId: "test-model", + supportedUrls: {}, + doGenerate: vi.fn(), + doStream: vi.fn(async () => ({ + stream: upstream, + warnings: [], + })), + } + const wrapped = wrap(model) + const { stream } = await wrapped.doStream(params) + const reader = stream.getReader() + + expect(await reader.read()).toEqual({ + done: false, + value: { type: "text-delta", id: "text-1", delta: "Hi" }, + }) + + let completed = false + const completionPromise = reader.read().then((result) => { + completed = true + return result + }) + + await conversationRequested + expect(fetchMock).toHaveBeenCalledTimes(2) + expect(completed).toBe(false) + + conversation.resolve(Response.json({ id: "saved-conversation" })) + expect(await completionPromise).toEqual({ done: true, value: undefined }) + }) +})