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(), }),