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.
This commit is contained in:
169
src/shared/case-insensitive.test.ts
Normal file
169
src/shared/case-insensitive.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
46
src/shared/case-insensitive.ts
Normal file
46
src/shared/case-insensitive.ts
Normal file
@@ -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<T>(obj: Record<string, T> | 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<T extends { name: string }>(
|
||||
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()
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user