Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/poly/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from poly.cli_commands.rtc import RTCCommand
from poly.cli_commands.sync import (
DiffCommand,
FetchCommand,
FormatCommand,
PullCommand,
PushCommand,
Expand All @@ -45,6 +46,7 @@
StudioCommand,
ProjectCommand,
TemplateCommand,
FetchCommand,
PullCommand,
PushCommand,
StatusCommand,
Expand Down
4 changes: 3 additions & 1 deletion src/poly/resources/api_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ def to_yaml_dict(self) -> dict:
"name": self.name,
"description": self.description,
"environments": self.environments.to_yaml_dict(),
"operations": [op.to_yaml_dict() for op in self.operations],
"operations": [
op.to_yaml_dict() for op in sorted(self.operations, key=lambda op: op.name)
],
}

@classmethod
Expand Down
5 changes: 4 additions & 1 deletion src/poly/resources/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,10 @@ def to_yaml_dict(self) -> dict:
output.update(flow_settings_dict)

if self.step_type == StepType.DEFAULT_STEP:
output["conditions"] = [condition.to_yaml_dict() for condition in self.conditions]
output["conditions"] = [
condition.to_yaml_dict()
for condition in sorted(self.conditions, key=lambda condition: condition.name)
]
output["extracted_entities"] = sorted(self.extracted_entities)

