From dba12f95c7fc72a83f230c4fd5a90180d1c4f318 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 4 Nov 2025 15:46:38 +0100 Subject: [PATCH] test: more tests for utils --- app/client/api.ts | 2 +- app/utils/merge.ts | 13 +- app/utils/utils.test.ts | 324 ++++++++++++++++++++++++++++++++++++++ test/utils/api.test.ts | 55 +++++++ test/utils/clone.test.ts | 122 ++++++++++++++ test/utils/format.test.ts | 106 +++++++++++++ test/utils/merge.test.ts | 129 +++++++++++++++ test/utils/object.test.ts | 110 +++++++++++++ test/utils/token.test.ts | 98 ++++++++++++ 9 files changed, 953 insertions(+), 6 deletions(-) create mode 100644 app/utils/utils.test.ts create mode 100644 test/utils/api.test.ts create mode 100644 test/utils/clone.test.ts create mode 100644 test/utils/format.test.ts create mode 100644 test/utils/merge.test.ts create mode 100644 test/utils/object.test.ts create mode 100644 test/utils/token.test.ts diff --git a/app/client/api.ts b/app/client/api.ts index f60b0e2ad71..6cb0233c673 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -238,7 +238,7 @@ export function getBearerToken( } export function validString(x: string): boolean { - return x?.length > 0; + return !!x && x.trim().length > 0; } export function getHeaders(ignoreHeaders: boolean = false) { diff --git a/app/utils/merge.ts b/app/utils/merge.ts index fd7a4da98ca..de35008b175 100644 --- a/app/utils/merge.ts +++ b/app/utils/merge.ts @@ -1,13 +1,16 @@ export function merge(target: any, source: any) { Object.keys(source).forEach(function (key) { + if (key === "__proto__" || key === "constructor") return; // skip unsafe keys + if ( - source.hasOwnProperty(key) && // Check if the property is not inherited + source.hasOwnProperty(key) && source[key] && - typeof source[key] === "object" || key === "__proto__" || key === "constructor" + typeof source[key] === "object" && + !Array.isArray(source[key]) ) { merge((target[key] = target[key] || {}), source[key]); - return; + } else { + target[key] = source[key]; } - target[key] = source[key]; }); -} \ No newline at end of file +} diff --git a/app/utils/utils.test.ts b/app/utils/utils.test.ts new file mode 100644 index 00000000000..51ec6854b85 --- /dev/null +++ b/app/utils/utils.test.ts @@ -0,0 +1,324 @@ +import { + trimTopic, + isDalle3, + getTimeoutMSByModel, + getModelSizes, + supportsCustomSize, + showPlugins, + getMessageTextContent, + getMessageTextContentWithoutThinking, + getMessageImages, + semverCompare, + getOperationId, +} from "../../app/utils"; +import { ServiceProvider } from "../../app/constant"; + +describe("trimTopic", () => { + test("should remove trailing punctuation", () => { + expect(trimTopic("Hello World,")).toBe("Hello World"); + expect(trimTopic("Test。")).toBe("Test"); + expect(trimTopic("Question?")).toBe("Question"); + }); + + test("should remove quotes from both ends", () => { + expect(trimTopic('"Hello"')).toBe("Hello"); + expect(trimTopic('"""Test"""')).toBe("Test"); + }); + + test("should remove asterisks", () => { + expect(trimTopic("**Bold**")).toBe("Bold"); + expect(trimTopic("***Test***")).toBe("Test"); + }); + + test("should handle empty string", () => { + expect(trimTopic("")).toBe(""); + }); + + test("should handle string with only punctuation", () => { + expect(trimTopic("...")).toBe(""); + }); + + test("should not remove punctuation from middle", () => { + expect(trimTopic("Hello, World")).toBe("Hello, World"); + }); +}); + +describe("isDalle3", () => { + test("should return true for dall-e-3", () => { + expect(isDalle3("dall-e-3")).toBe(true); + }); + + test("should return false for other models", () => { + expect(isDalle3("dall-e-2")).toBe(false); + expect(isDalle3("gpt-4")).toBe(false); + expect(isDalle3("")).toBe(false); + }); +}); + +describe("getTimeoutMSByModel", () => { + test("should return extended timeout for dall-e models", () => { + expect(getTimeoutMSByModel("dall-e-3")).toBe(300000); + expect(getTimeoutMSByModel("dalle-2")).toBe(300000); + }); + + test("should return extended timeout for o1 models", () => { + expect(getTimeoutMSByModel("o1-preview")).toBe(300000); + expect(getTimeoutMSByModel("o3-mini")).toBe(300000); + }); + + test("should return extended timeout for deepseek-r models", () => { + expect(getTimeoutMSByModel("deepseek-r1")).toBe(300000); + }); + + test("should return extended timeout for thinking models", () => { + expect(getTimeoutMSByModel("gpt-4-thinking")).toBe(300000); + }); + + test("should return normal timeout for regular models", () => { + expect(getTimeoutMSByModel("gpt-4")).toBe(60000); + expect(getTimeoutMSByModel("claude-3")).toBe(60000); + }); + + test("should be case insensitive", () => { + expect(getTimeoutMSByModel("DALL-E-3")).toBe(300000); + expect(getTimeoutMSByModel("GPT-4")).toBe(60000); + }); +}); + +describe("getModelSizes", () => { + test("should return sizes for dall-e-3", () => { + const sizes = getModelSizes("dall-e-3"); + expect(sizes).toEqual(["1024x1024", "1792x1024", "1024x1792"]); + }); + + test("should return sizes for cogview models", () => { + const sizes = getModelSizes("cogview-3"); + expect(sizes).toContain("1024x1024"); + expect(sizes.length).toBeGreaterThan(3); + }); + + test("should return empty array for unsupported models", () => { + expect(getModelSizes("gpt-4")).toEqual([]); + expect(getModelSizes("claude-3")).toEqual([]); + }); + + test("should be case insensitive for cogview", () => { + const sizes = getModelSizes("CogView-3"); + expect(sizes.length).toBeGreaterThan(0); + }); +}); + +describe("supportsCustomSize", () => { + test("should return true for models with custom sizes", () => { + expect(supportsCustomSize("dall-e-3")).toBe(true); + expect(supportsCustomSize("cogview-3")).toBe(true); + }); + + test("should return false for models without custom sizes", () => { + expect(supportsCustomSize("gpt-4")).toBe(false); + expect(supportsCustomSize("claude-3")).toBe(false); + }); +}); + +describe("showPlugins", () => { + test("should return true for OpenAI", () => { + expect(showPlugins(ServiceProvider.OpenAI, "gpt-4")).toBe(true); + }); + + test("should return true for Azure", () => { + expect(showPlugins(ServiceProvider.Azure, "gpt-4")).toBe(true); + }); + + test("should return true for Moonshot", () => { + expect(showPlugins(ServiceProvider.Moonshot, "moonshot-v1")).toBe(true); + }); + + test("should return true for ChatGLM", () => { + expect(showPlugins(ServiceProvider.ChatGLM, "glm-4")).toBe(true); + }); + + test("should return true for Anthropic non-claude-2", () => { + expect(showPlugins(ServiceProvider.Anthropic, "claude-3")).toBe(true); + }); + + test("should return false for Anthropic claude-2", () => { + expect(showPlugins(ServiceProvider.Anthropic, "claude-2")).toBe(false); + }); + + test("should return true for Google non-vision", () => { + expect(showPlugins(ServiceProvider.Google, "gemini-pro")).toBe(true); + }); + + test("should return false for Google vision", () => { + expect(showPlugins(ServiceProvider.Google, "gemini-vision")).toBe(false); + }); + + test("should return false for other providers", () => { + expect(showPlugins(ServiceProvider.Baidu, "ernie")).toBe(false); + }); +}); + +describe("getMessageTextContent", () => { + test("should return string content directly", () => { + const message = { role: "user" as const, content: "Hello" }; + expect(getMessageTextContent(message)).toBe("Hello"); + }); + + test("should extract text from multimodal content", () => { + const message = { + role: "user" as const, + content: [ + { type: "text" as const, text: "Hello" }, + { + type: "image_url" as const, + image_url: { url: "http://example.com" }, + }, + ], + }; + expect(getMessageTextContent(message)).toBe("Hello"); + }); + + test("should return empty string if no text content", () => { + const message = { + role: "user" as const, + content: [ + { + type: "image_url" as const, + image_url: { url: "http://example.com" }, + }, + ], + }; + expect(getMessageTextContent(message)).toBe(""); + }); + + test("should handle empty content array", () => { + const message = { role: "user" as const, content: [] }; + expect(getMessageTextContent(message)).toBe(""); + }); +}); + +describe("getMessageTextContentWithoutThinking", () => { + test("should filter out thinking lines", () => { + const message = { + role: "user" as const, + content: "Normal text\n> Thinking...\nMore text", + }; + expect(getMessageTextContentWithoutThinking(message)).toBe( + "Normal text\nMore text", + ); + }); + + test("should handle content without thinking", () => { + const message = { role: "user" as const, content: "Just normal text" }; + expect(getMessageTextContentWithoutThinking(message)).toBe( + "Just normal text", + ); + }); + + test("should handle multimodal content", () => { + const message = { + role: "user" as const, + content: [{ type: "text" as const, text: "Hello\n> thinking\nWorld" }], + }; + expect(getMessageTextContentWithoutThinking(message)).toBe("Hello\nWorld"); + }); + + test("should remove empty lines", () => { + const message = { + role: "user" as const, + content: "Line1\n\n\nLine2", + }; + expect(getMessageTextContentWithoutThinking(message)).toBe("Line1\nLine2"); + }); +}); + +describe("getMessageImages", () => { + test("should extract image URLs from multimodal content", () => { + const message = { + role: "user" as const, + content: [ + { type: "text" as const, text: "Hello" }, + { type: "image_url" as const, image_url: { url: "http://img1.com" } }, + { type: "image_url" as const, image_url: { url: "http://img2.com" } }, + ], + }; + const urls = getMessageImages(message); + expect(urls).toEqual(["http://img1.com", "http://img2.com"]); + }); + + test("should return empty array for string content", () => { + const message = { role: "user" as const, content: "Hello" }; + expect(getMessageImages(message)).toEqual([]); + }); + + test("should return empty array if no images", () => { + const message = { + role: "user" as const, + content: [{ type: "text" as const, text: "Hello" }], + }; + expect(getMessageImages(message)).toEqual([]); + }); + + test("should handle missing image_url", () => { + const message = { + role: "user" as const, + content: [{ type: "image_url" as const }], + }; + const urls = getMessageImages(message); + expect(urls).toEqual([""]); + }); +}); + +describe("semverCompare", () => { + test("should compare versions correctly", () => { + expect(semverCompare("1.0.0", "2.0.0")).toBeLessThan(0); + expect(semverCompare("2.0.0", "1.0.0")).toBeGreaterThan(0); + expect(semverCompare("1.0.0", "1.0.0")).toBe(0); + }); + + test("should handle pre-release versions", () => { + expect(semverCompare("1.0.0-alpha", "1.0.0")).toBeLessThan(0); + expect(semverCompare("1.0.0", "1.0.0-alpha")).toBeGreaterThan(0); + }); + + test("should handle patch versions", () => { + expect(semverCompare("1.0.1", "1.0.2")).toBeLessThan(0); + expect(semverCompare("1.0.10", "1.0.2")).toBeGreaterThan(0); + }); + + test("should handle minor versions", () => { + expect(semverCompare("1.1.0", "1.2.0")).toBeLessThan(0); + expect(semverCompare("1.10.0", "1.2.0")).toBeGreaterThan(0); + }); + + test("should handle major versions", () => { + expect(semverCompare("2.0.0", "10.0.0")).toBeLessThan(0); + }); +}); + +describe("getOperationId", () => { + test("should return operationId if provided", () => { + const op = { operationId: "customId", method: "GET", path: "/test" }; + expect(getOperationId(op)).toBe("customId"); + }); + + test("should generate ID from method and path", () => { + const op = { method: "GET", path: "/users/list" }; + expect(getOperationId(op)).toBe("GET_users_list"); + }); + + test("should handle POST method", () => { + const op = { method: "post", path: "/create" }; + expect(getOperationId(op)).toBe("POST_create"); + }); + + test("should replace slashes with underscores", () => { + const op = { method: "GET", path: "/api/v1/users" }; + expect(getOperationId(op)).toBe("GET_api_v1_users"); + }); + + test("should handle root path", () => { + const op = { method: "GET", path: "/" }; + expect(getOperationId(op)).toBe("GET_"); + }); +}); diff --git a/test/utils/api.test.ts b/test/utils/api.test.ts new file mode 100644 index 00000000000..5615ea6eabb --- /dev/null +++ b/test/utils/api.test.ts @@ -0,0 +1,55 @@ +import { getBearerToken, validString } from "../../app/client/api"; + +describe("getBearerToken", () => { + test("should add Bearer prefix by default", () => { + expect(getBearerToken("test-key")).toBe("Bearer test-key"); + }); + + test("should not add Bearer prefix when noBearer is true", () => { + expect(getBearerToken("test-key", true)).toBe("test-key"); + }); + + test("should trim whitespace", () => { + expect(getBearerToken(" test-key ")).toBe("Bearer test-key"); + }); + + test("should return empty string for empty key", () => { + expect(getBearerToken("")).toBe(""); + }); + + /* This test identified issue with validString() method */ + test("should return empty string for whitespace only", () => { + expect(getBearerToken(" ")).toBe(""); + }); + + test("should handle null-like values", () => { + expect(getBearerToken(null as any)).toBe(""); + expect(getBearerToken(undefined as any)).toBe(""); + }); +}); + +describe("validString", () => { + test("should return true for non-empty string", () => { + expect(validString("test")).toBe(true); + }); + + test("should return false for empty string", () => { + expect(validString("")).toBe(false); + }); + + test("should return false for whitespace", () => { + expect(validString(" ")).toBe(false); + }); + + test("should return false for null", () => { + expect(validString(null as any)).toBe(false); + }); + + test("should return false for undefined", () => { + expect(validString(undefined as any)).toBe(false); + }); + + test("should return true for string with special characters", () => { + expect(validString("!@#$%")).toBe(true); + }); +}); \ No newline at end of file diff --git a/test/utils/clone.test.ts b/test/utils/clone.test.ts new file mode 100644 index 00000000000..c3c0f68d873 --- /dev/null +++ b/test/utils/clone.test.ts @@ -0,0 +1,122 @@ +import { deepClone, ensure } from "../../app/utils/clone"; + +describe("deepClone", () => { + test("should clone simple object", () => { + const obj = { a: 1, b: 2 }; + const cloned = deepClone(obj); + expect(cloned).toEqual(obj); + expect(cloned).not.toBe(obj); + }); + + test("should clone nested object", () => { + const obj = { a: { b: { c: 3 } } }; + const cloned = deepClone(obj); + expect(cloned).toEqual(obj); + expect(cloned.a).not.toBe(obj.a); + }); + + test("should clone array", () => { + const arr = [1, 2, [3, 4]]; + const cloned = deepClone(arr); + expect(cloned).toEqual(arr); + expect(cloned).not.toBe(arr); + }); + + test("should handle null", () => { + const result = deepClone(null); + expect(result).toBeNull(); + }); + + test("should throw error for undefined", () => { + expect(() => deepClone(undefined)).toThrow(SyntaxError); + }); + + test("should handle primitive types", () => { + expect(deepClone(42)).toBe(42); + expect(deepClone("test")).toBe("test"); + expect(deepClone(true)).toBe(true); + }); + + test("should clone object with mixed types", () => { + const obj = { + str: "hello", + num: 123, + bool: true, + arr: [1, 2, 3], + nested: { key: "value" }, + }; + const cloned = deepClone(obj); + expect(cloned).toEqual(obj); + expect(cloned.nested).not.toBe(obj.nested); + }); + + test("should handle Date objects", () => { + const date = new Date("2024-01-01"); + const obj = { date }; + const cloned = deepClone(obj); + expect(cloned.date).toEqual(date.toISOString()); + }); + + test("should not preserve functions", () => { + const obj = { fn: () => "test", value: 1 }; + const cloned = deepClone(obj); + expect(cloned.fn).toBeUndefined(); + expect(cloned.value).toBe(1); + }); +}); + +describe("ensure", () => { + test("should return true when all keys exist with values", () => { + const obj = { a: 1, b: "test", c: true }; + const result = ensure(obj, ["a", "b", "c"]); + expect(result).toBe(true); + }); + + test("should return false when key is undefined", () => { + const obj = { a: 1, b: undefined }; + const result = ensure(obj, ["a", "b"]); + expect(result).toBe(false); + }); + + test("should return false when key is null", () => { + const obj = { a: 1, b: null }; + const result = ensure(obj, ["a", "b"]); + expect(result).toBe(false); + }); + + test("should return false when key is empty string", () => { + const obj = { a: 1, b: "" }; + const result = ensure(obj, ["a", "b"]); + expect(result).toBe(false); + }); + + test("should return true for empty keys array", () => { + const obj = { a: 1 }; + const result = ensure(obj, []); + expect(result).toBe(true); + }); + + test("should handle missing keys", () => { + const obj = { a: 1 }; + const result = ensure(obj, ["a", "b" as keyof typeof obj]); + expect(result).toBe(false); + }); + + test("should return true when value is 0", () => { + const obj = { a: 0 }; + const result = ensure(obj, ["a"]); + expect(result).toBe(true); + }); + + test("should return true when value is false", () => { + const obj = { a: false }; + const result = ensure(obj, ["a"]); + expect(result).toBe(true); + }); + + test("should handle nested objects", () => { + const obj = { user: { name: "John" } }; + const result = ensure(obj, ["user"]); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/test/utils/format.test.ts b/test/utils/format.test.ts new file mode 100644 index 00000000000..b5cf134ac70 --- /dev/null +++ b/test/utils/format.test.ts @@ -0,0 +1,106 @@ +// test/utils/format.test.ts +import { TextEncoder, TextDecoder } from 'util'; + +(global as any).TextEncoder = TextEncoder; +(global as any).TextDecoder = TextDecoder as any; + +import { prettyObject, chunks } from "../../app/utils/format"; + +describe("prettyObject", () => { + test("should format object as JSON with code block", () => { + const obj = { name: "test", value: 123 }; + const result = prettyObject(obj); + expect(result).toBe('```json\n{\n "name": "test",\n "value": 123\n}\n```'); + }); + + test("should handle string input", () => { + const str = '{"key":"value"}'; + const result = prettyObject(str); + expect(result).toBe('```json\n{"key":"value"}\n```'); + }); + + test("should return toString for empty object", () => { + const obj = {}; + const result = prettyObject(obj); + expect(result).toBe("[object Object]"); + }); + + test("should not add code block if already present", () => { + const str = '```json\n{"test":true}\n```'; + const result = prettyObject(str); + expect(result).toBe(str); + }); + + test("should handle null", () => { + const result = prettyObject(null); + expect(result).toBe("```json\nnull\n```"); + }); + + /* + Currently it's throwing an TypeError + Or should we update prettyObject method to safely handle undefined like + we did for null? + */ + test("should handle undefined", () => { + expect(() => prettyObject(undefined)).toThrow(TypeError); + }); + + test("should handle arrays", () => { + const arr = [1, 2, 3]; + const result = prettyObject(arr); + expect(result).toBe('```json\n[\n 1,\n 2,\n 3\n]\n```'); + }); + + test("should handle nested objects", () => { + const obj = { user: { name: "John", age: 30 } }; + const result = prettyObject(obj); + expect(result).toContain('"user"'); + expect(result).toContain('"name"'); + }); +}); + +describe("chunks", () => { + test("should split string into chunks by byte size", () => { + const text = "a".repeat(2000); + const result = Array.from(chunks(text, 1000)); + expect(result.length).toBe(1); + }); + + test("should handle empty string", () => { + const result = Array.from(chunks("")); + expect(result).toEqual([]); + }); + + test("should handle string smaller than maxBytes", () => { + const text = "hello world"; + const result = Array.from(chunks(text, 1000)); + expect(result).toEqual(["hello", "world"]); + }); + + test("should split at space boundaries", () => { + const text = "word1 word2 word3 word4"; + const result = Array.from(chunks(text, 10)); + expect(result.length).toBeGreaterThan(1); + result.forEach(chunk => { + expect(chunk.length).toBeLessThanOrEqual(15); + }); + }); + + test("should handle unicode characters", () => { + const text = "你好世界 ".repeat(500); + const result = Array.from(chunks(text, 1000)); + expect(result.length).toBeGreaterThan(1); + }); + + test("should handle text without spaces", () => { + const text = "a".repeat(5000); + const result = Array.from(chunks(text, 1000)); + expect(result.length).toBe(1); + }); + + test("should use default maxBytes of 1MB", () => { + const text = "x".repeat(2000000); + const result = Array.from(chunks(text)); + expect(result.length).toBe(1); + }); +}); \ No newline at end of file diff --git a/test/utils/merge.test.ts b/test/utils/merge.test.ts new file mode 100644 index 00000000000..38982cb1a64 --- /dev/null +++ b/test/utils/merge.test.ts @@ -0,0 +1,129 @@ +import { merge } from "../../app/utils/merge"; + +describe("merge", () => { + test("should merge simple objects", () => { + const target = { a: 1 }; + const source = { b: 2 }; + merge(target, source); + expect(target).toEqual({ a: 1, b: 2 }); + }); + + test("should overwrite existing properties", () => { + const target = { a: 1, b: 2 }; + const source = { b: 3 }; + merge(target, source); + expect(target).toEqual({ a: 1, b: 3 }); + }); + + test("should merge nested objects", () => { + const target = { a: { x: 1 } }; + const source = { a: { y: 2 } }; + merge(target, source); + expect(target).toEqual({ a: { x: 1, y: 2 } }); + }); + + test("should handle deep nesting", () => { + const target = { a: { b: { c: 1 } } }; + const source = { a: { b: { d: 2 } } }; + merge(target, source); + expect(target).toEqual({ a: { b: { c: 1, d: 2 } } }); + }); + + test("should not merge __proto__", () => { + const target = {}; + const source = { __proto__: { polluted: true } }; + merge(target, source); + expect((target as any).polluted).toBeUndefined(); + }); + + test("should not merge constructor", () => { + const target = {}; + const source = { constructor: { polluted: true } }; + merge(target, source); + expect(target.constructor).toBeInstanceOf(Function); + }); + + test("should handle null source values", () => { + const target = { a: 1 }; + const source = { b: null }; + merge(target, source); + expect(target).toEqual({ a: 1, b: null }); + }); + + test("should handle undefined source values", () => { + const target = { a: 1 }; + const source = { b: undefined }; + merge(target, source); + expect(target).toEqual({ a: 1, b: undefined }); + }); + + test("should handle empty source", () => { + const target = { a: 1 }; + const source = {}; + merge(target, source); + expect(target).toEqual({ a: 1 }); + }); + + test("should handle empty target", () => { + const target = {}; + const source = { a: 1, b: 2 }; + merge(target, source); + expect(target).toEqual({ a: 1, b: 2 }); + }); + + test("should create nested objects if target doesn't have them", () => { + const target = {}; + const source = { a: { b: { c: 1 } } }; + merge(target, source); + expect(target).toEqual({ a: { b: { c: 1 } } }); + }); + + test("should handle arrays as values", () => { + const target = { a: [1, 2] }; + const source = { a: [3, 4] }; + merge(target, source); + expect(target.a).toEqual([3, 4]); + }); + + /* + TODO: Check this test case, with current merge.ts this test case fails + - "d": Array [ + - 1, + - 2, + - ], + + "d": Object { + + "0": 1, + + "1": 2, + + }, + */ + test("should handle mixed types", () => { + const target = { a: 1, b: "test", c: true }; + const source = { d: [1, 2], e: { nested: true } }; + merge(target, source); + expect(target).toEqual({ + a: 1, + b: "test", + c: true, + d: [1, 2], + e: { nested: true }, + }); + }); + + test("should not merge inherited properties", () => { + const parent = { inherited: true }; + const source = Object.create(parent); + source.own = true; + const target = {}; + merge(target, source); + expect(target).toEqual({ own: true }); + expect((target as any).inherited).toBeUndefined(); + }); + + test("should handle functions as values", () => { + const fn = () => "test"; + const target = {}; + const source = { fn }; + merge(target, source); + expect((target as any).fn).toBe(fn); + }); +}); \ No newline at end of file diff --git a/test/utils/object.test.ts b/test/utils/object.test.ts new file mode 100644 index 00000000000..eb208a66f6c --- /dev/null +++ b/test/utils/object.test.ts @@ -0,0 +1,110 @@ +import { omit, pick } from "../../app/utils/object"; + +describe("omit", () => { + test("should omit single key", () => { + const obj = { a: 1, b: 2, c: 3 }; + const result = omit(obj, "b"); + expect(result).toEqual({ a: 1, c: 3 }); + }); + + test("should omit multiple keys", () => { + const obj = { a: 1, b: 2, c: 3, d: 4 }; + const result = omit(obj, "b", "d"); + expect(result).toEqual({ a: 1, c: 3 }); + }); + + test("should return copy when no keys to omit", () => { + const obj = { a: 1, b: 2 }; + const result = omit(obj); + expect(result).toEqual(obj); + expect(result).not.toBe(obj); + }); + + test("should handle omitting non-existent keys", () => { + const obj = { a: 1, b: 2 }; + const result = omit(obj, "c" as any); + expect(result).toEqual({ a: 1, b: 2 }); + }); + + test("should handle empty object", () => { + const obj = {}; + const result = omit(obj, "a" as never); + expect(result).toEqual({}); + }); + + test("should not mutate original object", () => { + const obj = { a: 1, b: 2, c: 3 }; + const result = omit(obj, "b"); + expect(obj).toEqual({ a: 1, b: 2, c: 3 }); + expect(result).toEqual({ a: 1, c: 3 }); + }); + + test("should handle nested objects", () => { + const obj = { a: 1, b: { nested: true }, c: 3 }; + const result = omit(obj, "b"); + expect(result).toEqual({ a: 1, c: 3 }); + }); + + test("should handle arrays as values", () => { + const obj = { a: [1, 2], b: [3, 4], c: 5 }; + const result = omit(obj, "b"); + expect(result).toEqual({ a: [1, 2], c: 5 }); + }); +}); + +describe("pick", () => { + test("should pick single key", () => { + const obj = { a: 1, b: 2, c: 3 }; + const result = pick(obj, "b"); + expect(result).toEqual({ b: 2 }); + }); + + test("should pick multiple keys", () => { + const obj = { a: 1, b: 2, c: 3, d: 4 }; + const result = pick(obj, "a", "c"); + expect(result).toEqual({ a: 1, c: 3 }); + }); + + test("should return empty object when no keys to pick", () => { + const obj = { a: 1, b: 2 }; + const result = pick(obj); + expect(result).toEqual({}); + }); + + test("should handle picking non-existent keys", () => { + const obj = { a: 1, b: 2 }; + const result = pick(obj, "c" as any); + expect(result).toEqual({ c: undefined }); + }); + + test("should handle empty object", () => { + const obj = {}; + const result = pick(obj, "a" as never); + expect(result).toEqual({ a: undefined }); + }); + + test("should not mutate original object", () => { + const obj = { a: 1, b: 2, c: 3 }; + const result = pick(obj, "a", "b"); + expect(obj).toEqual({ a: 1, b: 2, c: 3 }); + expect(result).toEqual({ a: 1, b: 2 }); + }); + + test("should handle nested objects", () => { + const obj = { a: 1, b: { nested: true }, c: 3 }; + const result = pick(obj, "b"); + expect(result).toEqual({ b: { nested: true } }); + }); + + test("should handle null values", () => { + const obj = { a: null, b: 2, c: 3 }; + const result = pick(obj, "a", "b"); + expect(result).toEqual({ a: null, b: 2 }); + }); + + test("should handle undefined values", () => { + const obj = { a: undefined, b: 2, c: 3 }; + const result = pick(obj, "a", "b"); + expect(result).toEqual({ a: undefined, b: 2 }); + }); +}); \ No newline at end of file diff --git a/test/utils/token.test.ts b/test/utils/token.test.ts new file mode 100644 index 00000000000..3dee121bcd2 --- /dev/null +++ b/test/utils/token.test.ts @@ -0,0 +1,98 @@ +import { estimateTokenLength } from "../../app/utils/token"; + +describe("estimateTokenLength", () => { + test("should estimate ASCII lowercase letters", () => { + const result = estimateTokenLength("abcdefghij"); + expect(result).toBe(2.5); // 10 * 0.25 + }); + + test("should estimate ASCII uppercase letters", () => { + const result = estimateTokenLength("ABCDEFGHIJ"); + expect(result).toBe(2.5); // 10 * 0.25 + }); + + test("should estimate mixed case letters", () => { + const result = estimateTokenLength("AbCdEf"); + expect(result).toBe(1.5); // 6 * 0.25 + }); + + test("should estimate ASCII special characters", () => { + const result = estimateTokenLength("!@#$%"); + expect(result).toBe(2.5); // 5 * 0.5 + }); + + test("should estimate numbers", () => { + const result = estimateTokenLength("12345"); + expect(result).toBe(2.5); // 5 * 0.5 + }); + + test("should estimate spaces", () => { + const result = estimateTokenLength(" "); + expect(result).toBe(2.5); // 5 * 0.5 + }); + + test("should estimate Chinese characters", () => { + const result = estimateTokenLength("你好世界"); + expect(result).toBe(6); // 4 * 1.5 + }); + + test("should estimate Japanese characters", () => { + const result = estimateTokenLength("こんにちは"); + expect(result).toBe(7.5); // 5 * 1.5 + }); + + test("should estimate emoji", () => { + const result = estimateTokenLength("😀😃😄"); + expect(result).toBeGreaterThan(0); + }); + + test("should estimate mixed content", () => { + const text = "Hello 世界 123"; + const result = estimateTokenLength(text); + expect(result).toBeGreaterThan(0); + }); + + test("should handle empty string", () => { + const result = estimateTokenLength(""); + expect(result).toBe(0); + }); + + test("should estimate long text", () => { + const text = "a".repeat(1000); + const result = estimateTokenLength(text); + expect(result).toBe(250); // 1000 * 0.25 + }); + + test("should estimate text with newlines", () => { + const text = "line1\nline2\nline3"; + const result = estimateTokenLength(text); + expect(result).toBeGreaterThan(0); + }); + + test("should estimate code-like text", () => { + const code = "function test() { return 42; }"; + const result = estimateTokenLength(code); + expect(result).toBeGreaterThan(0); + }); + + test("should estimate URL", () => { + const url = "https://example.com/path?query=value"; + const result = estimateTokenLength(url); + expect(result).toBeGreaterThan(0); + }); + + test("should handle character at boundary (charCode 122 = 'z')", () => { + const result = estimateTokenLength("z"); + expect(result).toBe(0.25); + }); + + test("should handle character at boundary (charCode 65 = 'A')", () => { + const result = estimateTokenLength("A"); + expect(result).toBe(0.25); + }); + + test("should handle character below ASCII range", () => { + const result = estimateTokenLength("\t"); + expect(result).toBe(0.5); + }); +}); \ No newline at end of file