Files
wehub-resource-sync db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:25 +08:00

173 lines
7.1 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for the durable workflow naming helpers.
These helpers derive the **stable** durable names a hosted workflow registers
under. Stability matters: durable replay resumes an in-flight orchestration only
if the orchestration name still resolves, so the round-trip
(``workflow_orchestrator_name`` ↔ ``workflow_name_from_orchestrator``) and the
validation rules (reject empty / malformed / auto-generated names) are the
contract the multi-workflow hosting builds on.
"""
import uuid
import pytest
from agent_framework_durabletask import (
DURABLE_NAME_PREFIX,
is_auto_generated_workflow_name,
validate_executor_id,
validate_workflow_name,
workflow_name_from_orchestrator,
workflow_orchestrator_name,
)
from agent_framework_durabletask._workflows.naming import (
MAX_EXECUTOR_ID_LENGTH,
SUBWORKFLOW_REQUEST_SEPARATOR,
qualify_subworkflow_request_id,
split_subworkflow_request_id,
)
class TestWorkflowOrchestratorName:
"""``workflow_orchestrator_name`` derives ``dafx-{name}`` for valid names."""
def test_prepends_prefix(self) -> None:
assert workflow_orchestrator_name("orders") == "dafx-orders"
def test_uses_shared_prefix_constant(self) -> None:
assert workflow_orchestrator_name("orders") == f"{DURABLE_NAME_PREFIX}orders"
@pytest.mark.parametrize("name", ["a", "Order_Processor", "spam-detection", "wf123"])
def test_accepts_valid_names(self, name: str) -> None:
assert workflow_orchestrator_name(name) == f"dafx-{name}"
@pytest.mark.parametrize("name", ["", "1abc", "has space", "bad/char", "emoji😀"])
def test_rejects_invalid_names(self, name: str) -> None:
with pytest.raises(ValueError):
workflow_orchestrator_name(name)
class TestWorkflowNameRoundTrip:
"""``workflow_name_from_orchestrator`` inverts ``workflow_orchestrator_name``."""
@pytest.mark.parametrize("name", ["orders", "Order_Processor", "spam-detection", "wf123"])
def test_round_trips(self, name: str) -> None:
orchestrator = workflow_orchestrator_name(name)
assert workflow_name_from_orchestrator(orchestrator) == name
def test_returns_none_without_prefix(self) -> None:
# A bare orchestration name (no dafx- prefix) is "not one of ours".
assert workflow_name_from_orchestrator("workflow_orchestrator") is None
class TestValidateExecutorId:
"""``validate_executor_id`` guards the durable-naming / nested-HITL contract."""
@pytest.mark.parametrize("executor_id", ["router", "agent_node", "reviewer-node", "a", "Step1"])
def test_accepts_ordinary_ids(self, executor_id: str) -> None:
validate_executor_id(executor_id) # does not raise
def test_rejects_empty(self) -> None:
with pytest.raises(ValueError, match="non-empty"):
validate_executor_id("")
def test_rejects_id_containing_separator(self) -> None:
bad = f"a{SUBWORKFLOW_REQUEST_SEPARATOR}b"
with pytest.raises(ValueError, match="reserved sub-workflow request separator"):
validate_executor_id(bad)
def test_rejects_overly_long_id(self) -> None:
with pytest.raises(ValueError, match="too long"):
validate_executor_id("x" * (MAX_EXECUTOR_ID_LENGTH + 1))
class TestSubworkflowRequestIdQualification:
"""Round-trip of the ``{executor}~{ordinal}~{leaf}`` qualified-request-id scheme."""
def test_separator_is_url_safe_tilde(self) -> None:
# '~' is RFC 3986 unreserved and (unlike '::') never appears in core request ids.
assert SUBWORKFLOW_REQUEST_SEPARATOR == "~"
def test_qualify_then_split_round_trips(self) -> None:
qualified = qualify_subworkflow_request_id("sub", 2, "req-9")
assert qualified == "sub~2~req-9"
assert split_subworkflow_request_id(qualified) == ("sub", 2, "req-9")
def test_split_returns_none_for_bare_id(self) -> None:
assert split_subworkflow_request_id("req-9") is None
def test_split_preserves_double_colon_leaf(self) -> None:
# A functional workflow's ``auto::0`` leaf survives one peel as the remainder.
assert split_subworkflow_request_id("sub~0~auto::0") == ("sub", 0, "auto::0")
def test_split_treats_double_colon_only_id_as_bare(self) -> None:
# ``auto::0`` has no '~', so it is a bare leaf, not a nested hop.
assert split_subworkflow_request_id("auto::0") is None
def test_split_treats_non_integer_ordinal_as_bare(self) -> None:
# A value whose second segment is not an integer is not a structural hop.
assert split_subworkflow_request_id("a~b~c") is None
def test_nested_qualification_round_trips(self) -> None:
deep = qualify_subworkflow_request_id("mid", 0, qualify_subworkflow_request_id("leaf", 1, "deep"))
assert deep == "mid~0~leaf~1~deep"
hop = split_subworkflow_request_id(deep)
assert hop is not None
executor_id, ordinal, remainder = hop
assert (executor_id, ordinal) == ("mid", 0)
assert split_subworkflow_request_id(remainder) == ("leaf", 1, "deep")
def test_returns_none_for_prefix_only(self) -> None:
assert workflow_name_from_orchestrator(DURABLE_NAME_PREFIX) is None
def test_strips_only_leading_prefix(self) -> None:
# Reverse is meant for orchestration names; it strips just the prefix, so a
# scoped activity-style name returns the remainder verbatim.
assert workflow_name_from_orchestrator("dafx-orders-translator") == "orders-translator"
class TestValidateWorkflowName:
"""``validate_workflow_name`` rejects unstable / unsafe identities."""
@pytest.mark.parametrize("name", ["a", "A", "wf", "Order_Processor", "spam-detection", "x" * 63])
def test_accepts_valid(self, name: str) -> None:
validate_workflow_name(name) # should not raise
def test_rejects_empty(self) -> None:
with pytest.raises(ValueError, match="non-empty"):
validate_workflow_name("")
@pytest.mark.parametrize("name", ["1abc", "-abc", "_abc", "has space", "bad/char", "a.b", "x" * 64])
def test_rejects_malformed(self, name: str) -> None:
with pytest.raises(ValueError, match="invalid"):
validate_workflow_name(name)
def test_rejects_auto_generated(self) -> None:
name = f"WorkflowBuilder-{uuid.uuid4()}"
with pytest.raises(ValueError, match="auto-generated"):
validate_workflow_name(name)
class TestIsAutoGeneratedWorkflowName:
"""``is_auto_generated_workflow_name`` detects WorkflowBuilder defaults."""
def test_detects_uuid_default(self) -> None:
assert is_auto_generated_workflow_name(f"WorkflowBuilder-{uuid.uuid4()}") is True
def test_detects_uppercase_hex_uuid(self) -> None:
assert is_auto_generated_workflow_name(f"WorkflowBuilder-{str(uuid.uuid4()).upper()}") is True
@pytest.mark.parametrize(
"name",
[
"orders",
"WorkflowBuilder",
"WorkflowBuilder-not-a-uuid",
"MyWorkflowBuilder-3f2b1c0a-1234-5678-9abc-def012345678",
],
)
def test_ignores_explicit_names(self, name: str) -> None:
assert is_auto_generated_workflow_name(name) is False