From c7ed990769f792ed5df8fcdee065cd21770021e7 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Wed, 10 Jun 2026 15:58:30 -0700 Subject: [PATCH] fix: preserve non-oneOf schema array order (#91891) --- .../lib/codex-app-server-protocol-source.ts | 17 +++++-- .../codex-app-server-protocol-source.test.ts | 50 +++++++++++++++---- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/scripts/lib/codex-app-server-protocol-source.ts b/scripts/lib/codex-app-server-protocol-source.ts index 32d71cb2d4e..f2ffe347841 100644 --- a/scripts/lib/codex-app-server-protocol-source.ts +++ b/scripts/lib/codex-app-server-protocol-source.ts @@ -385,10 +385,19 @@ export function normalizeGeneratedTypeScript(text: string): string { .replaceAll("| null | null", "| null"); } -export function canonicalizeCodexAppServerProtocolJson(value: unknown): unknown { +// Sort typed-object arrays for schema keywords whose item order does not affect +// payload validity; preserve order everywhere else, especially prefixItems. +const typeSortedSchemaArrayKeys = new Set(["anyOf", "enum", "oneOf", "required"]); + +export function canonicalizeCodexAppServerProtocolJson( + value: unknown, + parentKey?: string, +): unknown { if (Array.isArray(value)) { - const items = value.map(canonicalizeCodexAppServerProtocolJson); - return sortCodexProtocolJsonArrayByType(items); + const items = value.map((item) => canonicalizeCodexAppServerProtocolJson(item)); + return parentKey !== undefined && typeSortedSchemaArrayKeys.has(parentKey) + ? sortCodexProtocolJsonArrayByType(items) + : items; } if (!isPlainObject(value)) { @@ -397,7 +406,7 @@ export function canonicalizeCodexAppServerProtocolJson(value: unknown): unknown const sorted: Record = {}; const entries = Object.entries(value) - .map(([key, child]) => [key, canonicalizeCodexAppServerProtocolJson(child)] as const) + .map(([key, child]) => [key, canonicalizeCodexAppServerProtocolJson(child, key)] as const) .toSorted(([left], [right]) => { if (left < right) { return -1; diff --git a/test/scripts/codex-app-server-protocol-source.test.ts b/test/scripts/codex-app-server-protocol-source.test.ts index 4118bf7f2a3..b34aa172022 100644 --- a/test/scripts/codex-app-server-protocol-source.test.ts +++ b/test/scripts/codex-app-server-protocol-source.test.ts @@ -206,16 +206,31 @@ describe("Codex app-server protocol JSON canonicalizer", () => { `); }); - it("sorts arrays only when plain object items expose top-level type values", () => { + it("sorts typed-object arrays only for order-insensitive schema keywords", () => { expect( canonicalizeCodexAppServerProtocolJson({ - enum: ["z", "a"], + anyOf: [ + { z: 1, type: "string" }, + { type: "integer", a: 2 }, + ], + enum: [ + { z: 1, type: "z" }, + { type: "a", a: 2 }, + ], mixed: [{ type: "b" }, "item", { type: "a" }], oneOf: [ - { title: "Second", z: true }, - { a: true, title: "First" }, + { type: "object", z: true }, + { a: true, type: "array" }, + { type: "object", z: false }, + ], + prefixItems: [ + { z: 1, type: "string" }, + { type: "number", a: 2 }, + ], + required: [ + { z: 1, type: "z" }, + { type: "a", a: 2 }, ], - required: ["z", "a"], typed: [ { type: "beta", z: 1 }, { type: "alpha", z: 2 }, @@ -223,16 +238,31 @@ describe("Codex app-server protocol JSON canonicalizer", () => { ], }), ).toEqual({ - enum: ["z", "a"], + anyOf: [ + { a: 2, type: "integer" }, + { type: "string", z: 1 }, + ], + enum: [ + { a: 2, type: "a" }, + { type: "z", z: 1 }, + ], mixed: [{ type: "b" }, "item", { type: "a" }], oneOf: [ - { title: "Second", z: true }, - { a: true, title: "First" }, + { a: true, type: "array" }, + { type: "object", z: true }, + { type: "object", z: false }, + ], + prefixItems: [ + { type: "string", z: 1 }, + { a: 2, type: "number" }, + ], + required: [ + { a: 2, type: "a" }, + { type: "z", z: 1 }, ], - required: ["z", "a"], typed: [ - { type: "alpha", z: 2 }, { type: "beta", z: 1 }, + { type: "alpha", z: 2 }, { type: "beta", z: 3 }, ], });