diff --git a/.changeset/resumable-streams-unit-tests.md b/.changeset/resumable-streams-unit-tests.md new file mode 100644 index 000000000..890f15c21 --- /dev/null +++ b/.changeset/resumable-streams-unit-tests.md @@ -0,0 +1,5 @@ +--- +"@voltagent/resumable-streams": patch +--- + +Add unit tests for resumable-streams package diff --git a/packages/resumable-streams/package.json b/packages/resumable-streams/package.json index 8c2c03a27..11d784b8a 100644 --- a/packages/resumable-streams/package.json +++ b/packages/resumable-streams/package.json @@ -9,7 +9,10 @@ "redis": "^4.7.0", "resumable-stream": "^2.2.10" }, - "devDependencies": {}, + "devDependencies": { + "@vitest/coverage-v8": "^3.2.4", + "vitest": "^3.2.4" + }, "exports": { ".": { "import": { diff --git a/packages/resumable-streams/src/chat-session.spec.ts b/packages/resumable-streams/src/chat-session.spec.ts new file mode 100644 index 000000000..dee8e0f4d --- /dev/null +++ b/packages/resumable-streams/src/chat-session.spec.ts @@ -0,0 +1,377 @@ +import type { ResumableStreamAdapter } from "@voltagent/core"; +import { describe, expect, it, vi } from "vitest"; +import { createResumableChatSession } from "./chat-session"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const makeAdapter = (overrides: Partial = {}): ResumableStreamAdapter => ({ + createStream: vi.fn(async () => "generated-stream-id"), + resumeStream: vi.fn(async () => null), + getActiveStreamId: vi.fn(async () => null), + clearActiveStream: vi.fn(async () => {}), + ...overrides, +}); + +const makeReadableStream = (text = "hello"): ReadableStream => { + return new ReadableStream({ + start(controller) { + controller.enqueue(text); + controller.close(); + }, + }); +}; + +// --------------------------------------------------------------------------- +// createResumableChatSession — validation +// --------------------------------------------------------------------------- + +describe("createResumableChatSession", () => { + describe("validation", () => { + it("throws when conversationId is missing", () => { + expect(() => + createResumableChatSession({ + adapter: makeAdapter(), + conversationId: "", + userId: "u1", + }), + ).toThrow("conversationId is required"); + }); + + it("throws when userId is missing", () => { + expect(() => + createResumableChatSession({ + adapter: makeAdapter(), + conversationId: "conv1", + userId: "", + }), + ).toThrow("userId is required"); + }); + + it("creates a session without throwing for valid inputs", () => { + expect(() => + createResumableChatSession({ + adapter: makeAdapter(), + conversationId: "conv1", + userId: "u1", + }), + ).not.toThrow(); + }); + }); + + // --------------------------------------------------------------------------- + // createStream + // --------------------------------------------------------------------------- + + describe("createStream", () => { + it("delegates to adapter.createStream and returns the stream id", async () => { + const adapter = makeAdapter({ + createStream: vi.fn(async () => "sid-123"), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const streamId = await session.createStream(makeReadableStream()); + expect(streamId).toBe("sid-123"); + expect(adapter.createStream).toHaveBeenCalledOnce(); + }); + + it("passes conversationId and userId to adapter.createStream", async () => { + const adapter = makeAdapter({ + createStream: vi.fn(async () => "sid-xyz"), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv-abc", + userId: "user-42", + }); + + await session.createStream(makeReadableStream()); + expect(adapter.createStream).toHaveBeenCalledWith( + expect.objectContaining({ conversationId: "conv-abc", userId: "user-42" }), + ); + }); + }); + + // --------------------------------------------------------------------------- + // resumeStream + // --------------------------------------------------------------------------- + + describe("resumeStream", () => { + it("returns null when no stream exists for the given id", async () => { + const adapter = makeAdapter({ + resumeStream: vi.fn(async () => null), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const stream = await session.resumeStream("unknown-id"); + expect(stream).toBeNull(); + }); + + it("returns the stream when it exists", async () => { + const readable = makeReadableStream("data"); + const adapter = makeAdapter({ + resumeStream: vi.fn(async () => readable), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const stream = await session.resumeStream("known-id"); + expect(stream).toBe(readable); + }); + }); + + // --------------------------------------------------------------------------- + // getActiveStreamId + // --------------------------------------------------------------------------- + + describe("getActiveStreamId", () => { + it("returns null when no active stream", async () => { + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => null), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const sid = await session.getActiveStreamId(); + expect(sid).toBeNull(); + }); + + it("returns the active stream id when one is set", async () => { + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => "active-sid"), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const sid = await session.getActiveStreamId(); + expect(sid).toBe("active-sid"); + }); + }); + + // --------------------------------------------------------------------------- + // clearActiveStream + // --------------------------------------------------------------------------- + + describe("clearActiveStream", () => { + it("calls adapter.clearActiveStream with context", async () => { + const adapter = makeAdapter(); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + await session.clearActiveStream(); + expect(adapter.clearActiveStream).toHaveBeenCalledWith( + expect.objectContaining({ conversationId: "conv1", userId: "u1" }), + ); + }); + + it("passes streamId when provided", async () => { + const adapter = makeAdapter(); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + await session.clearActiveStream("sid-to-clear"); + expect(adapter.clearActiveStream).toHaveBeenCalledWith( + expect.objectContaining({ streamId: "sid-to-clear" }), + ); + }); + }); + + // --------------------------------------------------------------------------- + // consumeSseStream + // --------------------------------------------------------------------------- + + describe("consumeSseStream", () => { + it("creates a stream from the provided SSE stream", async () => { + const adapter = makeAdapter({ + createStream: vi.fn(async () => "sse-sid"), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + await session.consumeSseStream({ stream: makeReadableStream() }); + expect(adapter.createStream).toHaveBeenCalledOnce(); + }); + + it("does not throw when adapter.createStream rejects", async () => { + const adapter = makeAdapter({ + createStream: vi.fn(async () => { + throw new Error("persist failed"); + }), + }); + const logger = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }; + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + logger: logger as never, + }); + + // Should not throw even when adapter fails + await expect( + session.consumeSseStream({ stream: makeReadableStream() }), + ).resolves.toBeUndefined(); + expect(logger.error).toHaveBeenCalled(); + }); + }); + + // --------------------------------------------------------------------------- + // onFinish + // --------------------------------------------------------------------------- + + describe("onFinish", () => { + it("clears the active stream when one has been created", async () => { + const adapter = makeAdapter({ + createStream: vi.fn(async () => "fin-sid"), + clearActiveStream: vi.fn(async () => {}), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + await session.createStream(makeReadableStream()); + await session.onFinish(); + expect(adapter.clearActiveStream).toHaveBeenCalledWith( + expect.objectContaining({ streamId: "fin-sid" }), + ); + }); + + it("does nothing when there is no active stream", async () => { + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => null), + clearActiveStream: vi.fn(async () => {}), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + await session.onFinish(); + expect(adapter.clearActiveStream).not.toHaveBeenCalled(); + }); + + it("does not throw when clearActiveStream rejects", async () => { + const adapter = makeAdapter({ + createStream: vi.fn(async () => "fin-sid"), + clearActiveStream: vi.fn(async () => { + throw new Error("clear failed"); + }), + }); + const logger = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }; + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + logger: logger as never, + }); + + await session.createStream(makeReadableStream()); + await expect(session.onFinish()).resolves.toBeUndefined(); + expect(logger.error).toHaveBeenCalled(); + }); + }); + + // --------------------------------------------------------------------------- + // resumeResponse + // --------------------------------------------------------------------------- + + describe("resumeResponse", () => { + it("returns 204 when no active stream exists", async () => { + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => null), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const response = await session.resumeResponse(); + expect(response.status).toBe(204); + }); + + it("returns 204 and clears stream when the stream cannot be resumed", async () => { + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => "stale-sid"), + resumeStream: vi.fn(async () => null), + clearActiveStream: vi.fn(async () => {}), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const response = await session.resumeResponse(); + expect(response.status).toBe(204); + expect(adapter.clearActiveStream).toHaveBeenCalledWith( + expect.objectContaining({ streamId: "stale-sid" }), + ); + }); + + it("returns 200 with body when the stream can be resumed", async () => { + const readable = makeReadableStream("chunk"); + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => "live-sid"), + resumeStream: vi.fn(async () => readable), + }); + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + }); + + const response = await session.resumeResponse(); + expect(response.status).toBe(200); + expect(response.body).not.toBeNull(); + }); + + it("returns 204 when getActiveStreamId throws", async () => { + const adapter = makeAdapter({ + getActiveStreamId: vi.fn(async () => { + throw new Error("network error"); + }), + }); + const logger = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }; + const session = createResumableChatSession({ + adapter, + conversationId: "conv1", + userId: "u1", + logger: logger as never, + }); + + const response = await session.resumeResponse(); + expect(response.status).toBe(204); + expect(logger.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/resumable-streams/src/resumable-streams.spec.ts b/packages/resumable-streams/src/resumable-streams.spec.ts new file mode 100644 index 000000000..a0abefb73 --- /dev/null +++ b/packages/resumable-streams/src/resumable-streams.spec.ts @@ -0,0 +1,361 @@ +import { safeStringify } from "@voltagent/internal"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + createMemoryResumableStreamActiveStore, + createResumableStreamAdapter, + createResumableStreamGenericStore, + createResumableStreamMemoryStore, + createResumableStreamVoltOpsStore, + resolveResumableStreamAdapter, +} from "./resumable-streams"; + +// Marker keys used internally +const DISABLED = "__voltagentResumableDisabled"; +const DISABLED_REASON = "__voltagentResumableDisabledReason"; +const STORE_TYPE = "__voltagentResumableStoreType"; + +const isDisabled = (value: unknown): boolean => + !!(value && typeof value === "object" && (value as Record)[DISABLED] === true); + +const getStoreType = (value: unknown): string | null => { + if (!value || typeof value !== "object") return null; + const t = (value as Record)[STORE_TYPE]; + return typeof t === "string" ? t : null; +}; + +// --------------------------------------------------------------------------- +// Mocks +// --------------------------------------------------------------------------- + +vi.mock("resumable-stream/generic", () => ({ + createResumableStreamContext: vi.fn(() => ({ + createNewResumableStream: vi.fn(async (_id: string, makeStream: () => ReadableStream) => + makeStream(), + ), + resumeExistingStream: vi.fn(async () => null), + })), +})); + +vi.mock("resumable-stream/redis", () => ({ + createResumableStreamContext: vi.fn(() => ({ + createNewResumableStream: vi.fn(async (_id: string, makeStream: () => ReadableStream) => + makeStream(), + ), + resumeExistingStream: vi.fn(async () => null), + })), +})); + +vi.mock("redis", () => ({ + createClient: vi.fn(() => ({ + connect: vi.fn(async () => {}), + publish: vi.fn(async () => 0), + subscribe: vi.fn(async () => {}), + unsubscribe: vi.fn(async () => {}), + set: vi.fn(async () => "OK"), + get: vi.fn(async () => null), + del: vi.fn(async () => 1), + incr: vi.fn(async () => 1), + })), +})); + +vi.mock("@voltagent/core", () => ({ + getGlobalVoltOpsClient: vi.fn(() => null), + VoltOpsClient: vi.fn().mockImplementation(() => ({ + sendRequest: vi.fn( + async () => new Response(safeStringify({ streamId: "test-id" }), { status: 200 }), + ), + })), +})); + +// --------------------------------------------------------------------------- +// createMemoryResumableStreamActiveStore +// --------------------------------------------------------------------------- + +describe("createMemoryResumableStreamActiveStore", () => { + it("returns null for an unknown context", async () => { + const store = createMemoryResumableStreamActiveStore(); + const result = await store.getActiveStreamId({ conversationId: "c1", userId: "u1" }); + expect(result).toBeNull(); + }); + + it("stores and retrieves an active stream id", async () => { + const store = createMemoryResumableStreamActiveStore(); + await store.setActiveStreamId({ conversationId: "c1", userId: "u1" }, "stream-abc"); + const result = await store.getActiveStreamId({ conversationId: "c1", userId: "u1" }); + expect(result).toBe("stream-abc"); + }); + + it("clears the active stream id when no streamId specified", async () => { + const store = createMemoryResumableStreamActiveStore(); + await store.setActiveStreamId({ conversationId: "c1", userId: "u1" }, "stream-abc"); + await store.clearActiveStream({ conversationId: "c1", userId: "u1" }); + const result = await store.getActiveStreamId({ conversationId: "c1", userId: "u1" }); + expect(result).toBeNull(); + }); + + it("does not clear when streamId does not match", async () => { + const store = createMemoryResumableStreamActiveStore(); + await store.setActiveStreamId({ conversationId: "c1", userId: "u1" }, "stream-abc"); + await store.clearActiveStream({ conversationId: "c1", userId: "u1", streamId: "other-id" }); + const result = await store.getActiveStreamId({ conversationId: "c1", userId: "u1" }); + expect(result).toBe("stream-abc"); + }); + + it("clears when streamId matches the stored one", async () => { + const store = createMemoryResumableStreamActiveStore(); + await store.setActiveStreamId({ conversationId: "c1", userId: "u1" }, "stream-abc"); + await store.clearActiveStream({ conversationId: "c1", userId: "u1", streamId: "stream-abc" }); + const result = await store.getActiveStreamId({ conversationId: "c1", userId: "u1" }); + expect(result).toBeNull(); + }); + + it("isolates contexts with different conversationId", async () => { + const store = createMemoryResumableStreamActiveStore(); + await store.setActiveStreamId({ conversationId: "c1", userId: "u1" }, "stream-1"); + await store.setActiveStreamId({ conversationId: "c2", userId: "u1" }, "stream-2"); + expect(await store.getActiveStreamId({ conversationId: "c1", userId: "u1" })).toBe("stream-1"); + expect(await store.getActiveStreamId({ conversationId: "c2", userId: "u1" })).toBe("stream-2"); + }); +}); + +// --------------------------------------------------------------------------- +// createResumableStreamMemoryStore +// --------------------------------------------------------------------------- + +describe("createResumableStreamMemoryStore", () => { + it("returns a store marked with type 'memory'", async () => { + const store = await createResumableStreamMemoryStore(); + expect(getStoreType(store)).toBe("memory"); + }); + + it("is not marked as disabled", async () => { + const store = await createResumableStreamMemoryStore(); + expect(isDisabled(store)).toBe(false); + }); + + it("exposes all required methods", async () => { + const store = await createResumableStreamMemoryStore(); + expect(typeof store.createNewResumableStream).toBe("function"); + expect(typeof store.resumeExistingStream).toBe("function"); + expect(typeof store.getActiveStreamId).toBe("function"); + expect(typeof store.setActiveStreamId).toBe("function"); + expect(typeof store.clearActiveStream).toBe("function"); + }); + + it("sets and gets active stream ids", async () => { + const store = await createResumableStreamMemoryStore(); + await store.setActiveStreamId({ conversationId: "conv1", userId: "user1" }, "sid-1"); + const sid = await store.getActiveStreamId({ conversationId: "conv1", userId: "user1" }); + expect(sid).toBe("sid-1"); + }); + + it("accepts a custom keyPrefix without throwing", async () => { + const store = await createResumableStreamMemoryStore({ keyPrefix: "my-app" }); + expect(getStoreType(store)).toBe("memory"); + }); +}); + +// --------------------------------------------------------------------------- +// createResumableStreamGenericStore +// --------------------------------------------------------------------------- + +describe("createResumableStreamGenericStore", () => { + const makePublisher = () => ({ + connect: vi.fn(async () => {}), + publish: vi.fn(async () => 0), + set: vi.fn(async () => "OK" as const), + get: vi.fn(async () => null as string | null), + incr: vi.fn(async () => 1), + del: vi.fn(async () => 1), + }); + + const makeSubscriber = () => ({ + connect: vi.fn(async () => {}), + subscribe: vi.fn(async () => 1), + unsubscribe: vi.fn(async () => {}), + }); + + it("returns a store marked with type 'custom'", async () => { + const store = await createResumableStreamGenericStore({ + publisher: makePublisher(), + subscriber: makeSubscriber(), + }); + expect(getStoreType(store)).toBe("custom"); + }); + + it("is not disabled", async () => { + const store = await createResumableStreamGenericStore({ + publisher: makePublisher(), + subscriber: makeSubscriber(), + }); + expect(isDisabled(store)).toBe(false); + }); + + it("throws when publisher is missing", async () => { + await expect( + createResumableStreamGenericStore({ + publisher: undefined as never, + subscriber: makeSubscriber(), + }), + ).rejects.toThrow("Generic resumable streams require both publisher and subscriber"); + }); + + it("throws when subscriber is missing", async () => { + await expect( + createResumableStreamGenericStore({ + publisher: makePublisher(), + subscriber: undefined as never, + }), + ).rejects.toThrow("Generic resumable streams require both publisher and subscriber"); + }); +}); + +// --------------------------------------------------------------------------- +// createResumableStreamVoltOpsStore +// --------------------------------------------------------------------------- + +describe("createResumableStreamVoltOpsStore", () => { + beforeEach(() => { + vi.stubEnv("VOLTAGENT_PUBLIC_KEY", ""); + vi.stubEnv("VOLTAGENT_SECRET_KEY", ""); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("returns a disabled store when no keys are provided", async () => { + const store = await createResumableStreamVoltOpsStore(); + expect(isDisabled(store)).toBe(true); + const reason = (store as Record)[DISABLED_REASON] as string; + expect(reason).toContain("VOLTAGENT_PUBLIC_KEY"); + }); + + it("returns a non-disabled store when keys are supplied via options", async () => { + const store = await createResumableStreamVoltOpsStore({ + publicKey: "pk_test", + secretKey: "sk_test", + }); + expect(isDisabled(store)).toBe(false); + expect(getStoreType(store)).toBe("voltops"); + }); + + it("uses env vars for keys", async () => { + vi.stubEnv("VOLTAGENT_PUBLIC_KEY", "pk_env"); + vi.stubEnv("VOLTAGENT_SECRET_KEY", "sk_env"); + const store = await createResumableStreamVoltOpsStore(); + expect(isDisabled(store)).toBe(false); + expect(getStoreType(store)).toBe("voltops"); + }); +}); + +// --------------------------------------------------------------------------- +// createResumableStreamAdapter +// --------------------------------------------------------------------------- + +describe("createResumableStreamAdapter", () => { + beforeEach(() => { + vi.stubEnv("VOLTAGENT_PUBLIC_KEY", ""); + vi.stubEnv("VOLTAGENT_SECRET_KEY", ""); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("throws when no streamStore is provided", async () => { + await expect(createResumableStreamAdapter({ streamStore: undefined as never })).rejects.toThrow( + "Resumable stream store is required", + ); + }); + + it("returns a disabled adapter when the store is disabled", async () => { + const disabledStore = await createResumableStreamVoltOpsStore(); // no keys → disabled + const adapter = await createResumableStreamAdapter({ streamStore: disabledStore }); + expect(isDisabled(adapter)).toBe(true); + }); + + it("builds a valid adapter from a memory store", async () => { + const store = await createResumableStreamMemoryStore(); + const adapter = await createResumableStreamAdapter({ streamStore: store }); + expect(isDisabled(adapter)).toBe(false); + expect(typeof adapter.createStream).toBe("function"); + expect(typeof adapter.resumeStream).toBe("function"); + expect(typeof adapter.getActiveStreamId).toBe("function"); + expect(typeof adapter.clearActiveStream).toBe("function"); + }); + + it("propagates store type marker to the adapter", async () => { + const store = await createResumableStreamMemoryStore(); + const adapter = await createResumableStreamAdapter({ streamStore: store }); + expect(getStoreType(adapter)).toBe("memory"); + }); + + it("throws when store has no active stream capability and none is provided", async () => { + const minimalStore = { + createNewResumableStream: vi.fn(async () => null), + resumeExistingStream: vi.fn(async () => null), + }; + await expect(createResumableStreamAdapter({ streamStore: minimalStore })).rejects.toThrow( + "Resumable stream activeStreamStore is required", + ); + }); + + it("accepts an explicit activeStreamStore", async () => { + const minimalStore = { + createNewResumableStream: vi.fn(async () => null), + resumeExistingStream: vi.fn(async () => null), + }; + const activeStore = createMemoryResumableStreamActiveStore(); + const adapter = await createResumableStreamAdapter({ + streamStore: minimalStore, + activeStreamStore: activeStore, + }); + expect(isDisabled(adapter)).toBe(false); + expect(typeof adapter.createStream).toBe("function"); + }); +}); + +// --------------------------------------------------------------------------- +// resolveResumableStreamAdapter +// --------------------------------------------------------------------------- + +describe("resolveResumableStreamAdapter", () => { + beforeEach(() => { + vi.stubEnv("VOLTAGENT_PUBLIC_KEY", ""); + vi.stubEnv("VOLTAGENT_SECRET_KEY", ""); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("returns undefined when no adapter is provided", () => { + const result = resolveResumableStreamAdapter(undefined); + expect(result).toBeUndefined(); + }); + + it("returns the adapter when it is valid", async () => { + const store = await createResumableStreamMemoryStore(); + const adapter = await createResumableStreamAdapter({ streamStore: store }); + const result = resolveResumableStreamAdapter(adapter); + expect(result).toBe(adapter); + }); + + it("returns undefined and warns when the adapter is disabled", async () => { + const disabledStore = await createResumableStreamVoltOpsStore(); // no keys + const adapter = await createResumableStreamAdapter({ streamStore: disabledStore }); + const logger = { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() }; + const result = resolveResumableStreamAdapter(adapter, logger as never); + expect(result).toBeUndefined(); + expect(logger.warn).toHaveBeenCalled(); + }); + + it("returns undefined and errors on an invalid adapter shape", () => { + const logger = { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() }; + const badAdapter = { notAnAdapter: true } as never; + const result = resolveResumableStreamAdapter(badAdapter, logger as never); + expect(result).toBeUndefined(); + expect(logger.error).toHaveBeenCalled(); + }); +}); diff --git a/packages/resumable-streams/vitest.config.ts b/packages/resumable-streams/vitest.config.ts new file mode 100644 index 000000000..c6a3b7cee --- /dev/null +++ b/packages/resumable-streams/vitest.config.ts @@ -0,0 +1,31 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +export default defineConfig({ + resolve: { + alias: { + "@voltagent/core": path.resolve(__dirname, "../../packages/core/src/index.ts"), + "@voltagent/internal": path.resolve(__dirname, "../../packages/internal/src/index.ts"), + }, + }, + test: { + include: ["**/*.spec.ts"], + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.d.ts", "src/**/index.ts"], + }, + typecheck: { + include: ["**/**/*.spec-d.ts"], + exclude: ["**/**/*.spec.ts"], + }, + globals: true, + testTimeout: 10000, + hookTimeout: 10000, + }, +});