UNPKG

@langchain/langgraph

Version:

LangGraph

212 lines 9.37 kB
import { describe, beforeEach, afterAll, it, expect, vi } from "vitest"; import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons"; import { ensureLangGraphConfig, getStore, getWriter, getConfig, recastCheckpointNamespace, getParentCheckpointNamespace, } from "./config.js"; import { CHECKPOINT_NAMESPACE_SEPARATOR, CHECKPOINT_NAMESPACE_END, } from "../../constants.js"; describe("ensureLangGraphConfig", () => { // Save original to restore after tests const originalGetRunnableConfig = AsyncLocalStorageProviderSingleton.getRunnableConfig; beforeEach(() => { // Reset the mock before each test AsyncLocalStorageProviderSingleton.getRunnableConfig = vi.fn(); }); afterAll(() => { // Restore the original after all tests AsyncLocalStorageProviderSingleton.getRunnableConfig = originalGetRunnableConfig; }); it("should return a default config when no arguments provided", () => { // Mock the AsyncLocalStorage to return undefined AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(undefined); const result = ensureLangGraphConfig(); expect(result).toEqual({ tags: [], metadata: {}, callbacks: undefined, recursionLimit: 25, configurable: {}, }); }); it("should merge multiple configs, with later configs taking precedence", () => { // Mock the AsyncLocalStorage to return undefined AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(undefined); const config1 = { tags: ["tag1"], metadata: { key1: "value1" }, configurable: { option1: "value1" }, }; const config2 = { tags: ["tag2"], metadata: { key2: "value2" }, configurable: { option2: "value2" }, }; const result = ensureLangGraphConfig(config1, config2); // The implementation completely replaces objects rather than merging them expect(result).toEqual({ tags: ["tag2"], metadata: { key2: "value2", option2: "value2" }, callbacks: undefined, recursionLimit: 25, configurable: { option2: "value2" }, }); }); it("should copy values from AsyncLocalStorage if available", () => { // Mock values from AsyncLocalStorage const asyncLocalStorageConfig = { tags: ["storage-tag"], metadata: { storage: "value" }, callbacks: { copy: () => ({ type: "copied-callback" }) }, configurable: { storageOption: "value" }, }; AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(asyncLocalStorageConfig); const result = ensureLangGraphConfig(); expect(result.tags).toEqual(["storage-tag"]); expect(result.metadata || {}).toEqual({ storage: "value", storageOption: "value", }); expect(result.configurable).toEqual({ storageOption: "value" }); expect(result.callbacks).toEqual({ type: "copied-callback" }); }); it("should handle undefined config values", () => { AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(undefined); const config1 = undefined; const config2 = { tags: ["tag2"], metadata: undefined, }; const result = ensureLangGraphConfig(config1, config2); expect(result.tags).toEqual(["tag2"]); expect(result.metadata).toEqual({}); }); it("should copy scalar values to metadata from configurable", () => { AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(undefined); const config = { configurable: { stringValue: "string", numberValue: 42, booleanValue: true, objectValue: { should: "not be copied" }, __privateValue: "should not be copied", }, }; const result = ensureLangGraphConfig(config); expect(result.metadata).toEqual({ stringValue: "string", numberValue: 42, booleanValue: true, // objectValue and __privateValue should not be copied }); }); it("should not overwrite existing metadata values with configurable values", () => { AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(undefined); const config = { metadata: { key: "original value" }, configurable: { key: "should not overwrite", }, }; const result = ensureLangGraphConfig(config); expect(result.metadata?.key).toEqual("original value"); }); }); describe("getStore, getWriter, getConfig", () => { // Save original to restore after tests const originalGetRunnableConfig = AsyncLocalStorageProviderSingleton.getRunnableConfig; beforeEach(() => { // Reset the mock before each test AsyncLocalStorageProviderSingleton.getRunnableConfig = vi.fn(); }); afterAll(() => { // Restore the original after all tests AsyncLocalStorageProviderSingleton.getRunnableConfig = originalGetRunnableConfig; }); it("getStore should return store from config", () => { const mockStore = {}; AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue({ store: mockStore, }); const result = getStore(); expect(result).toBe(mockStore); }); it("getWriter should return writer from configurable", () => { const mockWriter = () => { }; AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue({ configurable: { writer: mockWriter, }, }); const result = getWriter(); expect(result).toBe(mockWriter); }); it("getConfig should return the full config", () => { const mockConfig = { key: "value" }; AsyncLocalStorageProviderSingleton.getRunnableConfig = vi .fn() .mockReturnValue(mockConfig); const result = getConfig(); expect(result).toBe(mockConfig); }); }); describe("recastCheckpointNamespace", () => { it("should filter out numeric parts of the namespace", () => { const namespace = `parent${CHECKPOINT_NAMESPACE_SEPARATOR}123${CHECKPOINT_NAMESPACE_SEPARATOR}child`; const result = recastCheckpointNamespace(namespace); expect(result).toBe(`parent${CHECKPOINT_NAMESPACE_SEPARATOR}child`); }); it("should remove parts after CHECKPOINT_NAMESPACE_END", () => { const namespace = `part1${CHECKPOINT_NAMESPACE_SEPARATOR}part2${CHECKPOINT_NAMESPACE_END}extra`; const result = recastCheckpointNamespace(namespace); expect(result).toBe(`part1${CHECKPOINT_NAMESPACE_SEPARATOR}part2`); }); it("should handle complex namespace with numeric parts and CHECKPOINT_NAMESPACE_END", () => { const namespace = `root${CHECKPOINT_NAMESPACE_SEPARATOR}123${CHECKPOINT_NAMESPACE_SEPARATOR}child${CHECKPOINT_NAMESPACE_END}extra${CHECKPOINT_NAMESPACE_SEPARATOR}456`; const result = recastCheckpointNamespace(namespace); expect(result).toBe(`root${CHECKPOINT_NAMESPACE_SEPARATOR}child`); }); it("should return the original namespace when no filtering needed", () => { const namespace = `part1${CHECKPOINT_NAMESPACE_SEPARATOR}part2`; const result = recastCheckpointNamespace(namespace); expect(result).toBe(namespace); }); }); describe("getParentCheckpointNamespace", () => { it("should return the parent namespace by removing the last part", () => { const namespace = `parent${CHECKPOINT_NAMESPACE_SEPARATOR}child`; const result = getParentCheckpointNamespace(namespace); expect(result).toBe("parent"); }); it("should skip trailing numeric parts", () => { const namespace = `parent${CHECKPOINT_NAMESPACE_SEPARATOR}child${CHECKPOINT_NAMESPACE_SEPARATOR}123${CHECKPOINT_NAMESPACE_SEPARATOR}456`; const result = getParentCheckpointNamespace(namespace); expect(result).toBe("parent"); }); it("should return empty string for top-level namespace", () => { const namespace = "singlePart"; const result = getParentCheckpointNamespace(namespace); expect(result).toBe(""); }); it("should handle namespace with mixed numeric and non-numeric parts", () => { const namespace = `root${CHECKPOINT_NAMESPACE_SEPARATOR}sub1${CHECKPOINT_NAMESPACE_SEPARATOR}123${CHECKPOINT_NAMESPACE_SEPARATOR}sub2`; const result = getParentCheckpointNamespace(namespace); // The implementation stops at the first numeric part, not at the last non-numeric part expect(result).toBe(`root${CHECKPOINT_NAMESPACE_SEPARATOR}sub1${CHECKPOINT_NAMESPACE_SEPARATOR}123`); }); }); //# sourceMappingURL=config.test.js.map