From 3c441235ca0b1dca2a5c72bfc67bb42d5952e6ec Mon Sep 17 00:00:00 2001 From: andreasjansson Date: Thu, 4 Dec 2025 12:09:55 +0100 Subject: [PATCH 1/6] fix: handle inline allOf enum schemas without $ref The allOf can contain either: 1. A $ref pointing to a separate schema 2. An inline enum definition directly The code was assuming $ref always exists, but real schemas often have the enum inlined in allOf[0] directly. --- cog_safe_push/schema.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cog_safe_push/schema.py b/cog_safe_push/schema.py index 703c9c5..914af23 100644 --- a/cog_safe_push/schema.py +++ b/cog_safe_push/schema.py @@ -72,10 +72,15 @@ def check_backwards_compatible( # We allow defaults to be changed elif "allOf" in spec: - choice_schema = model_schemas[spec["allOf"][0]["$ref"].split("/")[-1]] - test_choice_schema = test_model_schemas[ - spec["allOf"][0]["$ref"].split("/")[-1] - ] + if "$ref" in spec["allOf"][0]: + ref_name = spec["allOf"][0]["$ref"].split("/")[-1] + choice_schema = model_schemas[ref_name] + test_ref_name = test_spec["allOf"][0]["$ref"].split("/")[-1] + test_choice_schema = test_model_schemas[test_ref_name] + else: + choice_schema = spec["allOf"][0] + test_choice_schema = test_spec["allOf"][0] + choice_type = choice_schema["type"] test_choice_type = test_choice_schema["type"] if test_choice_type != choice_type: From 0e7d03381c0ee8ef604add8f9bace702a6d48157 Mon Sep 17 00:00:00 2001 From: andreasjansson Date: Thu, 4 Dec 2025 12:10:11 +0100 Subject: [PATCH 2/6] test: add tests for inline allOf enum schemas Added tests for enums defined inline in allOf (without $ref): - test_inline_enum_identical: no changes, should pass - test_inline_enum_added_choice: adding choices is backwards compatible - test_inline_enum_removed_choice: removing choices breaks compatibility - test_inline_enum_changed_type: changing enum type breaks compatibility --- test/test_schema.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_schema.py b/test/test_schema.py index f30eb8b..adecb2e 100644 --- a/test/test_schema.py +++ b/test/test_schema.py @@ -133,7 +133,8 @@ def test_decreased_maximum(): check_backwards_compatible(new, old, train=False) -def test_changed_choice_type(): +def test_changed_choice_type_with_ref(): + """Test enum type change when using $ref.""" old = { "Input": make_input_schema( {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} @@ -155,7 +156,8 @@ def test_changed_choice_type(): check_backwards_compatible(new, old, train=False) -def test_added_choice(): +def test_added_choice_with_ref(): + """Test adding enum choices when using $ref.""" old = { "Input": make_input_schema( {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} From ba5dc00958c8059c1257700085f2c36e3cc4a6e9 Mon Sep 17 00:00:00 2001 From: andreasjansson Date: Thu, 4 Dec 2025 12:11:03 +0100 Subject: [PATCH 3/6] fix: simplify allOf enum handling since schemas are dereferenced Since get_openapi_schema() calls dereference_schema(), all $ref are already resolved by the time check_backwards_compatible is called. The allOf will always contain the inline enum definition directly. --- cog_safe_push/schema.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cog_safe_push/schema.py b/cog_safe_push/schema.py index 914af23..74909dd 100644 --- a/cog_safe_push/schema.py +++ b/cog_safe_push/schema.py @@ -72,14 +72,8 @@ def check_backwards_compatible( # We allow defaults to be changed elif "allOf" in spec: - if "$ref" in spec["allOf"][0]: - ref_name = spec["allOf"][0]["$ref"].split("/")[-1] - choice_schema = model_schemas[ref_name] - test_ref_name = test_spec["allOf"][0]["$ref"].split("/")[-1] - test_choice_schema = test_model_schemas[test_ref_name] - else: - choice_schema = spec["allOf"][0] - test_choice_schema = test_spec["allOf"][0] + choice_schema = spec["allOf"][0] + test_choice_schema = test_spec["allOf"][0] choice_type = choice_schema["type"] test_choice_type = test_choice_schema["type"] From 1f5a70a062517c92a95ed0bbb5854119aa126112 Mon Sep 17 00:00:00 2001 From: andreasjansson Date: Thu, 4 Dec 2025 12:11:21 +0100 Subject: [PATCH 4/6] test: update enum tests to use inline allOf (no $ref) Since schemas are dereferenced before check_backwards_compatible, $ref is never present. Tests now use the realistic inline enum format. --- test/test_schema.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/test/test_schema.py b/test/test_schema.py index adecb2e..3bd76d9 100644 --- a/test/test_schema.py +++ b/test/test_schema.py @@ -133,20 +133,18 @@ def test_decreased_maximum(): check_backwards_compatible(new, old, train=False) -def test_changed_choice_type_with_ref(): - """Test enum type change when using $ref.""" +def test_changed_choice_type(): + """Test enum type change with inline allOf.""" old = { "Input": make_input_schema( - {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} + {"choice": {"allOf": [{"type": "string", "enum": ["A", "B", "C"]}]}} ), - "choice": {"type": "string", "enum": ["A", "B", "C"]}, "Output": {"type": "string"}, } new = { "Input": make_input_schema( - {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} + {"choice": {"allOf": [{"type": "integer", "enum": [1, 2, 3]}]}} ), - "choice": {"type": "integer", "enum": [1, 2, 3]}, "Output": {"type": "string"}, } with pytest.raises( @@ -156,38 +154,35 @@ def test_changed_choice_type_with_ref(): check_backwards_compatible(new, old, train=False) -def test_added_choice_with_ref(): - """Test adding enum choices when using $ref.""" +def test_added_choice(): + """Test adding enum choices is backwards compatible.""" old = { "Input": make_input_schema( - {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} + {"choice": {"allOf": [{"type": "string", "enum": ["A", "B", "C"]}]}} ), - "choice": {"type": "string", "enum": ["A", "B", "C"]}, "Output": {"type": "string"}, } new = { "Input": make_input_schema( - {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} + {"choice": {"allOf": [{"type": "string", "enum": ["A", "B", "C", "D"]}]}} ), - "choice": {"type": "string", "enum": ["A", "B", "C", "D"]}, "Output": {"type": "string"}, } check_backwards_compatible(new, old, train=False) # Should not raise def test_removed_choice(): + """Test removing enum choices breaks compatibility.""" old = { "Input": make_input_schema( - {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} + {"choice": {"allOf": [{"type": "string", "enum": ["A", "B", "C"]}]}} ), - "choice": {"type": "string", "enum": ["A", "B", "C"]}, "Output": {"type": "string"}, } new = { "Input": make_input_schema( - {"choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}} + {"choice": {"allOf": [{"type": "string", "enum": ["A", "B"]}]}} ), - "choice": {"type": "string", "enum": ["A", "B"]}, "Output": {"type": "string"}, } with pytest.raises( From 933d53bc3667bc7a6b91f7c4782b1460e8914fbf Mon Sep 17 00:00:00 2001 From: andreasjansson Date: Thu, 4 Dec 2025 12:11:35 +0100 Subject: [PATCH 5/6] test: update test_multiple_incompatibilities to use inline allOf --- test/test_schema.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_schema.py b/test/test_schema.py index 3bd76d9..4f55217 100644 --- a/test/test_schema.py +++ b/test/test_schema.py @@ -227,10 +227,9 @@ def test_multiple_incompatibilities(): { "text": {"type": "string"}, "number": {"type": "integer", "minimum": 0}, - "choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}, + "choice": {"allOf": [{"type": "string", "enum": ["A", "B", "C"]}]}, } ), - "choice": {"type": "string", "enum": ["A", "B", "C"]}, "Output": {"type": "string"}, } new = { @@ -238,11 +237,10 @@ def test_multiple_incompatibilities(): { "text": {"type": "integer"}, "number": {"type": "integer", "minimum": 1}, - "choice": {"allOf": [{"$ref": "#/components/schemas/choice"}]}, + "choice": {"allOf": [{"type": "string", "enum": ["A", "B"]}]}, "new_required": {"type": "string"}, } ), - "choice": {"type": "string", "enum": ["A", "B"]}, "Output": {"type": "integer"}, } with pytest.raises(IncompatibleSchemaError) as exc_info: From 6fb00fa2fa39b93c7645825c849574afc83243a0 Mon Sep 17 00:00:00 2001 From: andreasjansson Date: Thu, 4 Dec 2025 12:11:59 +0100 Subject: [PATCH 6/6] test: add realistic enum test with full metadata (aspect_ratio example) --- test/test_schema.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/test_schema.py b/test/test_schema.py index 4f55217..9c76dc8 100644 --- a/test/test_schema.py +++ b/test/test_schema.py @@ -191,6 +191,54 @@ def test_removed_choice(): check_backwards_compatible(new, old, train=False) +def test_realistic_enum_with_metadata(): + """Test enum with full realistic metadata (like aspect_ratio).""" + old = { + "Input": make_input_schema( + { + "aspect_ratio": { + "allOf": [ + { + "enum": ["1:1", "2:3", "3:2", "4:3", "16:9"], + "type": "string", + "title": "aspect_ratio", + "description": "An enumeration.", + } + ], + "default": "1:1", + "x-order": 2, + "description": "Aspect ratio for expansion.", + } + } + ), + "Output": {"type": "string", "title": "Output", "format": "uri"}, + } + new = { + "Input": make_input_schema( + { + "aspect_ratio": { + "allOf": [ + { + "enum": ["1:1", "2:3", "3:2", "4:3"], # removed 16:9 + "type": "string", + "title": "aspect_ratio", + "description": "An enumeration.", + } + ], + "default": "1:1", + "x-order": 2, + "description": "Aspect ratio for expansion.", + } + } + ), + "Output": {"type": "string", "title": "Output", "format": "uri"}, + } + with pytest.raises( + IncompatibleSchemaError, match="Input aspect_ratio is missing choices: '16:9'" + ): + check_backwards_compatible(new, old, train=False) + + def test_new_required_input(): old = { "Input": make_input_schema({"text": {"type": "string"}}),