From faad07130cc7a0d51777e8869bd33f3d47a905a9 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 4 Nov 2026 22:11:37 +0000 Subject: [PATCH] fix: resolve allOf field types in OpenAPI compatibility guard --- .../cli/generator/flag-names-cli-compat.ts | 24 +++++++++- .../generator/flag-names-cli-compat.test.ts | 45 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/cli/generator/flag-names-cli-compat.ts b/packages/cli/generator/flag-names-cli-compat.ts index ed2384e1cf..2f3c192913 100644 --- a/packages/cli/generator/flag-names-cli-compat.ts +++ b/packages/cli/generator/flag-names-cli-compat.ts @@ -79,8 +79,9 @@ function staleness( if (field === undefined) { return "field missing"; } - if (field["type"] !== entry.type) { - return `field type is now ${JSON.stringify(field["type"])}`; + const type = fieldType(openapi, field); + if (type !== entry.type) { + return `field type is now ${JSON.stringify(type)}`; } const overrides = operation["x-forge-params"] as Overrides | undefined; if (overrides?.[entry.wire]?.["flagName"] !== undefined) { @@ -103,6 +104,25 @@ function bodyField( return deref(openapi, record(schema?.["properties"])?.[wire]); } +function fieldType( + openapi: ForgeOpenApiDocument, + field: Record +): unknown { + let schema: Record | undefined = field; + for (let depth = 0; depth < 10 && schema !== undefined; depth++) { + if (schema["type"] !== undefined) { + return schema["type"]; + } + // Forge can wrap a reference in allOf to attach property annotations. + const allOf = schema["allOf"]; + if (!Array.isArray(allOf) || allOf.length !== 1) { + return undefined; + } + schema = deref(openapi, allOf[0]); + } + return undefined; +} + function deref( openapi: ForgeOpenApiDocument, value: unknown diff --git a/packages/cli/src/__tests__/generator/flag-names-cli-compat.test.ts b/packages/cli/src/__tests__/generator/flag-names-cli-compat.test.ts index 6b485f8a4b..aa8624b299 100644 --- a/packages/cli/src/__tests__/generator/flag-names-cli-compat.test.ts +++ b/packages/cli/src/__tests__/generator/flag-names-cli-compat.test.ts @@ -68,6 +68,33 @@ describe("applyTemporaryFlagNames", () => { }); }); + it.each([ + { type: "string" }, + { $ref: "#/components/schemas/precursor_mode" }, + { allOf: [{ $ref: "#/components/schemas/precursor_mode" }] }, + ])("names a field wrapped in a single allOf: %j", (schema) => { + const doc = document(); + const schemas = doc.components?.["schemas"] as Record; + const mode = { + allOf: [schema], + "x-fern-property-name": "precursor-mode", + }; + schemas["precursor_config"] = { + type: "object", + properties: { mode }, + }; + + applyTemporaryFlagNames(doc); + + expect(overrides(doc, PRECURSOR, "put")).toEqual({ + mode: { flagName: "detection-mode" }, + }); + expect(mode).toEqual({ + allOf: [schema], + "x-fern-property-name": "precursor-mode", + }); + }); + it.each<[string, (doc: ForgeOpenApiDocument) => void, string]>([ [ "a missing operation", @@ -106,6 +133,24 @@ describe("applyTemporaryFlagNames", () => { }, 'field type is now "integer"', ], + [ + "a retyped field wrapped in allOf", + (doc) => { + const schemas = doc.components?.["schemas"] as Record; + schemas["precursor_mode"] = { allOf: [{ type: "integer" }] }; + }, + 'field type is now "integer"', + ], + [ + "a field with conflicting allOf types", + (doc) => { + const schemas = doc.components?.["schemas"] as Record; + schemas["precursor_mode"] = { + allOf: [{ type: "string" }, { type: "integer" }], + }; + }, + "field type is now undefined", + ], [ "an upstream flagName", (doc) => {