From 5193bea26214ecda2c7910bb47acf09181efba34 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 14 Sep 2026 16:10:12 +0200 Subject: [PATCH] fix(workflows): default CreateBookmarkArgs.IncludeActivityInstanceId to true (#8157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unset CreateBookmarkArgs objects were hashing without ActivityInstanceId, so CreateBookmark(stimulus) and new CreateBookmarkArgs { Stimulus = … } silently diverged from CreateBookmark() and the documented overload default. Explicit IncludeActivityInstanceId = false call sites are unchanged. Co-authored-by: Cursor Agent --- .../Models/CreateBookmarkArgs.cs | 6 +- .../Models/CreateBookmarkArgsTests.cs | 110 ++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 test/unit/Elsa.Workflows.Core.UnitTests/Models/CreateBookmarkArgsTests.cs diff --git a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs index 477275a95..05df27302 100644 --- a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs +++ b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs @@ -31,9 +31,11 @@ public class CreateBookmarkArgs public bool AutoBurn { get; set; } = true; /// - /// Whether the activity instance ID should be included in the bookmark payload. + /// Whether the activity instance ID should be included in the stimulus hash. Defaults to true + /// so unset objects match CreateBookmark() and documented overload defaults. + /// Set to false for broadcast-style bookmarks that must resume regardless of activity instance. /// - public bool IncludeActivityInstanceId { get; set; } + public bool IncludeActivityInstanceId { get; set; } = true; /// /// Whether the activity being resumed should be automatically completed if CallBack is not specified. diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Models/CreateBookmarkArgsTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Models/CreateBookmarkArgsTests.cs new file mode 100644 index 000000000..a3e290d3a --- /dev/null +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Models/CreateBookmarkArgsTests.cs @@ -0,0 +1,110 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.Core.UnitTests.Models; + +public class CreateBookmarkArgsTests +{ + [Fact] + public void NewCreateBookmarkArgs_IncludeActivityInstanceId_DefaultsToTrue() + { + var args = new CreateBookmarkArgs(); + + Assert.True(args.IncludeActivityInstanceId); + } + + [Fact] + public void UnsetIncludeActivityInstanceId_InObjectInitializer_IsTrue() + { + var args = new CreateBookmarkArgs + { + Stimulus = "order-created" + }; + + Assert.True(args.IncludeActivityInstanceId); + } + + [Fact] + public void ExplicitFalse_IsPreserved() + { + var args = new CreateBookmarkArgs + { + Stimulus = "order-created", + IncludeActivityInstanceId = false + }; + + Assert.False(args.IncludeActivityInstanceId); + } + + [Fact] + public async Task CreateBookmark_StimulusConvenience_MatchesParameterlessIdentityInclusion() + { + var context = await CreateContextAsync(); + + var noArgs = context.CreateBookmark(); + var withStimulus = context.CreateBookmark("order-created"); + + Assert.Contains(context.Id, noArgs.Hash); + Assert.Contains(context.Id, withStimulus.Hash); + } + + [Fact] + public async Task CreateBookmark_UnsetArgs_MatchesStimulusConvenienceHash() + { + var context = await CreateContextAsync(); + const string stimulus = "order-created"; + + var fromConvenience = context.CreateBookmark(stimulus); + var fromUnsetArgs = context.CreateBookmark(new CreateBookmarkArgs + { + Stimulus = stimulus + }); + var fromCallbackOverload = context.CreateBookmark(stimulus, callback: null); + + Assert.Equal(fromConvenience.Hash, fromUnsetArgs.Hash); + Assert.Equal(fromConvenience.Hash, fromCallbackOverload.Hash); + Assert.Contains(context.Id, fromUnsetArgs.Hash); + } + + [Fact] + public async Task CreateBookmark_ExplicitFalse_ExcludesActivityInstanceIdFromHash() + { + var context = await CreateContextAsync(); + const string stimulus = "order-created"; + + var included = context.CreateBookmark(stimulus); + var excluded = context.CreateBookmark(new CreateBookmarkArgs + { + Stimulus = stimulus, + IncludeActivityInstanceId = false + }); + + Assert.Contains(context.Id, included.Hash); + Assert.DoesNotContain(context.Id, excluded.Hash); + Assert.NotEqual(included.Hash, excluded.Hash); + } + + private static Task CreateContextAsync() + { + var fixture = new ActivityTestFixture(new WriteLine("test")); + fixture.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + }); + return fixture.BuildAsync(); + } + + /// + /// Encodes hash inputs so tests can see whether the activity instance ID was passed through. + /// + private sealed class ConcatHasher : IHasher + { + public string Hash(string value) => value; + + public string Hash(params object?[] values) => + string.Join("|", values.Select(value => value?.ToString() ?? string.Empty)); + } +}