mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-08 08:07:17 +00:00
The profile endpoint returns entries shaped like { id, memory, ... },
but the $fetch output schema expected string[] and threw for every
non-empty profile.
Add shared ProfileEntrySchema and ContainerTagProfileResponseSchema,
keep unknown fields, and cover object-shaped and empty profiles.
Refs #1849
236 lines
6.4 KiB
TypeScript
236 lines
6.4 KiB
TypeScript
import { describe, expect, it } from "bun:test"
|
|
import { readFileSync } from "node:fs"
|
|
import {
|
|
BulkDeleteMemoriesSchema,
|
|
ContainerTagProfileResponseSchema,
|
|
DocumentsWithMemoriesQuerySchema,
|
|
ListMemoriesQuerySchema,
|
|
SearchRequestSchema,
|
|
Searchv4RequestSchema,
|
|
} from "./api"
|
|
|
|
describe("search threshold schemas", () => {
|
|
it("do not contain redundant number transforms or unreachable range guards", () => {
|
|
const source = readFileSync(new URL("./api.ts", import.meta.url), "utf8")
|
|
const searchSchemas = source.slice(
|
|
source.indexOf("export const SearchRequestSchema"),
|
|
source.indexOf("export const SearchResultSchema"),
|
|
)
|
|
|
|
expect(searchSchemas).not.toContain(".transform(Number)")
|
|
expect(searchSchemas).not.toContain("v === undefined || (v >= 0 && v <= 1)")
|
|
})
|
|
|
|
it("preserves threshold defaults", () => {
|
|
const search = SearchRequestSchema.parse({ q: "memory" })
|
|
const searchV4 = Searchv4RequestSchema.parse({ q: "memory" })
|
|
|
|
expect(search.chunkThreshold).toBe(0)
|
|
expect(search.documentThreshold).toBe(0)
|
|
expect(searchV4.threshold).toBe(0.6)
|
|
})
|
|
|
|
it.each([0, 0.5, 1])("accepts inclusive threshold value %p", (threshold) => {
|
|
expect(
|
|
SearchRequestSchema.parse({
|
|
q: "memory",
|
|
chunkThreshold: threshold,
|
|
documentThreshold: threshold,
|
|
}),
|
|
).toMatchObject({
|
|
chunkThreshold: threshold,
|
|
documentThreshold: threshold,
|
|
})
|
|
expect(
|
|
Searchv4RequestSchema.parse({ q: "memory", threshold }).threshold,
|
|
).toBe(threshold)
|
|
})
|
|
|
|
it.each([
|
|
-0.1, 1.1,
|
|
])("rejects out-of-range threshold value %p", (threshold) => {
|
|
expect(
|
|
SearchRequestSchema.safeParse({
|
|
q: "memory",
|
|
chunkThreshold: threshold,
|
|
}).success,
|
|
).toBe(false)
|
|
expect(
|
|
SearchRequestSchema.safeParse({
|
|
q: "memory",
|
|
documentThreshold: threshold,
|
|
}).success,
|
|
).toBe(false)
|
|
expect(
|
|
Searchv4RequestSchema.safeParse({ q: "memory", threshold }).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it("does not coerce threshold strings", () => {
|
|
expect(
|
|
SearchRequestSchema.safeParse({
|
|
q: "memory",
|
|
chunkThreshold: "0.5",
|
|
}).success,
|
|
).toBe(false)
|
|
expect(
|
|
SearchRequestSchema.safeParse({
|
|
q: "memory",
|
|
documentThreshold: "0.5",
|
|
}).success,
|
|
).toBe(false)
|
|
expect(
|
|
Searchv4RequestSchema.safeParse({
|
|
q: "memory",
|
|
threshold: "0.5",
|
|
}).success,
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("pagination query schemas", () => {
|
|
it("preserve page/limit defaults", () => {
|
|
const listed = ListMemoriesQuerySchema.parse({})
|
|
expect(listed.page).toBe(1)
|
|
expect(listed.limit).toBe(10)
|
|
|
|
const docs = DocumentsWithMemoriesQuerySchema.parse({})
|
|
expect(docs.page).toBe(1)
|
|
expect(docs.limit).toBe(10)
|
|
})
|
|
|
|
it.each([
|
|
1, 50, 1100,
|
|
])("ListMemoriesQuerySchema accepts numeric limit %p", (limit) => {
|
|
expect(ListMemoriesQuerySchema.parse({ limit }).limit).toBe(limit)
|
|
})
|
|
|
|
it("ListMemoriesQuerySchema accepts numeric string page/limit", () => {
|
|
const parsed = ListMemoriesQuerySchema.parse({ page: "3", limit: "25" })
|
|
expect(parsed.page).toBe(3)
|
|
expect(parsed.limit).toBe(25)
|
|
})
|
|
|
|
it.each([
|
|
0, -5, 2.5,
|
|
])("ListMemoriesQuerySchema rejects non-positive or fractional numeric limit %p", (limit) => {
|
|
expect(ListMemoriesQuerySchema.safeParse({ limit }).success).toBe(false)
|
|
})
|
|
|
|
it.each([
|
|
0, -1, 1.5,
|
|
])("ListMemoriesQuerySchema rejects non-positive or fractional numeric page %p", (page) => {
|
|
expect(ListMemoriesQuerySchema.safeParse({ page }).success).toBe(false)
|
|
})
|
|
|
|
it("ListMemoriesQuerySchema still caps limit at 1100", () => {
|
|
expect(ListMemoriesQuerySchema.safeParse({ limit: 1101 }).success).toBe(
|
|
false,
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
0, -1, 2.5,
|
|
])("DocumentsWithMemoriesQuerySchema rejects invalid page %p", (page) => {
|
|
expect(DocumentsWithMemoriesQuerySchema.safeParse({ page }).success).toBe(
|
|
false,
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
0, -10, 2.5,
|
|
])("DocumentsWithMemoriesQuerySchema rejects invalid limit %p", (limit) => {
|
|
expect(DocumentsWithMemoriesQuerySchema.safeParse({ limit }).success).toBe(
|
|
false,
|
|
)
|
|
})
|
|
|
|
it("DocumentsWithMemoriesQuerySchema accepts a normal request", () => {
|
|
const parsed = DocumentsWithMemoriesQuerySchema.parse({
|
|
page: 2,
|
|
limit: 50,
|
|
})
|
|
expect(parsed.page).toBe(2)
|
|
expect(parsed.limit).toBe(50)
|
|
})
|
|
|
|
it("DocumentsWithMemoriesQuerySchema caps limit at 1000", () => {
|
|
expect(
|
|
DocumentsWithMemoriesQuerySchema.safeParse({ limit: 1001 }).success,
|
|
).toBe(false)
|
|
expect(
|
|
DocumentsWithMemoriesQuerySchema.safeParse({ limit: 200 }).success,
|
|
).toBe(true)
|
|
})
|
|
|
|
it("BulkDeleteMemoriesSchema caps containerTags at 100 entries of bounded length", () => {
|
|
const tooMany = {
|
|
containerTags: Array.from({ length: 101 }, (_, i) => `tag_${i}`),
|
|
}
|
|
expect(BulkDeleteMemoriesSchema.safeParse(tooMany).success).toBe(false)
|
|
|
|
const tagTooLong = { containerTags: ["x".repeat(257)] }
|
|
expect(BulkDeleteMemoriesSchema.safeParse(tagTooLong).success).toBe(false)
|
|
|
|
const ok = { containerTags: ["tag_a", "tag_b"] }
|
|
expect(BulkDeleteMemoriesSchema.safeParse(ok).success).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("ContainerTagProfileResponseSchema", () => {
|
|
const entry = {
|
|
id: "mem_123",
|
|
memory: "Nora owns the billing pipeline.",
|
|
isStatic: true,
|
|
isInference: false,
|
|
version: 1,
|
|
sourceCount: 2,
|
|
rootMemoryId: "mem_root",
|
|
parentMemoryId: null,
|
|
createdAt: "2026-10-27T06:52:34.160Z",
|
|
updatedAt: "2026-10-27T06:52:34.160Z",
|
|
metadata: { temporalContext: { documentDate: "2026-10-27" } },
|
|
}
|
|
|
|
it("parses the object-shaped entries returned by the API", () => {
|
|
const parsed = ContainerTagProfileResponseSchema.parse({
|
|
profile: {
|
|
static: [entry],
|
|
dynamic: [{ ...entry, isStatic: false, id: "mem_456" }],
|
|
},
|
|
})
|
|
|
|
expect(parsed.profile.static?.[0]).toMatchObject({
|
|
id: "mem_123",
|
|
memory: "Nora owns the billing pipeline.",
|
|
isStatic: true,
|
|
sourceCount: 2,
|
|
})
|
|
expect(parsed.profile.dynamic?.[0]?.id).toBe("mem_456")
|
|
})
|
|
|
|
it("preserves unknown entry and profile fields", () => {
|
|
const parsed = ContainerTagProfileResponseSchema.parse({
|
|
profile: {
|
|
static: [{ ...entry, futureEntryField: "kept" }],
|
|
buckets: { work: [entry] },
|
|
},
|
|
})
|
|
|
|
expect(
|
|
(parsed.profile.static?.[0] as Record<string, unknown>).futureEntryField,
|
|
).toBe("kept")
|
|
expect((parsed.profile as Record<string, unknown>).buckets).toEqual({
|
|
work: [entry],
|
|
})
|
|
})
|
|
|
|
it("accepts an empty profile", () => {
|
|
const parsed = ContainerTagProfileResponseSchema.parse({
|
|
profile: { static: [], dynamic: [] },
|
|
})
|
|
|
|
expect(parsed.profile.static).toEqual([])
|
|
expect(parsed.profile.dynamic).toEqual([])
|
|
})
|
|
})
|