supermemory/packages/validation/api.test.ts
Sundaram Kumar Jha 749b40a802 fix(validation): accept ISO sourceAddedAt in documents schema
The API returns memoryEntries[].sourceAddedAt as an ISO-8601 string.
z.date() rejected it, causing DocumentsWithMemoriesResponseSchema and
$fetch("@post/documents/documents") to fail on live responses.

Coerce the wire value to a Date and cover ISO/null sourceAddedAt values.

Refs #1847
2026-10-27 13:10:28 +05:30

236 lines
6.3 KiB
TypeScript

import { describe, expect, it } from "bun:test"
import { readFileSync } from "node:fs"
import {
BulkDeleteMemoriesSchema,
DocumentsWithMemoriesQuerySchema,
DocumentsWithMemoriesResponseSchema,
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("DocumentsWithMemoriesResponseSchema", () => {
const document = {
id: "doc_123",
orgId: "org_123",
userId: "user_123",
createdAt: "2026-10-27T06:48:08.112Z",
updatedAt: "2026-10-27T06:48:13.638Z",
memoryEntries: [
{
id: "mem_123",
memory: "Nora owns the billing pipeline.",
spaceId: "space_123",
orgId: "org_123",
createdAt: "2026-10-27T06:52:34.160Z",
updatedAt: "2026-10-27T06:52:34.160Z",
sourceAddedAt: "2026-10-27T06:52:34.160Z",
sourceRelevanceScore: 0.87,
sourceMetadata: null,
spaceContainerTag: "space_123",
},
],
}
const pagination = {
currentPage: 1,
limit: 10,
totalItems: 1,
totalPages: 1,
}
it("coerces sourceAddedAt ISO strings returned by the API", () => {
const parsed = DocumentsWithMemoriesResponseSchema.parse({
documents: [document],
pagination,
})
const sourceAddedAt = parsed.documents[0]?.memoryEntries[0]?.sourceAddedAt
expect(sourceAddedAt).toBeInstanceOf(Date)
expect(sourceAddedAt?.toISOString()).toBe("2026-10-27T06:52:34.160Z")
})
it("accepts a null sourceAddedAt", () => {
const parsed = DocumentsWithMemoriesResponseSchema.parse({
documents: [
{
...document,
memoryEntries: [
{ ...document.memoryEntries[0], sourceAddedAt: null },
],
},
],
pagination,
})
expect(parsed.documents[0]?.memoryEntries[0]?.sourceAddedAt).toBeNull()
})
})