output["prompt"] = self.prompt
Expand Down
21 changes: 16 additions & 5 deletions src/poly/resources/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,13 @@ def __init__(self, name: str, arguments: list[FunctionCallArgumentAssertion | di
]

def to_yaml_dict(self) -> dict:
return {"name": self.name, "arguments": [arg.to_yaml_dict() for arg in self.arguments]}
return {
"name": self.name,
"arguments": [
arg.to_yaml_dict()
for arg in sorted(self.arguments, key=lambda arg: arg.parameter_name)
],
}

def to_proto(self) -> FunctionCallAssertionProto:
return FunctionCallAssertionProto(
Expand Down Expand Up @@ -166,7 +172,8 @@ def to_yaml_dict(self) -> dict:
response["prompt_assertions"] = self.prompts
if self.function_calls:
response["function_call_assertions"] = [
function_call.to_yaml_dict() for function_call in self.function_calls
function_call.to_yaml_dict()
for function_call in sorted(self.function_calls, key=lambda call: call.name)
]
return response

Expand Down Expand Up @@ -481,12 +488,16 @@ class TestCaseApiMocks:
mocks: dict[str, dict[str, list[ApiResponseRule]]] = field(default_factory=dict)

def to_yaml_dict(self) -> dict:
# Integration and operation names are sorted so pulls produce stable YAML; the
# rules within an operation are a sequence (see `repeat`) and keep their order.
return {
integration_name: {
operation_name: [rule.to_yaml_dict() for rule in rules]
for operation_name, rules in operations.items()
operation_name: [
rule.to_yaml_dict() for rule in self.mocks[integration_name][operation_name]
]
for operation_name in sorted(self.mocks[integration_name])
}
for integration_name, operations in self.mocks.items()
for integration_name in sorted(self.mocks)
}

@classmethod
Expand Down
102 changes: 102 additions & 0 deletions src/poly/tests/resources_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2551,6 +2551,35 @@ def test_get_raw_no_code_step(self):
"""Test that raw property returns correct YAML representation for no code step."""
self.assertEqual(TEST_NO_CODE_FLOW_STEP.raw, FLOW_NO_CODE_STEP_RAW)

def test_conditions_sorted_by_name(self):
"""Test that conditions are serialized in alphabetical order by name."""
step = FlowStep(
resource_id="flow-123_step-1",
step_id="step-1",
name="Test Step",
flow_id="flow-123",
flow_name="Test Flow",
step_type=StepType.DEFAULT_STEP,
conditions=[
Condition(
resource_id=f"cond-{name}",
name=name,
description="",
condition_type=ConditionType.STEP,
child_step="step-2",
step_id="step-1",
flow_id="flow-123",
)
for name in ["zebra", "apple", "monkey"]
],
prompt="Hello, how can I help you?",
position={"x": 0.0, "y": 0.0},
extracted_entities=[],
)

condition_names = [c["name"] for c in step.to_yaml_dict()["conditions"]]
self.assertEqual(condition_names, ["apple", "monkey", "zebra"])

def test_to_pretty(self):
"""Test converting flow step to pretty format with function name mapping."""
resource_mappings = [
Expand Down Expand Up @@ -5122,6 +5151,22 @@ def test_api_integration_to_yaml_dict_and_from_yaml_dict(self):
self.assertEqual(i2.operations[0].name, "get")
self.assertEqual(i2.operations[0].resource_id, "")

def test_api_integration_operations_sorted_by_name(self):
"""Operations serialize in alphabetical order by name."""
integration = ApiIntegration(
resource_id="int-1",
name="TestAPI",
operations=[
ApiIntegrationOperation(
resource_id=f"op-{name}", name=name, method="GET", resource="/x"
)
for name in ["refund", "charge", "get_customer"]
],
)

operation_names = [op["name"] for op in integration.to_yaml_dict()["operations"]]
self.assertEqual(operation_names, ["charge", "get_customer", "refund"])

def test_api_integration_build_protos(self):
"""ApiIntegration build_create_proto, build_update_proto, build_delete_proto set id and environments."""
env = ApiIntegrationEnvironments.from_dict(
Expand Down Expand Up @@ -7179,6 +7224,39 @@ def _sample_test_case(self) -> TestCase:
language="en-GB",
)

def test_assertions_sorted_by_name(self):
"""Function call assertions and their arguments serialize in alphabetical order."""
assertions = TestCaseAssertion(
resource_id="TEST-ordering",
name="assertions",
prompts=[],
function_calls=[
FunctionCallAssertion(
name=name,
arguments=[
FunctionCallArgumentAssertion(
parameter_name=parameter_name,
expected_value="value",
value_type="string",
)
for parameter_name in ["zebra", "apple", "monkey"]
],
)
for name in ["transfer_call", "book_appointment", "lookup_order"]
],
)

function_calls = assertions.to_yaml_dict()["function_call_assertions"]

self.assertEqual(
[call["name"] for call in function_calls],
["book_appointment", "lookup_order", "transfer_call"],
)
self.assertEqual(
[arg["parameter_name"] for arg in function_calls[0]["arguments"]],
["apple", "monkey", "zebra"],
)

def test_to_yaml_dict_from_yaml_dict_roundtrip(self):
test_case = self._sample_test_case()
yaml_dict = test_case.to_yaml_dict()
Expand Down Expand Up @@ -7967,6 +8045,30 @@ def test_omits_empty_values_from_yaml(self):

self.assertNotIn("api_mocks", yaml_dict)

def test_yaml_sorts_integration_and_operation_names(self):
"""Integration/operation names serialize alphabetically; rule order is preserved."""
api_mocks = self._api_mocks(
{
"payments": {
"refund": [ApiResponseRule(respond=ApiResponse(status=200))],
"charge": [
ApiResponseRule(respond=ApiResponse(status=500), repeat=2),
ApiResponseRule(respond=ApiResponse(status=201)),
],
},
"crm": {"get_customer": [ApiResponseRule(respond=ApiResponse(status=200))]},
}
)

mocks_yaml = self._test_case(api_mocks=api_mocks).to_yaml_dict()["api_mocks"]

self.assertEqual(list(mocks_yaml), ["crm", "payments"])
self.assertEqual(list(mocks_yaml["payments"]), ["charge", "refund"])
self.assertEqual(
mocks_yaml["payments"]["charge"],
[{"respond": {"status": 500}, "repeat": 2}, {"respond": {"status": 201}}],
)

def _operation_mock(self, **overrides) -> TestCaseApiOperationMock:
defaults = {
"resource_id": f"{self.RESOURCE_ID}:crm:get_customer",
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading