From fe0cb1f4c5378d0743d0459fd7aaba75e948f0d6 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Thu, 23 Jan 2025 16:35:32 -0800 Subject: [PATCH] fix(js): Handle undefined values in object equality checks (#1460) --- js/package.json | 2 +- js/src/index.ts | 2 +- js/src/tests/jestlike/jest.test.ts | 11 ++++++ js/src/utils/jestlike/index.ts | 59 +++++++++++++++--------------- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/js/package.json b/js/package.json index 724547d41..7c9300862 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "langsmith", - "version": "0.3.2", + "version": "0.3.3", "description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.", "packageManager": "yarn@1.22.19", "files": [ diff --git a/js/src/index.ts b/js/src/index.ts index 16be66106..1d05820cb 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -18,4 +18,4 @@ export { RunTree, type RunTreeConfig } from "./run_trees.js"; export { overrideFetchImplementation } from "./singletons/fetch.js"; // Update using yarn bump-version -export const __version__ = "0.3.2"; +export const __version__ = "0.3.3"; diff --git a/js/src/tests/jestlike/jest.test.ts b/js/src/tests/jestlike/jest.test.ts index 5d9c68dcc..f92ef67a0 100644 --- a/js/src/tests/jestlike/jest.test.ts +++ b/js/src/tests/jestlike/jest.test.ts @@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import * as ls from "../../jest/index.js"; import { type SimpleEvaluator } from "../../jest/index.js"; +import { objectHash } from "../../utils/jestlike/index.js"; const myEvaluator: SimpleEvaluator = (params) => { const { referenceOutputs, outputs } = params; @@ -253,3 +254,13 @@ ls.describe("Test Linkedin Post", () => { } ); }); + +test("object hash should work on undefined values", async () => { + expect( + objectHash({ + foo: "bar", + baz: undefined, + qux: null, + }) + ).toEqual("88d67a35803b03a787d9fce25ebed027807c68ce0c3dee9f818fc58a43dd10af"); +}); diff --git a/js/src/utils/jestlike/index.ts b/js/src/utils/jestlike/index.ts index e78f44337..ae01c5754 100644 --- a/js/src/utils/jestlike/index.ts +++ b/js/src/utils/jestlike/index.ts @@ -96,41 +96,42 @@ export function logOutputs(output: Record) { context.setLoggedOutput(output); } +export function objectHash(obj: KVMap, depth = 0): string { + // Prevent infinite recursion + if (depth > 50) { + throw new Error( + "Object is too deep to check equality for serialization. Please use a simpler example." + ); + } + + if (Array.isArray(obj)) { + const arrayHash = obj.map((item) => objectHash(item, depth + 1)).join(","); + return crypto.createHash("sha256").update(arrayHash).digest("hex"); + } + + if (obj && typeof obj === "object") { + const sortedHash = Object.keys(obj) + .sort() + .map((key) => `${key}:${objectHash(obj[key], depth + 1)}`) + .join(","); + return crypto.createHash("sha256").update(sortedHash).digest("hex"); + } + + return ( + crypto + .createHash("sha256") + // Treat null and undefined as equal for serialization purposes + .update(JSON.stringify(obj ?? null)) + .digest("hex") + ); +} + export function generateWrapperFromJestlikeMethods( methods: Record, testRunnerName: string ) { const { expect, test, describe, beforeAll, afterAll } = methods; - const objectHash = (obj: KVMap, depth = 0): string => { - // Prevent infinite recursion - if (depth > 50) { - throw new Error( - "Object is too deep to check equality for serialization. Please use a simpler example." - ); - } - - if (Array.isArray(obj)) { - const arrayHash = obj - .map((item) => objectHash(item, depth + 1)) - .join(","); - return crypto.createHash("sha256").update(arrayHash).digest("hex"); - } - - if (obj && typeof obj === "object") { - const sortedHash = Object.keys(obj) - .sort() - .map((key) => `${key}:${objectHash(obj[key], depth + 1)}`) - .join(","); - return crypto.createHash("sha256").update(sortedHash).digest("hex"); - } - - return crypto - .createHash("sha256") - .update(JSON.stringify(obj)) - .digest("hex"); - }; - async function _createProject( client: Client, datasetId: string,