feat(automate): allow automation results with no affected objects (#488)

* allow empty affected objects

* adds unit tests for `attach_result_to_objects` method

Introduces tests for handling empty object lists and objects with IDs.

Enhances error handling for cases where objects lack IDs, ensuring robustness in the functionality.

Confirms that the method correctly appends results under various scenarios.

* line length
This commit is contained in:
Jonathon Broughton
2026-02-24 20:21:59 +00:00
committed by GitHub
parent c1f82fa0d2
commit aa16234e7f
2 changed files with 119 additions and 9 deletions
+9 -9
View File
@@ -493,29 +493,29 @@ class AutomationContext:
Args:
level: Result level.
category (str): A short tag for the event type.
affected_objects (Union[Base, List[Base]]): A single object or a list of
objects that are causing the info case.
affected_objects (Union[Base, List[Base]]): A single object, a list of
objects, or an empty list. When empty, a result case is still
appended with no object IDs (e.g. for skipped rules or version-level
messages).
message (Optional[str]): Optional message.
metadata: User provided metadata key value pairs
visual_overrides: Case specific 3D visual overrides.
"""
if isinstance(affected_objects, list):
if len(affected_objects) < 1:
raise ValueError(
f"Need atleast one object to report a(n) {level.value.upper()}"
)
object_list = affected_objects
else:
object_list = [affected_objects]
ids: Dict[str, Optional[str]] = {}
# When objects are provided, each must have an id (empty list allowed for
# version-level/skipped results).
for o in object_list:
# validate that the Base.id is not None. If its a None, throw an Exception
if not o.id:
if not getattr(o, "id", None):
raise Exception(
f"You can only attach {level} results to objects with an id."
)
ids[o.id] = o.applicationId
ids[o.id] = getattr(o, "applicationId", None)
print(
f"Created new {level.value.upper()}"
f" category: {category} caused by: {message}"
@@ -0,0 +1,110 @@
"""Unit tests for AutomationContext.attach_result_to_objects contract."""
from unittest.mock import MagicMock
import pytest
from speckle_automate import AutomationContext
from speckle_automate.schema import (
AutomationRunData,
ObjectResultLevel,
VersionCreationTrigger,
VersionCreationTriggerPayload,
)
from specklepy.objects.base import Base
def _minimal_automation_context() -> AutomationContext:
run_data = AutomationRunData(
project_id="p",
speckle_server_url="http://localhost",
automation_id="a",
automation_run_id="r",
function_run_id="f",
triggers=[
VersionCreationTrigger(
trigger_type="versionCreation",
payload=VersionCreationTriggerPayload(model_id="m", version_id="v"),
)
],
)
return AutomationContext(
automation_run_data=run_data,
speckle_client=MagicMock(),
_server_transport=MagicMock(),
_speckle_token="",
)
def test_attach_result_to_objects_accepts_empty_list() -> None:
"""Empty affected_objects appends one result case with no object IDs."""
ctx = _minimal_automation_context()
assert len(ctx._automation_result.object_results) == 0
ctx.attach_result_to_objects(
ObjectResultLevel.WARNING,
"SkippedRule",
[],
message="No elements to check.",
)
assert len(ctx._automation_result.object_results) == 1
case = ctx._automation_result.object_results[0]
assert case.level == ObjectResultLevel.WARNING
assert case.category == "SkippedRule"
assert case.object_app_ids == {}
assert case.message == "No elements to check."
def test_attach_result_to_objects_with_objects_appends_case_with_ids() -> None:
"""Single or multiple objects with id produce result case with object_app_ids."""
ctx = _minimal_automation_context()
obj1 = Base()
obj1.id = "id-one"
obj1.applicationId = "app-one"
obj2 = Base()
obj2.id = "id-two"
ctx.attach_result_to_objects(
ObjectResultLevel.ERROR,
"BadType",
[obj1, obj2],
message="Invalid type.",
)
assert len(ctx._automation_result.object_results) == 1
case = ctx._automation_result.object_results[0]
assert case.level == ObjectResultLevel.ERROR
assert case.category == "BadType"
assert case.object_app_ids == {"id-one": "app-one", "id-two": None}
assert case.message == "Invalid type."
def test_attach_result_to_objects_raises_when_object_has_no_id() -> None:
"""At least one object without id raises."""
ctx = _minimal_automation_context()
obj = Base()
obj.id = None
with pytest.raises(Exception, match="results to objects with an id"):
ctx.attach_result_to_objects(
ObjectResultLevel.ERROR,
"Bad",
obj,
message="No id.",
)
assert len(ctx._automation_result.object_results) == 0
def test_attach_info_to_objects_accepts_empty_list() -> None:
"""attach_info_to_objects (convenience method) also accepts empty list."""
ctx = _minimal_automation_context()
ctx.attach_info_to_objects("VersionLevel", [], message="No levels in model.")
assert len(ctx._automation_result.object_results) == 1
case = ctx._automation_result.object_results[0]
assert case.level == ObjectResultLevel.INFO
assert case.category == "VersionLevel"
assert case.object_app_ids == {}