From febc32d7f4bcac0c06508aa597469bca14e5f54d Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Fri, 23 Jan 2026 00:54:50 +0900 Subject: [PATCH] feat(shared): add case-insensitive utilities for agent name matching Add findCaseInsensitive, includesCaseInsensitive, findByNameCaseInsensitive, and equalsIgnoreCase functions for consistent case-insensitive lookups across the codebase. Enables 'Oracle', 'oracle', 'ORACLE' to work interchangeably. --- src/shared/case-insensitive.test.ts | 169 ++++++++++++++++++++++++++++ src/shared/case-insensitive.ts | 46 ++++++++ src/shared/index.ts | 1 + 3 files changed, 216 insertions(+) create mode 100644 src/shared/case-insensitive.test.ts create mode 100644 src/shared/case-insensitive.ts diff --git a/src/shared/case-insensitive.test.ts b/src/shared/case-insensitive.test.ts new file mode 100644 index 000000000..0d58f2b36 --- /dev/null +++ b/src/shared/case-insensitive.test.ts @@ -0,0 +1,169 @@ +import { describe, test, expect } from "bun:test" +import { + findCaseInsensitive, + includesCaseInsensitive, + findByNameCaseInsensitive, + equalsIgnoreCase, +} from "./case-insensitive" + +describe("findCaseInsensitive", () => { + test("returns undefined for empty/undefined object", () => { + // #given - undefined object + const obj = undefined + + // #when - lookup any key + const result = findCaseInsensitive(obj, "key") + + // #then - returns undefined + expect(result).toBeUndefined() + }) + + test("finds exact match first", () => { + // #given - object with exact key + const obj = { Oracle: "value1", oracle: "value2" } + + // #when - lookup with exact case + const result = findCaseInsensitive(obj, "Oracle") + + // #then - returns exact match + expect(result).toBe("value1") + }) + + test("finds case-insensitive match when no exact match", () => { + // #given - object with lowercase key + const obj = { oracle: "value" } + + // #when - lookup with uppercase + const result = findCaseInsensitive(obj, "ORACLE") + + // #then - returns case-insensitive match + expect(result).toBe("value") + }) + + test("returns undefined when key not found", () => { + // #given - object without target key + const obj = { other: "value" } + + // #when - lookup missing key + const result = findCaseInsensitive(obj, "oracle") + + // #then - returns undefined + expect(result).toBeUndefined() + }) +}) + +describe("includesCaseInsensitive", () => { + test("returns true for exact match", () => { + // #given - array with exact value + const arr = ["explore", "librarian"] + + // #when - check exact match + const result = includesCaseInsensitive(arr, "explore") + + // #then - returns true + expect(result).toBe(true) + }) + + test("returns true for case-insensitive match", () => { + // #given - array with lowercase values + const arr = ["explore", "librarian"] + + // #when - check uppercase value + const result = includesCaseInsensitive(arr, "EXPLORE") + + // #then - returns true + expect(result).toBe(true) + }) + + test("returns true for mixed case match", () => { + // #given - array with mixed case values + const arr = ["Oracle", "Sisyphus"] + + // #when - check different case + const result = includesCaseInsensitive(arr, "oracle") + + // #then - returns true + expect(result).toBe(true) + }) + + test("returns false when value not found", () => { + // #given - array without target value + const arr = ["explore", "librarian"] + + // #when - check missing value + const result = includesCaseInsensitive(arr, "oracle") + + // #then - returns false + expect(result).toBe(false) + }) + + test("returns false for empty array", () => { + // #given - empty array + const arr: string[] = [] + + // #when - check any value + const result = includesCaseInsensitive(arr, "explore") + + // #then - returns false + expect(result).toBe(false) + }) +}) + +describe("findByNameCaseInsensitive", () => { + test("finds element by exact name", () => { + // #given - array with named objects + const arr = [{ name: "Oracle", value: 1 }, { name: "explore", value: 2 }] + + // #when - find by exact name + const result = findByNameCaseInsensitive(arr, "Oracle") + + // #then - returns matching element + expect(result).toEqual({ name: "Oracle", value: 1 }) + }) + + test("finds element by case-insensitive name", () => { + // #given - array with named objects + const arr = [{ name: "Oracle", value: 1 }, { name: "explore", value: 2 }] + + // #when - find by different case + const result = findByNameCaseInsensitive(arr, "oracle") + + // #then - returns matching element + expect(result).toEqual({ name: "Oracle", value: 1 }) + }) + + test("returns undefined when name not found", () => { + // #given - array without target name + const arr = [{ name: "Oracle", value: 1 }] + + // #when - find missing name + const result = findByNameCaseInsensitive(arr, "librarian") + + // #then - returns undefined + expect(result).toBeUndefined() + }) +}) + +describe("equalsIgnoreCase", () => { + test("returns true for same case", () => { + // #given - same strings + // #when - compare + // #then - returns true + expect(equalsIgnoreCase("oracle", "oracle")).toBe(true) + }) + + test("returns true for different case", () => { + // #given - strings with different case + // #when - compare + // #then - returns true + expect(equalsIgnoreCase("Oracle", "ORACLE")).toBe(true) + expect(equalsIgnoreCase("Sisyphus-Junior", "sisyphus-junior")).toBe(true) + }) + + test("returns false for different strings", () => { + // #given - different strings + // #when - compare + // #then - returns false + expect(equalsIgnoreCase("oracle", "explore")).toBe(false) + }) +}) diff --git a/src/shared/case-insensitive.ts b/src/shared/case-insensitive.ts new file mode 100644 index 000000000..03951bc45 --- /dev/null +++ b/src/shared/case-insensitive.ts @@ -0,0 +1,46 @@ +/** + * Case-insensitive lookup and comparison utilities for agent/config names. + * Used throughout the codebase to allow "Oracle", "oracle", "ORACLE" to work the same. + */ + +/** + * Find a value in an object using case-insensitive key matching. + * First tries exact match, then falls back to lowercase comparison. + */ +export function findCaseInsensitive(obj: Record | undefined, key: string): T | undefined { + if (!obj) return undefined + const exactMatch = obj[key] + if (exactMatch !== undefined) return exactMatch + const lowerKey = key.toLowerCase() + for (const [k, v] of Object.entries(obj)) { + if (k.toLowerCase() === lowerKey) return v + } + return undefined +} + +/** + * Check if an array includes a value using case-insensitive comparison. + */ +export function includesCaseInsensitive(arr: string[], value: string): boolean { + const lowerValue = value.toLowerCase() + return arr.some((item) => item.toLowerCase() === lowerValue) +} + +/** + * Find an element in array using case-insensitive name matching. + * Useful for finding agents/categories by name. + */ +export function findByNameCaseInsensitive( + arr: T[], + name: string +): T | undefined { + const lowerName = name.toLowerCase() + return arr.find((item) => item.name.toLowerCase() === lowerName) +} + +/** + * Check if two strings are equal (case-insensitive). + */ +export function equalsIgnoreCase(a: string, b: string): boolean { + return a.toLowerCase() === b.toLowerCase() +} diff --git a/src/shared/index.ts b/src/shared/index.ts index ec775ef30..8efa978fa 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -29,3 +29,4 @@ export * from "./agent-tool-restrictions" export * from "./model-requirements" export * from "./model-resolver" export * from "./model-availability" +export * from "./case-insensitive"