mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-28 06:24:40 +00:00
Co-authored-by: abhinav7x94 <raj.abhinav7x94@gmail.com> Co-authored-by: Rohit <71192000+rohitsux@users.noreply.github.com> Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com> Co-authored-by: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Co-authored-by: Rikinshah787 <Rikinshah787@users.noreply.github.com> Co-authored-by: Agnik47 <140933190+Agnik47@users.noreply.github.com>
186 lines
6 KiB
TypeScript
186 lines
6 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
import { makeTurnKey } from "./shared/cache"
|
|
import { normalizeBaseUrl } from "./shared/context"
|
|
import {
|
|
DEFAULT_VALUES,
|
|
SEARCH_LIMIT_BOUNDS,
|
|
clampSearchLimit,
|
|
deduplicateMemoriesForMode,
|
|
getNamespace,
|
|
} 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("getNamespace", () => {
|
|
it("uses the default namespace when no config is provided", () => {
|
|
expect(getNamespace()).toBe("sm_project_default")
|
|
})
|
|
|
|
it("uses the configured namespace", () => {
|
|
expect(getNamespace({ namespace: "user_1" })).toBe("user_1")
|
|
})
|
|
|
|
it("rejects an empty namespace", () => {
|
|
expect(() => getNamespace({ namespace: " " })).toThrow(
|
|
"non-empty namespace",
|
|
)
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
describe("normalizeBaseUrl", () => {
|
|
it("returns default URL when input is missing or empty", () => {
|
|
expect(normalizeBaseUrl()).toBe("https://api.supermemory.ai")
|
|
expect(normalizeBaseUrl("")).toBe("https://api.supermemory.ai")
|
|
expect(normalizeBaseUrl(" ")).toBe("https://api.supermemory.ai")
|
|
})
|
|
|
|
it("collapses single and multiple trailing slashes", () => {
|
|
expect(normalizeBaseUrl("http://localhost:6768/")).toBe(
|
|
"http://localhost:6768",
|
|
)
|
|
expect(normalizeBaseUrl("http://localhost:6768///")).toBe(
|
|
"http://localhost:6768",
|
|
)
|
|
expect(normalizeBaseUrl("https://api.supermemory.ai/")).toBe(
|
|
"https://api.supermemory.ai",
|
|
)
|
|
})
|
|
|
|
it("trims whitespace around URLs", () => {
|
|
expect(normalizeBaseUrl(" http://localhost:6768/ ")).toBe(
|
|
"http://localhost:6768",
|
|
)
|
|
})
|
|
})
|