chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user