Skip to content

Commit 76bd8e3

Browse files
authored
test: fix flaky key-order assertions in structured output tests (#2537)
Remove non-deterministic assertions on JSON key ordering in model responses. LLMs do not guarantee response key order matches schema property order. Tests now validate structured output correctness (valid JSON with expected keys and types) instead. Rename module/class/methods to reflect the new intent: - test_ResponseFormatOrder.py -> test_StructuredOutput.py - TestResponseFormatOrder -> TestStructuredOutput - test_*_reason_then_ans -> test_*_structured_output_reason_first - test_*_ans_then_reason -> test_*_structured_output_ans_first Extract shared _make_prompt helper to reduce test boilerplate. Schema ordering preservation is already covered by the Scala ResponseFormatOrderSuite unit tests.
1 parent 832a4f6 commit 76bd8e3

2 files changed

Lines changed: 114 additions & 186 deletions

File tree

cognitive/src/test/python/synapsemltest/services/openai/test_ResponseFormatOrder.py

Lines changed: 0 additions & 186 deletions
This file was deleted.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright (C) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License. See LICENSE in project root for information.
3+
4+
import json
5+
import subprocess
6+
import unittest
7+
8+
from pyspark.sql import SQLContext
9+
10+
from synapse.ml.core.init_spark import init_spark
11+
from synapse.ml.services.openai import OpenAIDefaults, OpenAIPrompt
12+
13+
spark = init_spark()
14+
sc = SQLContext(spark.sparkContext)
15+
16+
17+
def _make_json_schema(reason_first: bool) -> dict:
18+
props = {
19+
"ans": {"type": "string"},
20+
"reason": {"type": "string"},
21+
}
22+
if reason_first:
23+
props = {
24+
"reason": {"type": "string"},
25+
"ans": {"type": "string"},
26+
}
27+
28+
return {
29+
"type": "object",
30+
"properties": props,
31+
"required": list(props.keys()),
32+
"additionalProperties": False,
33+
}
34+
35+
36+
def _assert_valid_response(test_case, text):
37+
"""Validate that the model response is valid JSON with the expected keys.
38+
39+
We intentionally do NOT assert on the ordering of keys in the model
40+
response. OpenAI does not guarantee that response key order matches schema
41+
property order. Schema ordering preservation (Python dict -> LinkedHashMap
42+
-> serialized JSON request) is tested by the Scala ResponseFormatOrderSuite.
43+
"""
44+
test_case.assertIsInstance(text, str, "Response should be a string")
45+
test_case.assertTrue(len(text) > 0, "Response should not be empty")
46+
json_response = json.loads(text)
47+
test_case.assertIsInstance(
48+
json_response, dict, f"Response should be a JSON object: {text}"
49+
)
50+
test_case.assertIn("reason", json_response, f"Missing 'reason' key: {text}")
51+
test_case.assertIn("ans", json_response, f"Missing 'ans' key: {text}")
52+
test_case.assertIsInstance(
53+
json_response["reason"], str, f"'reason' should be a string: {text}"
54+
)
55+
test_case.assertIsInstance(
56+
json_response["ans"], str, f"'ans' should be a string: {text}"
57+
)
58+
59+
60+
class TestStructuredOutput(unittest.TestCase):
61+
@classmethod
62+
def setUpClass(cls):
63+
defaults = OpenAIDefaults()
64+
defaults.reset_model()
65+
66+
cls.subscriptionKey = json.loads(
67+
subprocess.check_output(
68+
"az keyvault secret show --vault-name mmlspark-build-keys"
69+
" --name openai-api-key-2",
70+
shell=True,
71+
)
72+
)["value"]
73+
cls.url = "https://synapseml-openai-2.openai.azure.com/"
74+
cls.api_version = "2025-04-01-preview"
75+
cls.deploymentName = "gpt-4.1-mini"
76+
77+
cls.df = spark.createDataFrame([("Paris", "City")], ["text", "category"])
78+
79+
def _make_prompt(self, api_type, reason_first):
80+
return (
81+
OpenAIPrompt()
82+
.setPromptTemplate("List 2 {category}: {text},")
83+
.setApiType(api_type)
84+
.setResponseFormat(_make_json_schema(reason_first=reason_first))
85+
.setOutputCol("out")
86+
.setDeploymentName(self.deploymentName)
87+
.setApiVersion(self.api_version)
88+
.setUrl(self.url)
89+
.setSubscriptionKey(self.subscriptionKey)
90+
)
91+
92+
def test_chat_structured_output_reason_first(self):
93+
prompt = self._make_prompt("chat_completions", reason_first=True)
94+
text = prompt.transform(self.df).select("out").first()[0]
95+
_assert_valid_response(self, text)
96+
97+
def test_chat_structured_output_ans_first(self):
98+
prompt = self._make_prompt("chat_completions", reason_first=False)
99+
text = prompt.transform(self.df).select("out").first()[0]
100+
_assert_valid_response(self, text)
101+
102+
def test_responses_structured_output_reason_first(self):
103+
prompt = self._make_prompt("responses", reason_first=True)
104+
text = prompt.transform(self.df).select("out").first()[0]
105+
_assert_valid_response(self, text)
106+
107+
def test_responses_structured_output_ans_first(self):
108+
prompt = self._make_prompt("responses", reason_first=False)
109+
text = prompt.transform(self.df).select("out").first()[0]
110+
_assert_valid_response(self, text)
111+
112+
113+
if __name__ == "__main__":
114+
unittest.main()

0 commit comments

Comments
 (0)