From e45d39d16b3e967fe3cb716a75e2532202d84c72 Mon Sep 17 00:00:00 2001 From: Sundaram Kumar Jha Date: Tue, 27 Oct 2026 13:43:31 +0530 Subject: [PATCH] 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 --- packages/lib/api.ts | 8 ++--- packages/validation/api.test.ts | 59 +++++++++++++++++++++++++++++++++ packages/validation/api.ts | 25 ++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packages/lib/api.ts b/packages/lib/api.ts index 7cf8755f..1d56ba0e 100644 --- a/packages/lib/api.ts +++ b/packages/lib/api.ts @@ -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(), }), diff --git a/packages/validation/api.test.ts b/packages/validation/api.test.ts index fa52ed89..aa561fae 100644 --- a/packages/validation/api.test.ts +++ b/packages/validation/api.test.ts @@ -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).futureEntryField, + ).toBe("kept") + expect((parsed.profile as Record).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([]) + }) +}) diff --git a/packages/validation/api.ts b/packages/validation/api.ts index ae6ac319..d2bf846e 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -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({