supermemory/packages/tools/src/tools-shared.test.ts

169 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "vitest"
import { makeTurnKey } from "./shared/cache"
import {
DEFAULT_VALUES,
SEARCH_LIMIT_BOUNDS,
clampSearchLimit,
deduplicateMemoriesForMode,
getContainerTags,
} from "./tools-shared"
describe("clampSearchLimit", () => {
it("keeps in-range integers", () => {
expect(clampSearchLimit(7)).toBe(7)
})
it("clamps into SEARCH_LIMIT_BOUNDS", () => {
expect(clampSearchLimit(0)).toBe(SEARCH_LIMIT_BOUNDS.min)
expect(clampSearchLimit(-5)).toBe(SEARCH_LIMIT_BOUNDS.min)
expect(clampSearchLimit(999)).toBe(SEARCH_LIMIT_BOUNDS.max)
})
it("floors fractional values and coerces numeric strings", () => {
expect(clampSearchLimit(7.9)).toBe(7)
expect(clampSearchLimit("12")).toBe(12)
})
it("falls back to the default for non-numeric input", () => {
expect(clampSearchLimit("lots")).toBe(DEFAULT_VALUES.limit)
expect(clampSearchLimit(undefined)).toBe(DEFAULT_VALUES.limit)
expect(clampSearchLimit(Number.POSITIVE_INFINITY)).toBe(
DEFAULT_VALUES.limit,
)
})
})
describe("getContainerTags", () => {
it("uses the default project when no config is provided", () => {
expect(getContainerTags()).toEqual(["sm_project_default"])
})
it("converts projectId into a project container tag", () => {
expect(getContainerTags({ projectId: "abc" })).toEqual(["sm_project_abc"])
})
it("uses explicit container tags", () => {
expect(getContainerTags({ containerTags: ["tag-a", "tag-b"] })).toEqual([
"tag-a",
"tag-b",
])
})
it("rejects config with both projectId and containerTags", () => {
expect(() =>
getContainerTags({
projectId: "abc",
containerTags: ["tag-a"],
}),
).toThrow("either projectId or containerTags")
})
})
describe("deduplicateMemoriesForMode", () => {
// The profile is not injected in "query" mode, so a memory that is both a
// profile fact and a search hit must survive in the search results —
// otherwise it is dropped from the prompt entirely.
it("keeps a search result that duplicates a profile memory in query mode", () => {
const deduplicated = deduplicateMemoriesForMode("query", {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [],
searchResults: [{ memory: "User is allergic to peanuts" }],
})
expect(deduplicated.searchResults).toEqual(["User is allergic to peanuts"])
expect(deduplicated.static).toEqual([])
expect(deduplicated.dynamic).toEqual([])
})
it("still deduplicates within the search results in query mode", () => {
const deduplicated = deduplicateMemoriesForMode("query", {
static: [],
dynamic: [],
searchResults: [
{ memory: "User likes TypeScript" },
"User likes TypeScript",
],
})
expect(deduplicated.searchResults).toEqual(["User likes TypeScript"])
})
it("deduplicates normalized fact variants within and across sources", () => {
const deduplicated = deduplicateMemoriesForMode("full", {
static: [
{ memory: "User likes TypeScript" },
{ memory: " user likes typescript " },
],
dynamic: [{ memory: "[2026-08-10] USER LIKES TYPESCRIPT" }],
searchResults: [{ memory: "User prefers async/await" }],
})
expect(deduplicated).toEqual({
static: ["User likes TypeScript"],
dynamic: [],
searchResults: ["User prefers async/await"],
})
})
it("deduplicates search results against the profile in full mode", () => {
const deduplicated = deduplicateMemoriesForMode("full", {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [{ memory: "User is shipping a release today" }],
searchResults: [
{ memory: "User is allergic to peanuts" },
{ memory: "User prefers async/await" },
],
})
expect(deduplicated.static).toEqual(["User is allergic to peanuts"])
expect(deduplicated.dynamic).toEqual(["User is shipping a release today"])
expect(deduplicated.searchResults).toEqual(["User prefers async/await"])
})
it("deduplicates search results against the profile in profile mode", () => {
const deduplicated = deduplicateMemoriesForMode("profile", {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [],
searchResults: [{ memory: "User is allergic to peanuts" }],
})
expect(deduplicated.static).toEqual(["User is allergic to peanuts"])
expect(deduplicated.searchResults).toEqual([])
})
})
describe("makeTurnKey", () => {
it("generates predictable turn key for standard inputs", () => {
expect(makeTurnKey("user-123", "thread-456", "full", "hello world")).toBe(
"user-123:thread-456:full:hello world",
)
})
it("normalizes and collapses whitespace in message", () => {
expect(
makeTurnKey("user-123", "thread-456", "full", " hello world \n "),
).toBe("user-123:thread-456:full:hello world")
})
it("handles undefined threadId cleanly", () => {
expect(makeTurnKey("user-123", undefined, "profile", "test")).toBe(
"user-123::profile:test",
)
})
it("escapes colons to prevent cache key collisions between tag and threadId", () => {
const keyA = makeTurnKey("user:123", "456", "profile", "hi")
const keyB = makeTurnKey("user", "123:456", "profile", "hi")
expect(keyA).toBe("user%3A123:456:profile:hi")
expect(keyB).toBe("user:123%3A456:profile:hi")
expect(keyA).not.toBe(keyB)
})
it("escapes percent signs to avoid ambiguity with encoded sequences", () => {
const keyA = makeTurnKey("user%3A123", "456", "profile", "hi")
const keyB = makeTurnKey("user:123", "456", "profile", "hi")
expect(keyA).toBe("user%253A123:456:profile:hi")
expect(keyB).toBe("user%3A123:456:profile:hi")
expect(keyA).not.toBe(keyB)
})
})