diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs
index 798c75ea3..0142ddc81 100644
--- a/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs
+++ b/src/Dapr.Workflow.Abstractions/PropagatedHistory.cs
@@ -21,36 +21,29 @@ namespace Dapr.Workflow;
///
/// Workflow history propagated from one or more ancestor workflows to a child workflow or activity.
///
+///
+/// Workflow history events in execution order (ancestor first, immediate parent last).
+///
///
-/// A propagated history is an ordered list of values,
+/// A propagated history is an ordered list of values,
/// one per ancestor workflow. Order is execution order: index 0 is the oldest ancestor,
/// the last entry is the immediate parent.
///
-/// Use for the full list, the FilterBy* methods to narrow by
-/// app, instance, or workflow name, and for the most
+/// Use for the full list, the FilterBy* methods to narrow by
+/// app, instance, or workflow name, and for the most
/// recent entry with a given name. Mirrors the PropagatedHistory type in the Go and Python SDKs.
///
///
-public sealed class PropagatedHistory
+public sealed class PropagatedHistory(IReadOnlyList events)
{
- private readonly IReadOnlyList _entries;
+ private readonly IReadOnlyList _events =
+ events ?? throw new ArgumentNullException(nameof(events));
///
- /// Initializes a new from the given workflow entries.
+ /// Returns every event in the propagated history, in execution
+ /// order (ancestor first, immediate parent last).
///
- ///
- /// Workflow entries in execution order (ancestor first, immediate parent last).
- ///
- public PropagatedHistory(IReadOnlyList entries)
- {
- _entries = entries ?? throw new ArgumentNullException(nameof(entries));
- }
-
- ///
- /// Returns every entry in the propagated history, in execution order
- /// (ancestor first, immediate parent last).
- ///
- public IReadOnlyList GetEntries() => _entries;
+ public IReadOnlyList Events => _events;
///
/// Returns an ordered, deduplicated list of app IDs in this propagated history.
@@ -58,14 +51,8 @@ public PropagatedHistory(IReadOnlyList entries)
public IReadOnlyList GetAppIds()
{
var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
- var result = new List(_entries.Count);
- foreach (var entry in _entries)
- {
- if (seen.Add(entry.AppId))
- {
- result.Add(entry.AppId);
- }
- }
+ var result = new List(_events.Count);
+ result.AddRange(from entry in _events where seen.Add(entry.AppId) select entry.AppId);
return result;
}
@@ -76,10 +63,10 @@ public IReadOnlyList GetAppIds()
///
/// The workflow name to filter by.
/// An empty list when no match is found.
- public IReadOnlyList FilterByWorkflowName(string name)
+ public IReadOnlyList GetEventsByWorkflowName(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
- return _entries
+ return _events
.Where(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase))
.ToList();
}
@@ -90,14 +77,14 @@ public IReadOnlyList FilterByWorkflowName(string name)
/// The workflow name to look up.
/// When this method returns , the last matching workflow entry; otherwise .
/// if a matching entry was found; otherwise .
- public bool TryGetLastWorkflowByName(string name, [NotNullWhen(true)] out PropagatedHistoryEntry? result)
+ public bool TryGetLastWorkflowEventByName(string name, [NotNullWhen(true)] out PropagatedHistoryEvent? result)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
- for (var i = _entries.Count - 1; i >= 0; i--)
+ for (var i = _events.Count - 1; i >= 0; i--)
{
- if (string.Equals(_entries[i].Name, name, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(_events[i].Name, name, StringComparison.OrdinalIgnoreCase))
{
- result = _entries[i];
+ result = _events[i];
return true;
}
}
@@ -111,10 +98,10 @@ public bool TryGetLastWorkflowByName(string name, [NotNullWhen(true)] out Propag
///
/// The Dapr App ID to filter by.
/// An empty list when no match is found.
- public IReadOnlyList FilterByAppId(string appId)
+ public IReadOnlyList GetByAppId(string appId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(appId);
- return _entries
+ return _events
.Where(e => string.Equals(e.AppId, appId, StringComparison.OrdinalIgnoreCase))
.ToList();
}
@@ -125,11 +112,11 @@ public IReadOnlyList FilterByAppId(string appId)
///
/// The workflow instance ID to filter by.
/// An empty list when no match is found.
- public IReadOnlyList FilterByInstanceId(string instanceId)
+ public IReadOnlyList GetByInstanceId(string instanceId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(instanceId);
- return _entries
+ return _events
.Where(e => string.Equals(e.InstanceId, instanceId, StringComparison.Ordinal))
- .ToList();
+ .ToList();
}
}
diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistoryActivityResult.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistoryActivityResult.cs
index 0f4133e81..0dc05ea78 100644
--- a/src/Dapr.Workflow.Abstractions/PropagatedHistoryActivityResult.cs
+++ b/src/Dapr.Workflow.Abstractions/PropagatedHistoryActivityResult.cs
@@ -17,34 +17,19 @@ namespace Dapr.Workflow;
/// A reconstructed view of a single activity invocation surfaced through propagated workflow history.
///
/// The scheduled name of the activity.
-/// Whether the activity was scheduled in the propagated history.
-/// Whether the activity completed successfully.
-/// Whether the activity failed.
+/// The resolved lifecycle status of this activity.
/// The JSON-encoded input payload, or null when unset.
/// The JSON-encoded output payload, or null when the activity has not completed.
-/// The failure details when is true, otherwise null.
+/// The failure details when is , otherwise null.
///
-/// Mirrors the ActivityResult type in the Go and Python SDKs so cross-language
-/// quickstarts and audit patterns line up. The /
-/// / flags carry that parity; projects them onto a
-/// single value for callers that prefer to switch on the lifecycle.
+/// Every activity surfaced through propagated history was scheduled, so
+/// reflects how far it progressed past scheduling:
+/// means it was scheduled but has not yet completed or failed,
+/// means it succeeded, and means it failed.
///
public sealed record PropagatedHistoryActivityResult(
string Name,
- bool Started,
- bool Completed,
- bool Failed,
+ PropagatedHistoryStatus Status,
string? Input,
string? Output,
- WorkflowTaskFailureDetails? FailureDetails)
-{
- ///
- /// The resolved lifecycle status of this activity, derived from the
- /// and flags. takes
- /// precedence over .
- ///
- public PropagatedHistoryTaskStatus Status =>
- Failed ? PropagatedHistoryTaskStatus.Failed
- : Completed ? PropagatedHistoryTaskStatus.Completed
- : PropagatedHistoryTaskStatus.Pending;
-}
+ WorkflowTaskFailureDetails? FailureDetails);
diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistoryChildWorkflowResult.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistoryChildWorkflowResult.cs
deleted file mode 100644
index a4196c697..000000000
--- a/src/Dapr.Workflow.Abstractions/PropagatedHistoryChildWorkflowResult.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// ------------------------------------------------------------------------
-// Copyright 2026 The Dapr Authors
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-// ------------------------------------------------------------------------
-
-namespace Dapr.Workflow;
-
-///
-/// A reconstructed view of a single child workflow invocation surfaced through propagated workflow history.
-///
-/// The scheduled name of the child workflow.
-/// Whether the child workflow was scheduled in the propagated history.
-/// Whether the child workflow completed successfully.
-/// Whether the child workflow failed.
-/// The JSON-encoded output payload, or null when the child workflow has not completed.
-/// The failure details when is true, otherwise null.
-///
-/// Mirrors the ChildWorkflowResult type in the Go and Python SDKs. The
-/// / / flags carry that
-/// parity; projects them onto a single value for callers that prefer to
-/// switch on the lifecycle.
-///
-public sealed record PropagatedHistoryChildWorkflowResult(
- string Name,
- bool Started,
- bool Completed,
- bool Failed,
- string? Output,
- WorkflowTaskFailureDetails? FailureDetails)
-{
- ///
- /// The resolved lifecycle status of this child workflow, derived from the
- /// and flags. takes
- /// precedence over .
- ///
- public PropagatedHistoryTaskStatus Status =>
- Failed ? PropagatedHistoryTaskStatus.Failed
- : Completed ? PropagatedHistoryTaskStatus.Completed
- : PropagatedHistoryTaskStatus.Pending;
-}
diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistoryEntry.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistoryEvent.cs
similarity index 68%
rename from src/Dapr.Workflow.Abstractions/PropagatedHistoryEntry.cs
rename to src/Dapr.Workflow.Abstractions/PropagatedHistoryEvent.cs
index abbc903ba..14a589d91 100644
--- a/src/Dapr.Workflow.Abstractions/PropagatedHistoryEntry.cs
+++ b/src/Dapr.Workflow.Abstractions/PropagatedHistoryEvent.cs
@@ -28,53 +28,50 @@ namespace Dapr.Workflow;
/// Activities resolved from this entry, in execution order.
/// Child workflows resolved from this entry, in execution order.
///
-/// One exists per ancestor workflow in a
+/// One exists per ancestor workflow in a
/// . Use and
-/// to look up specific items in this entry;
+/// to look up specific items in this entry;
/// the plural Get*ByName variants return every occurrence in execution order.
///
-public sealed class PropagatedHistoryEntry(
+public sealed class PropagatedHistoryEvent(
string instanceId,
string appId,
string name,
IReadOnlyList activities,
- IReadOnlyList childWorkflows)
+ IReadOnlyList childWorkflows)
{
- private readonly IReadOnlyList _activities =
- activities ?? throw new ArgumentNullException(nameof(activities));
- private readonly IReadOnlyList _childWorkflows =
- childWorkflows ?? throw new ArgumentNullException(nameof(childWorkflows));
-
- /// The instance ID of the ancestor workflow this entry describes.
+ /// The instance ID of the workflow this history event describes.
public string InstanceId { get; } = instanceId ?? throw new ArgumentNullException(nameof(instanceId));
- /// The Dapr App ID that ran this ancestor workflow.
+ /// The Dapr App ID that ran this workflow.
public string AppId { get; } = appId ?? throw new ArgumentNullException(nameof(appId));
- /// The name of this ancestor workflow.
+ /// The name of this workflow.
public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
- /// All activities executed in this entry, in execution order.
- public IReadOnlyList Activities => _activities;
+ /// All activities executed in this history event, in execution order.
+ public IReadOnlyList Activities { get; } =
+ activities ?? throw new ArgumentNullException(nameof(activities));
- /// All child workflows started in this entry, in execution order.
- public IReadOnlyList ChildWorkflows => _childWorkflows;
+ /// All workflows started in this history event, in execution order.
+ public IReadOnlyList Workflows { get; } =
+ childWorkflows ?? throw new ArgumentNullException(nameof(childWorkflows));
///
- /// Returns every activity in this entry whose scheduled name matches, in execution order.
+ /// Returns every activity in this history event whose scheduled name matches, in execution order.
///
/// The activity name to filter by.
/// An empty list when no match is found.
public IReadOnlyList GetActivitiesByName(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
- return _activities
+ return Activities
.Where(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase))
.ToList();
}
///
- /// Tries to return the most recent activity in this entry whose name matches.
+ /// Tries to return the most recent activity in this history event whose name matches.
///
/// The activity name to look up.
/// When this method returns , the last matching activity; otherwise .
@@ -82,11 +79,11 @@ public IReadOnlyList GetActivitiesByName(string
public bool TryGetLastActivityByName(string name, [NotNullWhen(true)] out PropagatedHistoryActivityResult? result)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
- for (var i = _activities.Count - 1; i >= 0; i--)
+ for (var i = Activities.Count - 1; i >= 0; i--)
{
- if (string.Equals(_activities[i].Name, name, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(Activities[i].Name, name, StringComparison.OrdinalIgnoreCase))
{
- result = _activities[i];
+ result = Activities[i];
return true;
}
}
@@ -96,32 +93,32 @@ public bool TryGetLastActivityByName(string name, [NotNullWhen(true)] out Propag
}
///
- /// Returns every child workflow in this entry whose name matches, in execution order.
+ /// Returns every child workflow in this history event whose name matches, in execution order.
///
/// The child workflow name to filter by.
/// An empty list when no match is found.
- public IReadOnlyList GetChildWorkflowsByName(string name)
+ public IReadOnlyList GetWorkflowsByName(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
- return _childWorkflows
+ return Workflows
.Where(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase))
.ToList();
}
///
- /// Tries to return the most recent child workflow in this entry whose name matches.
+ /// Tries to return the most recent child workflow in this history event whose name matches.
///
/// The child workflow name to look up.
/// When this method returns , the last matching child workflow; otherwise .
/// if a matching child workflow was found; otherwise .
- public bool TryGetLastChildWorkflowByName(string name, [NotNullWhen(true)] out PropagatedHistoryChildWorkflowResult? result)
+ public bool TryGetLastWorkflowByName(string name, [NotNullWhen(true)] out PropagatedHistoryWorkflowResult? result)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
- for (var i = _childWorkflows.Count - 1; i >= 0; i--)
+ for (var i = Workflows.Count - 1; i >= 0; i--)
{
- if (string.Equals(_childWorkflows[i].Name, name, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(Workflows[i].Name, name, StringComparison.OrdinalIgnoreCase))
{
- result = _childWorkflows[i];
+ result = Workflows[i];
return true;
}
}
diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistoryTaskStatus.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistoryStatus.cs
similarity index 79%
rename from src/Dapr.Workflow.Abstractions/PropagatedHistoryTaskStatus.cs
rename to src/Dapr.Workflow.Abstractions/PropagatedHistoryStatus.cs
index 9e85954bb..9ad77e04f 100644
--- a/src/Dapr.Workflow.Abstractions/PropagatedHistoryTaskStatus.cs
+++ b/src/Dapr.Workflow.Abstractions/PropagatedHistoryStatus.cs
@@ -19,12 +19,10 @@ namespace Dapr.Workflow;
///
///
/// Every task surfaced through propagated history was scheduled, so the status reflects how
-/// far it progressed past scheduling. It is a projection of the Completed and
-/// Failed flags on /
-/// , provided so callers can switch
-/// on a single value instead of evaluating the flags by hand.
+/// far it progressed past scheduling. Use a switch expression on this value to branch
+/// on the three possible states without needing to evaluate multiple boolean conditions.
///
-public enum PropagatedHistoryTaskStatus
+public enum PropagatedHistoryStatus
{
///
/// The task was scheduled but has not yet completed or failed in the propagated history.
diff --git a/src/Dapr.Workflow.Abstractions/PropagatedHistoryWorkflowResult.cs b/src/Dapr.Workflow.Abstractions/PropagatedHistoryWorkflowResult.cs
new file mode 100644
index 000000000..cfa3b9a43
--- /dev/null
+++ b/src/Dapr.Workflow.Abstractions/PropagatedHistoryWorkflowResult.cs
@@ -0,0 +1,33 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow;
+
+///
+/// A reconstructed view of a single child workflow invocation surfaced through propagated workflow history.
+///
+/// The scheduled name of the child workflow.
+/// The resolved lifecycle status of this child workflow.
+/// The JSON-encoded output payload, or null when the child workflow has not completed.
+/// The failure details when is , otherwise null.
+///
+/// Every child workflow surfaced through propagated history was scheduled, so
+/// reflects how far it progressed past scheduling:
+/// means it was scheduled but has not yet completed or failed,
+/// means it succeeded, and means it failed.
+///
+public sealed record PropagatedHistoryWorkflowResult(
+ string Name,
+ PropagatedHistoryStatus Status,
+ string? Output,
+ WorkflowTaskFailureDetails? FailureDetails);
diff --git a/src/Dapr.Workflow.Abstractions/WorkflowContext.cs b/src/Dapr.Workflow.Abstractions/WorkflowContext.cs
index 91de5db65..2c3ddc70c 100644
--- a/src/Dapr.Workflow.Abstractions/WorkflowContext.cs
+++ b/src/Dapr.Workflow.Abstractions/WorkflowContext.cs
@@ -341,8 +341,8 @@ public virtual Task CallChildWorkflowAsync(
/// specified a other than .
///
///
- /// Use and
- /// /
+ /// Use and
+ /// /
/// to look up the most recent matching item; the plural FilterBy* /
/// Get*ByName variants return every match.
///
diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs
index 2aa18deec..6333a9412 100644
--- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs
+++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs
@@ -1003,7 +1003,7 @@ private static WorkflowTaskFailedException CreateTaskFailedException(TaskFailedE
///
/// Converts a proto message into a public-facing
- /// by resolving each scheduled activity and child workflow
+ /// by resolving each scheduled activity and child workflow
/// against its matching completion/failure event.
///
///
@@ -1014,7 +1014,7 @@ private static WorkflowTaskFailedException CreateTaskFailedException(TaskFailedE
/// Malformed event bytes are surfaced as exceptions — a runtime sending unparseable
/// proto bytes is a contract violation we should not hide.
///
- private static PropagatedHistoryEntry ConvertChunk(PropagatedHistoryChunk chunk)
+ private static PropagatedHistoryEvent ConvertChunk(PropagatedHistoryChunk chunk)
{
var events = new List(chunk.RawEvents.Count);
foreach (var rawEvent in chunk.RawEvents)
@@ -1048,7 +1048,7 @@ private static PropagatedHistoryEntry ConvertChunk(PropagatedHistoryChunk chunk)
}
var activities = new List();
- var childWorkflows = new List();
+ var childWorkflows = new List();
foreach (var historyEvent in events)
{
switch (historyEvent.EventTypeCase)
@@ -1062,7 +1062,7 @@ private static PropagatedHistoryEntry ConvertChunk(PropagatedHistoryChunk chunk)
}
}
- return new PropagatedHistoryEntry(
+ return new PropagatedHistoryEvent(
chunk.InstanceId,
chunk.AppId,
chunk.WorkflowName,
@@ -1081,67 +1081,61 @@ private static PropagatedHistoryActivityResult ResolveActivity(
{
var scheduled = scheduleEvent.TaskScheduled;
var scheduleId = scheduleEvent.EventId;
- var completed = false;
- var failed = false;
+ var status = PropagatedHistoryStatus.Pending;
string? output = null;
WorkflowTaskFailureDetails? failureDetails = null;
if (completions.TryGetValue(scheduleId, out var completion))
{
- completed = true;
+ status = PropagatedHistoryStatus.Completed;
output = completion.TaskCompleted.Result;
}
if (failures.TryGetValue(scheduleId, out var failure))
{
- failed = true;
+ status = PropagatedHistoryStatus.Failed;
failureDetails = MapFailureDetails(failure.TaskFailed.FailureDetails);
}
return new PropagatedHistoryActivityResult(
Name: scheduled.Name,
- Started: true,
- Completed: completed,
- Failed: failed,
+ Status: status,
Input: scheduled.Input,
Output: output,
FailureDetails: failureDetails);
}
///
- /// Builds a by matching the create event's
+ /// Builds a by matching the create event's
/// EventId against subsequent ChildWorkflowInstanceCompleted /
/// ChildWorkflowInstanceFailed events.
///
- private static PropagatedHistoryChildWorkflowResult ResolveChildWorkflow(
+ private static PropagatedHistoryWorkflowResult ResolveChildWorkflow(
HistoryEvent createEvent,
IReadOnlyDictionary completions,
IReadOnlyDictionary failures)
{
var created = createEvent.ChildWorkflowInstanceCreated;
var creationId = createEvent.EventId;
- var completed = false;
- var failed = false;
+ var status = PropagatedHistoryStatus.Pending;
string? output = null;
WorkflowTaskFailureDetails? failureDetails = null;
if (completions.TryGetValue(creationId, out var completion))
{
- completed = true;
+ status = PropagatedHistoryStatus.Completed;
output = completion.ChildWorkflowInstanceCompleted.Result;
}
if (failures.TryGetValue(creationId, out var failure))
{
- failed = true;
+ status = PropagatedHistoryStatus.Failed;
failureDetails = MapFailureDetails(failure.ChildWorkflowInstanceFailed.FailureDetails);
}
- return new PropagatedHistoryChildWorkflowResult(
+ return new PropagatedHistoryWorkflowResult(
Name: created.Name,
- Started: true,
- Completed: completed,
- Failed: failed,
+ Status: status,
Output: output,
FailureDetails: failureDetails);
}
diff --git a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs
index 046d7048c..9a26fce26 100644
--- a/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs
+++ b/test/Dapr.IntegrationTest.Workflow/HistoryPropagationWorkflowTests.cs
@@ -397,7 +397,7 @@ public override async Task RunAsync(WorkflowContext conte
await context.CreateTimer(TimeSpan.Zero);
return new PropagationTestResult(
ChildReceivedPropagatedHistory: propagated is not null,
- PropagatedEntryCount: propagated?.GetEntries().Count ?? 0);
+ PropagatedEntryCount: propagated?.Events.Count ?? 0);
}
}
diff --git a/test/Dapr.Workflow.Abstractions.Test/PropagatedHistoryTests.cs b/test/Dapr.Workflow.Abstractions.Test/PropagatedHistoryTests.cs
new file mode 100644
index 000000000..a2d5c00a6
--- /dev/null
+++ b/test/Dapr.Workflow.Abstractions.Test/PropagatedHistoryTests.cs
@@ -0,0 +1,677 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Workflow.Abstractions.Test;
+
+public class PropagatedHistoryTests
+{
+ // ----- Helpers -----
+
+ static PropagatedHistoryEvent MakeEvent(string instanceId = "id1", string appId = "app1", string name = "WF1") =>
+ new(instanceId, appId, name, [], []);
+
+ // ----- Events property -----
+
+ [Fact]
+ public void Events_ReturnsEmpty_WhenConstructedWithEmptyList()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Empty(history.Events);
+ }
+
+ [Fact]
+ public void Events_ReturnsProvidedEvents()
+ {
+ var e1 = MakeEvent("id1", "app1", "WF1");
+ var e2 = MakeEvent("id2", "app2", "WF2");
+ var history = new PropagatedHistory([e1, e2]);
+ Assert.Equal(2, history.Events.Count);
+ Assert.Same(e1, history.Events[0]);
+ Assert.Same(e2, history.Events[1]);
+ }
+
+ // ----- GetAppIds -----
+
+ [Fact]
+ public void GetAppIds_ReturnsEmpty_WhenNoEvents()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Empty(history.GetAppIds());
+ }
+
+ [Fact]
+ public void GetAppIds_ReturnsOrderedDeduplicated_AppIds()
+ {
+ var events = new[]
+ {
+ MakeEvent("id1", "appA", "WF1"),
+ MakeEvent("id2", "appB", "WF2"),
+ MakeEvent("id3", "appA", "WF3"),
+ };
+ var history = new PropagatedHistory(events);
+ var appIds = history.GetAppIds();
+ Assert.Equal(2, appIds.Count);
+ Assert.Equal("appA", appIds[0]);
+ Assert.Equal("appB", appIds[1]);
+ }
+
+ [Fact]
+ public void GetAppIds_IsCaseInsensitive_ForDeduplication()
+ {
+ var events = new[]
+ {
+ MakeEvent("id1", "AppA", "WF1"),
+ MakeEvent("id2", "appa", "WF2"),
+ };
+ var history = new PropagatedHistory(events);
+ var appIds = history.GetAppIds();
+ Assert.Single(appIds);
+ Assert.Equal("AppA", appIds[0]);
+ }
+
+ [Fact]
+ public void GetAppIds_ReturnsAllDistinct_AppIds()
+ {
+ var events = new[]
+ {
+ MakeEvent("id1", "appX", "WF1"),
+ MakeEvent("id2", "appY", "WF2"),
+ MakeEvent("id3", "appZ", "WF3"),
+ };
+ var history = new PropagatedHistory(events);
+ var appIds = history.GetAppIds();
+ Assert.Equal(3, appIds.Count);
+ }
+
+ // ----- GetEventsByWorkflowName -----
+
+ [Fact]
+ public void GetEventsByWorkflowName_ThrowsOnNull()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetEventsByWorkflowName(null!));
+ }
+
+ [Fact]
+ public void GetEventsByWorkflowName_ThrowsOnWhitespace()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetEventsByWorkflowName(" "));
+ }
+
+ [Fact]
+ public void GetEventsByWorkflowName_ReturnsEmpty_WhenNoMatch()
+ {
+ var history = new PropagatedHistory([MakeEvent("id1", "app1", "WF1")]);
+ Assert.Empty(history.GetEventsByWorkflowName("WF2"));
+ }
+
+ [Fact]
+ public void GetEventsByWorkflowName_ReturnsMatching_InOrder()
+ {
+ var e1 = MakeEvent("id1", "app1", "WFMatch");
+ var e2 = MakeEvent("id2", "app2", "WFOther");
+ var e3 = MakeEvent("id3", "app3", "WFMatch");
+ var history = new PropagatedHistory([e1, e2, e3]);
+
+ var result = history.GetEventsByWorkflowName("WFMatch");
+ Assert.Equal(2, result.Count);
+ Assert.Same(e1, result[0]);
+ Assert.Same(e3, result[1]);
+ }
+
+ [Fact]
+ public void GetEventsByWorkflowName_IsCaseInsensitive()
+ {
+ var e = MakeEvent("id1", "app1", "MyWorkflow");
+ var history = new PropagatedHistory([e]);
+ Assert.Single(history.GetEventsByWorkflowName("myworkflow"));
+ Assert.Single(history.GetEventsByWorkflowName("MYWORKFLOW"));
+ }
+
+ // ----- TryGetLastWorkflowEventByName -----
+
+ [Fact]
+ public void TryGetLastWorkflowEventByName_ThrowsOnNull()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.TryGetLastWorkflowEventByName(null!, out _));
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowEventByName_ThrowsOnWhitespace()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.TryGetLastWorkflowEventByName(" ", out _));
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowEventByName_ReturnsFalse_WhenNoMatch()
+ {
+ var history = new PropagatedHistory([MakeEvent("id1", "app1", "WF1")]);
+ Assert.False(history.TryGetLastWorkflowEventByName("WF2", out var result));
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowEventByName_ReturnsTrue_AndLastMatch()
+ {
+ var e1 = MakeEvent("id1", "app1", "WFRepeat");
+ var e2 = MakeEvent("id2", "app2", "WFOther");
+ var e3 = MakeEvent("id3", "app1", "WFRepeat");
+ var history = new PropagatedHistory([e1, e2, e3]);
+
+ Assert.True(history.TryGetLastWorkflowEventByName("WFRepeat", out var result));
+ Assert.Same(e3, result);
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowEventByName_IsCaseInsensitive()
+ {
+ var e = MakeEvent("id1", "app1", "MyWorkflow");
+ var history = new PropagatedHistory([e]);
+ Assert.True(history.TryGetLastWorkflowEventByName("myworkflow", out var result));
+ Assert.Same(e, result);
+ }
+
+ // ----- FilterByAppId -----
+
+ [Fact]
+ public void FilterByAppId_ThrowsOnNull()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetByAppId(null!));
+ }
+
+ [Fact]
+ public void FilterByAppId_ThrowsOnWhitespace()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetByAppId(" "));
+ }
+
+ [Fact]
+ public void FilterByAppId_ReturnsEmpty_WhenNoMatch()
+ {
+ var history = new PropagatedHistory([MakeEvent("id1", "appA", "WF1")]);
+ Assert.Empty(history.GetByAppId("appB"));
+ }
+
+ [Fact]
+ public void FilterByAppId_ReturnsMatching_InOrder()
+ {
+ var e1 = MakeEvent("id1", "appA", "WF1");
+ var e2 = MakeEvent("id2", "appB", "WF2");
+ var e3 = MakeEvent("id3", "appA", "WF3");
+ var history = new PropagatedHistory([e1, e2, e3]);
+
+ var result = history.GetByAppId("appA");
+ Assert.Equal(2, result.Count);
+ Assert.Same(e1, result[0]);
+ Assert.Same(e3, result[1]);
+ }
+
+ [Fact]
+ public void FilterByAppId_IsCaseInsensitive()
+ {
+ var e = MakeEvent("id1", "AppA", "WF1");
+ var history = new PropagatedHistory([e]);
+ Assert.Single(history.GetByAppId("appa"));
+ Assert.Single(history.GetByAppId("APPA"));
+ }
+
+ // ----- FilterByInstanceId -----
+
+ [Fact]
+ public void FilterByInstanceId_ThrowsOnNull()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetByInstanceId(null!));
+ }
+
+ [Fact]
+ public void FilterByInstanceId_ThrowsOnWhitespace()
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetByInstanceId(" "));
+ }
+
+ [Fact]
+ public void FilterByInstanceId_ReturnsEmpty_WhenNoMatch()
+ {
+ var history = new PropagatedHistory([MakeEvent("idA", "app1", "WF1")]);
+ Assert.Empty(history.GetByInstanceId("idB"));
+ }
+
+ [Fact]
+ public void FilterByInstanceId_ReturnsMatching()
+ {
+ var e1 = MakeEvent("idA", "app1", "WF1");
+ var e2 = MakeEvent("idB", "app2", "WF2");
+ var e3 = MakeEvent("idA", "app3", "WF3");
+ var history = new PropagatedHistory([e1, e2, e3]);
+
+ var result = history.GetByInstanceId("idA");
+ Assert.Equal(2, result.Count);
+ Assert.Same(e1, result[0]);
+ Assert.Same(e3, result[1]);
+ }
+
+ [Fact]
+ public void FilterByInstanceId_IsCaseSensitive()
+ {
+ var e = MakeEvent("idA", "app1", "WF1");
+ var history = new PropagatedHistory([e]);
+ Assert.Empty(history.GetByInstanceId("ida"));
+ Assert.Empty(history.GetByInstanceId("IDA"));
+ Assert.Single(history.GetByInstanceId("idA"));
+ }
+}
+
+public class PropagatedHistoryEventTests
+{
+ // ----- Helpers -----
+
+ static PropagatedHistoryActivityResult MakeActivity(string name, PropagatedHistoryStatus status = PropagatedHistoryStatus.Completed) =>
+ new(name, status, null, null, null);
+
+ static PropagatedHistoryWorkflowResult MakeChildWorkflow(string name, PropagatedHistoryStatus status = PropagatedHistoryStatus.Completed) =>
+ new(name, status, null, null);
+
+ // ----- Constructor null guards -----
+
+ [Fact]
+ public void Constructor_ThrowsOnNull_InstanceId()
+ {
+ Assert.Throws(() =>
+ new PropagatedHistoryEvent(null!, "appId", "name", [], []));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsOnNull_AppId()
+ {
+ Assert.Throws(() =>
+ new PropagatedHistoryEvent("instanceId", null!, "name", [], []));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsOnNull_Name()
+ {
+ Assert.Throws(() =>
+ new PropagatedHistoryEvent("instanceId", "appId", null!, [], []));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsOnNull_Activities()
+ {
+ Assert.Throws(() =>
+ new PropagatedHistoryEvent("instanceId", "appId", "name", null!, []));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsOnNull_ChildWorkflows()
+ {
+ Assert.Throws(() =>
+ new PropagatedHistoryEvent("instanceId", "appId", "name", [], null!));
+ }
+
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ var activities = new[] { MakeActivity("Act1") };
+ var workflows = new[] { MakeChildWorkflow("CW1") };
+ var evt = new PropagatedHistoryEvent("inst-1", "my-app", "MyWorkflow", activities, workflows);
+
+ Assert.Equal("inst-1", evt.InstanceId);
+ Assert.Equal("my-app", evt.AppId);
+ Assert.Equal("MyWorkflow", evt.Name);
+ Assert.Equal(activities, evt.Activities);
+ Assert.Equal(workflows, evt.Workflows);
+ }
+
+ // ----- GetActivitiesByName -----
+
+ [Fact]
+ public void GetActivitiesByName_ThrowsOnNull()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.GetActivitiesByName(null!));
+ }
+
+ [Fact]
+ public void GetActivitiesByName_ThrowsOnWhitespace()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.GetActivitiesByName(" "));
+ }
+
+ [Fact]
+ public void GetActivitiesByName_ReturnsEmpty_WhenNoMatch()
+ {
+ var activities = new[] { MakeActivity("Act1") };
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", activities, []);
+ Assert.Empty(evt.GetActivitiesByName("Act2"));
+ }
+
+ [Fact]
+ public void GetActivitiesByName_ReturnsMatching_InOrder()
+ {
+ var a1 = MakeActivity("ActMatch");
+ var a2 = MakeActivity("ActOther");
+ var a3 = MakeActivity("ActMatch");
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [a1, a2, a3], []);
+
+ var result = evt.GetActivitiesByName("ActMatch");
+ Assert.Equal(2, result.Count);
+ Assert.Same(a1, result[0]);
+ Assert.Same(a3, result[1]);
+ }
+
+ [Fact]
+ public void GetActivitiesByName_IsCaseInsensitive()
+ {
+ var a = MakeActivity("MyActivity");
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [a], []);
+ Assert.Single(evt.GetActivitiesByName("myactivity"));
+ Assert.Single(evt.GetActivitiesByName("MYACTIVITY"));
+ }
+
+ // ----- TryGetLastActivityByName -----
+
+ [Fact]
+ public void TryGetLastActivityByName_ThrowsOnNull()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.TryGetLastActivityByName(null!, out _));
+ }
+
+ [Fact]
+ public void TryGetLastActivityByName_ThrowsOnWhitespace()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.TryGetLastActivityByName(" ", out _));
+ }
+
+ [Fact]
+ public void TryGetLastActivityByName_ReturnsFalse_WhenNoMatch()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [MakeActivity("Act1")], []);
+ Assert.False(evt.TryGetLastActivityByName("Act2", out var result));
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void TryGetLastActivityByName_ReturnsTrue_AndLastMatch()
+ {
+ var a1 = MakeActivity("ActRepeat", PropagatedHistoryStatus.Completed);
+ var a2 = MakeActivity("ActOther");
+ var a3 = MakeActivity("ActRepeat", PropagatedHistoryStatus.Failed);
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [a1, a2, a3], []);
+
+ Assert.True(evt.TryGetLastActivityByName("ActRepeat", out var result));
+ Assert.Same(a3, result);
+ }
+
+ [Fact]
+ public void TryGetLastActivityByName_IsCaseInsensitive()
+ {
+ var a = MakeActivity("MyActivity");
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [a], []);
+ Assert.True(evt.TryGetLastActivityByName("myactivity", out var result));
+ Assert.Same(a, result);
+ }
+
+ // ----- GetWorkflowsByName -----
+
+ [Fact]
+ public void GetWorkflowsByName_ThrowsOnNull()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.GetWorkflowsByName(null!));
+ }
+
+ [Fact]
+ public void GetWorkflowsByName_ThrowsOnWhitespace()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.GetWorkflowsByName(" "));
+ }
+
+ [Fact]
+ public void GetWorkflowsByName_ReturnsEmpty_WhenNoMatch()
+ {
+ var workflows = new[] { MakeChildWorkflow("CW1") };
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], workflows);
+ Assert.Empty(evt.GetWorkflowsByName("CW2"));
+ }
+
+ [Fact]
+ public void GetWorkflowsByName_ReturnsMatching_InOrder()
+ {
+ var cw1 = MakeChildWorkflow("CWMatch");
+ var cw2 = MakeChildWorkflow("CWOther");
+ var cw3 = MakeChildWorkflow("CWMatch");
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], [cw1, cw2, cw3]);
+
+ var result = evt.GetWorkflowsByName("CWMatch");
+ Assert.Equal(2, result.Count);
+ Assert.Same(cw1, result[0]);
+ Assert.Same(cw3, result[1]);
+ }
+
+ [Fact]
+ public void GetWorkflowsByName_IsCaseInsensitive()
+ {
+ var cw = MakeChildWorkflow("MyChild");
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], [cw]);
+ Assert.Single(evt.GetWorkflowsByName("mychild"));
+ Assert.Single(evt.GetWorkflowsByName("MYCHILD"));
+ }
+
+ // ----- TryGetLastWorkflowByName -----
+
+ [Fact]
+ public void TryGetLastWorkflowByName_ThrowsOnNull()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.TryGetLastWorkflowByName(null!, out _));
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowByName_ThrowsOnWhitespace()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], []);
+ Assert.Throws(() => evt.TryGetLastWorkflowByName(" ", out _));
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowByName_ReturnsFalse_WhenNoMatch()
+ {
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], [MakeChildWorkflow("CW1")]);
+ Assert.False(evt.TryGetLastWorkflowByName("CW2", out var result));
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowByName_ReturnsTrue_AndLastMatch()
+ {
+ var cw1 = MakeChildWorkflow("CWRepeat", PropagatedHistoryStatus.Completed);
+ var cw2 = MakeChildWorkflow("CWOther");
+ var cw3 = MakeChildWorkflow("CWRepeat", PropagatedHistoryStatus.Failed);
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], [cw1, cw2, cw3]);
+
+ Assert.True(evt.TryGetLastWorkflowByName("CWRepeat", out var result));
+ Assert.Same(cw3, result);
+ }
+
+ [Fact]
+ public void TryGetLastWorkflowByName_IsCaseInsensitive()
+ {
+ var cw = MakeChildWorkflow("MyChild");
+ var evt = new PropagatedHistoryEvent("id", "app", "wf", [], [cw]);
+ Assert.True(evt.TryGetLastWorkflowByName("mychild", out var result));
+ Assert.Same(cw, result);
+ }
+}
+
+public class PropagatedHistoryActivityResultTests
+{
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ var failure = new WorkflowTaskFailureDetails(typeof(InvalidOperationException).FullName!, "err");
+ var result = new PropagatedHistoryActivityResult("MyAct", PropagatedHistoryStatus.Failed, """{"x":1}""", null, failure);
+
+ Assert.Equal("MyAct", result.Name);
+ Assert.Equal(PropagatedHistoryStatus.Failed, result.Status);
+ Assert.Equal("""{"x":1}""", result.Input);
+ Assert.Null(result.Output);
+ Assert.Same(failure, result.FailureDetails);
+ }
+
+ [Fact]
+ public void Constructor_SetsNullableFields_ToNull()
+ {
+ var result = new PropagatedHistoryActivityResult("Act1", PropagatedHistoryStatus.Pending, null, null, null);
+ Assert.Null(result.Input);
+ Assert.Null(result.Output);
+ Assert.Null(result.FailureDetails);
+ }
+
+ [Fact]
+ public void RecordEquality_SameValues_AreEqual()
+ {
+ var r1 = new PropagatedHistoryActivityResult("Act", PropagatedHistoryStatus.Completed, "in", "out", null);
+ var r2 = new PropagatedHistoryActivityResult("Act", PropagatedHistoryStatus.Completed, "in", "out", null);
+ Assert.Equal(r1, r2);
+ }
+
+ [Fact]
+ public void RecordEquality_DifferentName_AreNotEqual()
+ {
+ var r1 = new PropagatedHistoryActivityResult("Act1", PropagatedHistoryStatus.Completed, null, null, null);
+ var r2 = new PropagatedHistoryActivityResult("Act2", PropagatedHistoryStatus.Completed, null, null, null);
+ Assert.NotEqual(r1, r2);
+ }
+
+ [Fact]
+ public void RecordEquality_DifferentStatus_AreNotEqual()
+ {
+ var r1 = new PropagatedHistoryActivityResult("Act", PropagatedHistoryStatus.Completed, null, null, null);
+ var r2 = new PropagatedHistoryActivityResult("Act", PropagatedHistoryStatus.Failed, null, null, null);
+ Assert.NotEqual(r1, r2);
+ }
+
+ [Fact]
+ public void CompletedActivity_HasOutputAndNoFailureDetails()
+ {
+ var result = new PropagatedHistoryActivityResult("Act", PropagatedHistoryStatus.Completed, """{"in":true}""", """{"out":42}""", null);
+ Assert.Equal(PropagatedHistoryStatus.Completed, result.Status);
+ Assert.NotNull(result.Output);
+ Assert.Null(result.FailureDetails);
+ }
+}
+
+public class PropagatedHistoryWorkflowResultTests
+{
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ var failure = new WorkflowTaskFailureDetails(typeof(Exception).FullName!, "err");
+ var result = new PropagatedHistoryWorkflowResult("ChildWF", PropagatedHistoryStatus.Failed, null, failure);
+
+ Assert.Equal("ChildWF", result.Name);
+ Assert.Equal(PropagatedHistoryStatus.Failed, result.Status);
+ Assert.Null(result.Output);
+ Assert.Same(failure, result.FailureDetails);
+ }
+
+ [Fact]
+ public void Constructor_SetsNullableFields_ToNull()
+ {
+ var result = new PropagatedHistoryWorkflowResult("CW", PropagatedHistoryStatus.Pending, null, null);
+ Assert.Null(result.Output);
+ Assert.Null(result.FailureDetails);
+ }
+
+ [Fact]
+ public void RecordEquality_SameValues_AreEqual()
+ {
+ var r1 = new PropagatedHistoryWorkflowResult("CW", PropagatedHistoryStatus.Completed, "out", null);
+ var r2 = new PropagatedHistoryWorkflowResult("CW", PropagatedHistoryStatus.Completed, "out", null);
+ Assert.Equal(r1, r2);
+ }
+
+ [Fact]
+ public void RecordEquality_DifferentName_AreNotEqual()
+ {
+ var r1 = new PropagatedHistoryWorkflowResult("CW1", PropagatedHistoryStatus.Completed, null, null);
+ var r2 = new PropagatedHistoryWorkflowResult("CW2", PropagatedHistoryStatus.Completed, null, null);
+ Assert.NotEqual(r1, r2);
+ }
+
+ [Fact]
+ public void RecordEquality_DifferentStatus_AreNotEqual()
+ {
+ var r1 = new PropagatedHistoryWorkflowResult("CW", PropagatedHistoryStatus.Completed, null, null);
+ var r2 = new PropagatedHistoryWorkflowResult("CW", PropagatedHistoryStatus.Pending, null, null);
+ Assert.NotEqual(r1, r2);
+ }
+
+ [Fact]
+ public void CompletedWorkflow_HasOutputAndNoFailureDetails()
+ {
+ var result = new PropagatedHistoryWorkflowResult("CW", PropagatedHistoryStatus.Completed, """{"done":true}""", null);
+ Assert.Equal(PropagatedHistoryStatus.Completed, result.Status);
+ Assert.NotNull(result.Output);
+ Assert.Null(result.FailureDetails);
+ }
+}
+
+public class PropagatedHistoryStatusTests
+{
+ [Fact]
+ public void Enum_HasExpectedValues()
+ {
+ Assert.Equal(0, (int)PropagatedHistoryStatus.Pending);
+ Assert.Equal(1, (int)PropagatedHistoryStatus.Completed);
+ Assert.Equal(2, (int)PropagatedHistoryStatus.Failed);
+ }
+
+ [Fact]
+ public void Enum_HasExactlyThreeValues()
+ {
+ var values = Enum.GetValues();
+ Assert.Equal(3, values.Length);
+ }
+
+ [Theory]
+ [InlineData(PropagatedHistoryStatus.Pending)]
+ [InlineData(PropagatedHistoryStatus.Completed)]
+ [InlineData(PropagatedHistoryStatus.Failed)]
+ public void Status_IsStoredCorrectly_InActivityResult(PropagatedHistoryStatus status)
+ {
+ var result = new PropagatedHistoryActivityResult("Act", status, null, null, null);
+ Assert.Equal(status, result.Status);
+ }
+
+ [Theory]
+ [InlineData(PropagatedHistoryStatus.Pending)]
+ [InlineData(PropagatedHistoryStatus.Completed)]
+ [InlineData(PropagatedHistoryStatus.Failed)]
+ public void Status_IsStoredCorrectly_InWorkflowResult(PropagatedHistoryStatus status)
+ {
+ var result = new PropagatedHistoryWorkflowResult("CW", status, null, null);
+ Assert.Equal(status, result.Status);
+ }
+}
diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs
index 5c8588c44..b17d1ff8b 100644
--- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs
+++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowHistoryPropagationTests.cs
@@ -28,9 +28,9 @@ namespace Dapr.Workflow.Test.Worker.Internal;
/// , propagating the scope to the
/// outgoing / ,
/// and exposing inbound propagated history through
-/// as
+/// as
/// values, each carrying typed /
-/// records.
+/// records.
///
public class WorkflowHistoryPropagationTests
{
@@ -162,13 +162,13 @@ public void GetPropagatedHistory_ReturnsSingleWorkflow_WhenOneEntryPropagated()
var history = context.GetPropagatedHistory();
Assert.NotNull(history);
- var entries = history.GetEntries();
+ var entries = history.Events;
Assert.Single(entries);
Assert.Equal("parent-app", entries[0].AppId);
Assert.Equal("parent-instance", entries[0].InstanceId);
Assert.Equal("ParentWorkflow", entries[0].Name);
Assert.Empty(entries[0].Activities);
- Assert.Empty(entries[0].ChildWorkflows);
+ Assert.Empty(entries[0].Workflows);
}
[Fact]
@@ -184,7 +184,7 @@ public void GetPropagatedHistory_PreservesEntryOrder()
var history = context.GetPropagatedHistory();
Assert.NotNull(history);
- var entries = history.GetEntries();
+ var entries = history.Events;
Assert.Equal(2, entries.Count);
Assert.Equal("gp-inst", entries[0].InstanceId);
Assert.Equal("p-inst", entries[1].InstanceId);
@@ -218,14 +218,11 @@ public void Activity_ResolvedAs_CompletedWithInputAndOutput()
TaskCompleted(eventId: 2, scheduledId: 1, result: "true"));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
Assert.True(workflow.TryGetLastActivityByName("ValidateMerchant", out var activity));
Assert.Equal("ValidateMerchant", activity.Name);
- Assert.True(activity.Started);
- Assert.True(activity.Completed);
- Assert.False(activity.Failed);
- Assert.Equal(PropagatedHistoryTaskStatus.Completed, activity.Status);
+ Assert.Equal(PropagatedHistoryStatus.Completed, activity.Status);
Assert.Equal("\"merchant-1\"", activity.Input);
Assert.Equal("true", activity.Output);
Assert.Null(activity.FailureDetails);
@@ -239,12 +236,10 @@ public void Activity_ResolvedAs_FailedWithFailureDetails()
TaskFailed(eventId: 2, scheduledId: 1, errorMessage: "card declined"));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
Assert.True(workflow.TryGetLastActivityByName("ValidateCard", out var activity));
- Assert.False(activity.Completed);
- Assert.True(activity.Failed);
- Assert.Equal(PropagatedHistoryTaskStatus.Failed, activity.Status);
+ Assert.Equal(PropagatedHistoryStatus.Failed, activity.Status);
Assert.NotNull(activity.FailureDetails);
Assert.Equal("card declined", activity.FailureDetails.ErrorMessage);
}
@@ -256,13 +251,10 @@ public void Activity_ResolvedAs_StartedOnly_WhenNotYetCompleted()
TaskScheduled(eventId: 1, name: "PendingCheck", input: "\"in\""));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
Assert.True(workflow.TryGetLastActivityByName("PendingCheck", out var activity));
- Assert.True(activity.Started);
- Assert.False(activity.Completed);
- Assert.False(activity.Failed);
- Assert.Equal(PropagatedHistoryTaskStatus.Pending, activity.Status);
+ Assert.Equal(PropagatedHistoryStatus.Pending, activity.Status);
Assert.Equal("\"in\"", activity.Input);
Assert.Null(activity.Output);
}
@@ -278,15 +270,15 @@ public void TryGetLastActivityByName_ReturnsMostRecentInvocation_WhenRetried()
TaskFailed(eventId: 4, scheduledId: 3, errorMessage: "card declined"));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
var all = workflow.GetActivitiesByName("ValidateCard");
Assert.Equal(2, all.Count);
- Assert.True(all[0].Completed);
- Assert.True(all[1].Failed);
+ Assert.Equal(PropagatedHistoryStatus.Completed, all[0].Status);
+ Assert.Equal(PropagatedHistoryStatus.Failed, all[1].Status);
Assert.True(workflow.TryGetLastActivityByName("ValidateCard", out var last));
- Assert.True(last.Failed);
+ Assert.Equal(PropagatedHistoryStatus.Failed, last.Status);
Assert.Equal("card declined", last.FailureDetails!.ErrorMessage);
}
@@ -297,7 +289,7 @@ public void TryGetLastActivityByName_ReturnsFalse_WhenMissing()
TaskScheduled(eventId: 1, name: "Real"));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
Assert.False(workflow.TryGetLastActivityByName("Missing", out var missing));
Assert.Null(missing);
@@ -315,13 +307,10 @@ public void ChildWorkflow_ResolvedAs_Completed()
ChildCompleted(eventId: 2, creationId: 1, result: "\"paid\""));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
- Assert.True(workflow.TryGetLastChildWorkflowByName("ProcessPayment", out var child));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
+ Assert.True(workflow.TryGetLastWorkflowByName("ProcessPayment", out var child));
- Assert.True(child.Started);
- Assert.True(child.Completed);
- Assert.False(child.Failed);
- Assert.Equal(PropagatedHistoryTaskStatus.Completed, child.Status);
+ Assert.Equal(PropagatedHistoryStatus.Completed, child.Status);
Assert.Equal("\"paid\"", child.Output);
}
@@ -333,11 +322,10 @@ public void ChildWorkflow_ResolvedAs_Failed()
ChildFailed(eventId: 2, creationId: 1, errorMessage: "boom"));
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
- Assert.True(workflow.TryGetLastChildWorkflowByName("ProcessPayment", out var child));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
+ Assert.True(workflow.TryGetLastWorkflowByName("ProcessPayment", out var child));
- Assert.True(child.Failed);
- Assert.Equal(PropagatedHistoryTaskStatus.Failed, child.Status);
+ Assert.Equal(PropagatedHistoryStatus.Failed, child.Status);
Assert.Equal("boom", child.FailureDetails!.ErrorMessage);
}
@@ -346,9 +334,9 @@ public void TryGetLastChildWorkflowByName_ReturnsFalse_WhenMissing()
{
var chunk = MakeChunk("app", "inst", "Wf");
var context = CreateContext(incomingPropagatedHistory: [chunk]);
- Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowByName("Wf", out var workflow));
+ Assert.True(context.GetPropagatedHistory()!.TryGetLastWorkflowEventByName("Wf", out var workflow));
- Assert.False(workflow.TryGetLastChildWorkflowByName("Missing", out var missing));
+ Assert.False(workflow.TryGetLastWorkflowByName("Missing", out var missing));
Assert.Null(missing);
}
@@ -360,9 +348,9 @@ public void TryGetLastChildWorkflowByName_ReturnsFalse_WhenMissing()
public void GetAppIds_ReturnsOrderedDeduplicatedList()
{
var history = new PropagatedHistory([
- new PropagatedHistoryEntry("i1", "appA", "WfA", [], []),
- new PropagatedHistoryEntry("i2", "appB", "WfB", [], []),
- new PropagatedHistoryEntry("i3", "appA", "WfA2", [], []),
+ new PropagatedHistoryEvent("i1", "appA", "WfA", [], []),
+ new PropagatedHistoryEvent("i2", "appB", "WfB", [], []),
+ new PropagatedHistoryEvent("i3", "appA", "WfA2", [], []),
]);
Assert.Equal(["appA", "appB"], history.GetAppIds());
@@ -372,20 +360,20 @@ public void GetAppIds_ReturnsOrderedDeduplicatedList()
public void TryGetLastWorkflowByName_ReturnsMostRecent_WhenNameRepeated()
{
var history = new PropagatedHistory([
- new PropagatedHistoryEntry("wf-1", "app", "Loop", [], []),
- new PropagatedHistoryEntry("wf-2", "app", "Loop", [], []),
+ new PropagatedHistoryEvent("wf-1", "app", "Loop", [], []),
+ new PropagatedHistoryEvent("wf-2", "app", "Loop", [], []),
]);
- Assert.True(history.TryGetLastWorkflowByName("Loop", out var last));
+ Assert.True(history.TryGetLastWorkflowEventByName("Loop", out var last));
Assert.Equal("wf-2", last.InstanceId);
- Assert.Equal(2, history.FilterByWorkflowName("Loop").Count);
+ Assert.Equal(2, history.GetEventsByWorkflowName("Loop").Count);
}
[Fact]
public void TryGetLastWorkflowByName_ReturnsFalse_WhenMissing()
{
var history = new PropagatedHistory([]);
- Assert.False(history.TryGetLastWorkflowByName("Missing", out var missing));
+ Assert.False(history.TryGetLastWorkflowEventByName("Missing", out var missing));
Assert.Null(missing);
}
@@ -395,6 +383,78 @@ public void PropagatedHistory_Ctor_ThrowsOnNullEntries()
Assert.Throws(() => new PropagatedHistory(null!));
}
+ [Fact]
+ public void GetByInstanceId_ReturnsMatchingEntries_WhenInstanceIdMatches()
+ {
+ var history = new PropagatedHistory([
+ new PropagatedHistoryEvent("inst-a", "app", "WfA", [], []),
+ new PropagatedHistoryEvent("inst-b", "app", "WfB", [], []),
+ new PropagatedHistoryEvent("inst-c", "app", "WfC", [], []),
+ ]);
+
+ var result = history.GetByInstanceId("inst-b");
+
+ Assert.Single(result);
+ Assert.Equal("inst-b", result[0].InstanceId);
+ }
+
+ [Fact]
+ public void GetByInstanceId_ReturnsEmptyList_WhenNoMatch()
+ {
+ var history = new PropagatedHistory([
+ new PropagatedHistoryEvent("inst-a", "app", "WfA", [], []),
+ ]);
+
+ var result = history.GetByInstanceId("inst-z");
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetByInstanceId_ReturnsBothEntries_WhenSameInstanceAppearsMultipleTimes()
+ {
+ // ContinueAsNew or replay can produce multiple chunks for the same instance ID.
+ var history = new PropagatedHistory([
+ new PropagatedHistoryEvent("inst-loop", "app", "LoopWf", [], []),
+ new PropagatedHistoryEvent("inst-other", "app", "OtherWf", [], []),
+ new PropagatedHistoryEvent("inst-loop", "app", "LoopWf", [], []),
+ ]);
+
+ var result = history.GetByInstanceId("inst-loop");
+
+ Assert.Equal(2, result.Count);
+ Assert.All(result, e => Assert.Equal("inst-loop", e.InstanceId));
+ }
+
+ [Fact]
+ public void GetByInstanceId_IsCaseSensitive()
+ {
+ // Instance IDs use Ordinal comparison (unlike app/workflow name lookups which are OrdinalIgnoreCase).
+ var history = new PropagatedHistory([
+ new PropagatedHistoryEvent("Instance-1", "app", "Wf", [], []),
+ ]);
+
+ Assert.Empty(history.GetByInstanceId("instance-1"));
+ Assert.Single(history.GetByInstanceId("Instance-1"));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void GetByInstanceId_ThrowsOnWhitespace(string? instanceId)
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetByInstanceId(instanceId!));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ public void GetByInstanceId_ThrowsOnNull(string? instanceId)
+ {
+ var history = new PropagatedHistory([]);
+ Assert.Throws(() => history.GetByInstanceId(instanceId!));
+ }
+
[Fact]
public void PropagatedHistory_NameAndAppIdLookups_AreCaseInsensitive()
{
@@ -402,50 +462,47 @@ public void PropagatedHistory_NameAndAppIdLookups_AreCaseInsensitive()
// and AppIds are matched case-insensitively elsewhere in the SDK. The propagated
// history lookups must follow the same contract.
var activity = new PropagatedHistoryActivityResult(
- Name: "ValidateMerchant", Started: true, Completed: true, Failed: false,
+ Name: "ValidateMerchant", Status: PropagatedHistoryStatus.Completed,
Input: null, Output: null, FailureDetails: null);
- var child = new PropagatedHistoryChildWorkflowResult(
- Name: "FraudDetection", Started: true, Completed: true, Failed: false,
+ var child = new PropagatedHistoryWorkflowResult(
+ Name: "FraudDetection", Status: PropagatedHistoryStatus.Completed,
Output: null, FailureDetails: null);
- var entry = new PropagatedHistoryEntry("inst-1", "AppA", "MerchantCheckout", [activity], [child]);
+ var entry = new PropagatedHistoryEvent("inst-1", "AppA", "MerchantCheckout", [activity], [child]);
var history = new PropagatedHistory([
entry,
- new PropagatedHistoryEntry("inst-2", "appa", "OtherWf", [], []),
+ new PropagatedHistoryEvent("inst-2", "appa", "OtherWf", [], []),
]);
// Both entries belong to the same app (differing only in casing), so the de-duped
// app-id list collapses to one, while a case-insensitive filter matches both entries.
Assert.Single(history.GetAppIds());
- Assert.Equal(2, history.FilterByAppId("APPA").Count);
- Assert.Single(history.FilterByWorkflowName("merchantcheckout"));
- Assert.True(history.TryGetLastWorkflowByName("MERCHANTCHECKOUT", out _));
+ Assert.Equal(2, history.GetByAppId("APPA").Count);
+ Assert.Single(history.GetEventsByWorkflowName("merchantcheckout"));
+ Assert.True(history.TryGetLastWorkflowEventByName("MERCHANTCHECKOUT", out _));
Assert.Single(entry.GetActivitiesByName("validatemerchant"));
Assert.True(entry.TryGetLastActivityByName("VALIDATEMERCHANT", out _));
- Assert.Single(entry.GetChildWorkflowsByName("frauddetection"));
- Assert.True(entry.TryGetLastChildWorkflowByName("FRAUDDETECTION", out _));
+ Assert.Single(entry.GetWorkflowsByName("frauddetection"));
+ Assert.True(entry.TryGetLastWorkflowByName("FRAUDDETECTION", out _));
}
[Fact]
- public void Status_ProjectsFlags_AndFailedTakesPrecedenceOverCompleted()
+ public void Status_IsStoredAndReadBackCorrectly()
{
var pending = new PropagatedHistoryActivityResult(
- Name: "A", Started: true, Completed: false, Failed: false,
+ Name: "A", Status: PropagatedHistoryStatus.Pending,
Input: null, Output: null, FailureDetails: null);
- var completed = pending with { Completed = true };
- var failed = pending with { Failed = true };
- // Defensive: if both flags are ever set, Failed wins.
- var both = pending with { Completed = true, Failed = true };
-
- Assert.Equal(PropagatedHistoryTaskStatus.Pending, pending.Status);
- Assert.Equal(PropagatedHistoryTaskStatus.Completed, completed.Status);
- Assert.Equal(PropagatedHistoryTaskStatus.Failed, failed.Status);
- Assert.Equal(PropagatedHistoryTaskStatus.Failed, both.Status);
-
- var child = new PropagatedHistoryChildWorkflowResult(
- Name: "C", Started: true, Completed: true, Failed: false,
+ var completed = pending with { Status = PropagatedHistoryStatus.Completed };
+ var failed = pending with { Status = PropagatedHistoryStatus.Failed };
+
+ Assert.Equal(PropagatedHistoryStatus.Pending, pending.Status);
+ Assert.Equal(PropagatedHistoryStatus.Completed, completed.Status);
+ Assert.Equal(PropagatedHistoryStatus.Failed, failed.Status);
+
+ var child = new PropagatedHistoryWorkflowResult(
+ Name: "C", Status: PropagatedHistoryStatus.Completed,
Output: null, FailureDetails: null);
- Assert.Equal(PropagatedHistoryTaskStatus.Completed, child.Status);
- Assert.Equal(PropagatedHistoryTaskStatus.Failed, (child with { Failed = true }).Status);
+ Assert.Equal(PropagatedHistoryStatus.Completed, child.Status);
+ Assert.Equal(PropagatedHistoryStatus.Failed, (child with { Status = PropagatedHistoryStatus.Failed }).Status);
}
// ------------------------------------------------------------------