fix(lib): parse object-shaped container tag profile entries

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
This commit is contained in:
Sundaram Kumar Jha 2026-10-27 13:43:31 +05:30
parent 225fbf0e0c
commit e45d39d16b
3 changed files with 86 additions and 6 deletions

View file

@ -7,6 +7,7 @@ import {
BulkDeleteMemoriesResponseSchema,
BulkDeleteMemoriesSchema,
ConnectionResponseSchema,
ContainerTagProfileResponseSchema,
ContainerTagSettingsUpdateSchema,
CreateProjectSchema,
DeleteProjectResponseSchema,
@ -312,12 +313,7 @@ export const apiSchema = createSchema({
output: ListContainerTagsResponseSchema,
},
"@get/container-tags/:containerTag/profile": {
output: z.object({
profile: z.object({
static: z.array(z.string()).optional(),
dynamic: z.array(z.string()).optional(),
}),
}),
output: ContainerTagProfileResponseSchema,
params: z.object({
containerTag: z.string(),
}),

View file

@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test"
import { readFileSync } from "node:fs"
import {
BulkDeleteMemoriesSchema,
ContainerTagProfileResponseSchema,
DocumentsWithMemoriesQuerySchema,
ListMemoriesQuerySchema,
SearchRequestSchema,
@ -175,3 +176,61 @@ describe("pagination query schemas", () => {
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([])
})
})

View file

@ -1471,6 +1471,31 @@ export const BulkDeleteMemoriesResponseSchema = z
description: "Response for bulk memory deletion",
})
export const ProfileEntrySchema = z
.object({
id: z.string(),
memory: z.string(),
isStatic: z.boolean().optional(),
isInference: z.boolean().optional(),
version: z.number().optional(),
sourceCount: z.number().optional(),
rootMemoryId: z.string().nullable().optional(),
parentMemoryId: z.string().nullable().optional(),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
metadata: z.record(z.unknown()).nullable().optional(),
})
.passthrough()
export const ContainerTagProfileResponseSchema = z.object({
profile: z
.object({
static: z.array(ProfileEntrySchema).optional(),
dynamic: z.array(ProfileEntrySchema).optional(),
})
.passthrough(),
})
export const ContainerTagListTypeSchema = z
.object({
id: z.string().openapi({