From 2c83c6ae1154033a589bf809508349823c684f80 Mon Sep 17 00:00:00 2001 From: "Eugenio \"Jay\" Zuccarelli" <11176606+jayzuccarelli@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:39:58 +0000 Subject: [PATCH] fix: keep extension fields on CreateTaskResult and GetTaskResult Both results extend the open Result type, but their v2 schemas were plain z.object wrappers, so unknown top-level keys were stripped on the polling path. The notification params schema already uses openObject for the same reason. Use it for both results as well, so projectTask() sees the same extension fields whether a task arrives by tasks/get or notifications/tasks. Co-Authored-By: Claude Opus 5.5 --- packages/ext-tasks/src/core/v2/index.test.ts | 23 ++++++++++++++++++++ packages/ext-tasks/src/core/v2/schemas.ts | 6 +++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/ext-tasks/src/core/v2/index.test.ts b/packages/ext-tasks/src/core/v2/index.test.ts index 00208fe..056207c 100644 --- a/packages/ext-tasks/src/core/v2/index.test.ts +++ b/packages/ext-tasks/src/core/v2/index.test.ts @@ -725,6 +725,29 @@ describe("V2 runtime wire contracts", () => { ).toBe(false); }); + it("keeps extension fields on task creation and tasks/get results", () => { + // Both results extend the open pinned Result type, like the notification + // params: stripping unknown top-level keys here would hide extensions from + // projectTask() on the polling path only. + const [task] = fc.sample(taskFor("working"), 1); + const created = CreateTaskResultV2Schema.safeParse( + asJson({ ...task, resultType: "task", "vendor/trace": { hop: 1 } }), + ); + const fetched = GetTaskResultV2Schema.safeParse( + asJson({ ...task, resultType: "complete", "vendor/trace": { hop: 1 } }), + ); + expect(created.success).toBe(true); + if (created.success) + expect((created.data as Record)["vendor/trace"]).toEqual( + { hop: 1 }, + ); + expect(fetched.success).toBe(true); + if (fetched.success) + expect((fetched.data as Record)["vendor/trace"]).toEqual( + { hop: 1 }, + ); + }); + it("decodes detailed task notifications with exact envelope discriminators", () => { fc.assert( fc.property(taskFor("working"), (task) => { diff --git a/packages/ext-tasks/src/core/v2/schemas.ts b/packages/ext-tasks/src/core/v2/schemas.ts index 51c182d..0370ffb 100644 --- a/packages/ext-tasks/src/core/v2/schemas.ts +++ b/packages/ext-tasks/src/core/v2/schemas.ts @@ -365,7 +365,9 @@ const DetailedTaskV2Schema = z.discriminatedUnion("status", [ CancelledTaskV2Schema, ]); -const CreateTaskResultV2Schema = z.object({ +// CreateTaskResult and GetTaskResult extend the open pinned Result type, so +// they keep unknown top-level keys, like the notification params below. +const CreateTaskResultV2Schema = openObject({ ...TaskBaseShape, status: TaskStatusV2Schema, resultType: z.literal("task"), @@ -392,7 +394,7 @@ const UpdateTaskRequestV2Schema = z.object({ const CancelTaskRequestV2Schema = taskIdRequestV2Schema("tasks/cancel"); const GetTaskResultV2Schema = z.intersection( DetailedTaskV2Schema, - z.object({ + openObject({ resultType: CompleteResultTypeSchema, _meta: MetaSchema.optional(), }),