diff --git a/src/ScadaLink.Commons/Messages/InboundApi/RouteToInstanceRequest.cs b/src/ScadaLink.Commons/Messages/InboundApi/RouteToInstanceRequest.cs
index 1a020b4..90bc6f1 100644
--- a/src/ScadaLink.Commons/Messages/InboundApi/RouteToInstanceRequest.cs
+++ b/src/ScadaLink.Commons/Messages/InboundApi/RouteToInstanceRequest.cs
@@ -4,12 +4,21 @@ namespace ScadaLink.Commons.Messages.InboundApi;
/// Request routed from Inbound API to a site to invoke a script on an instance.
/// Used by Route.To("instanceCode").Call("scriptName", params).
///
+///
+/// Audit Log #23 (ParentExecutionId): the spawning execution's ExecutionId
+/// — for an inbound-API-routed call this is the inbound request's per-request
+/// execution id. The site records it as the routed script execution's
+/// ParentExecutionId so a spawned execution points back at its spawner.
+/// Additive trailing member — null for requests built before the field existed
+/// or for routed calls with no spawning execution (e.g. the Central UI sandbox).
+///
public record RouteToCallRequest(
string CorrelationId,
string InstanceUniqueName,
string ScriptName,
IReadOnlyDictionary? Parameters,
- DateTimeOffset Timestamp);
+ DateTimeOffset Timestamp,
+ Guid? ParentExecutionId = null);
///
/// Response from a Route.To() call.
diff --git a/src/ScadaLink.InboundAPI/EndpointExtensions.cs b/src/ScadaLink.InboundAPI/EndpointExtensions.cs
index 4daec2c..3319800 100644
--- a/src/ScadaLink.InboundAPI/EndpointExtensions.cs
+++ b/src/ScadaLink.InboundAPI/EndpointExtensions.cs
@@ -92,8 +92,21 @@ public static class EndpointExtensions
? TimeSpan.FromSeconds(method.TimeoutSeconds)
: options.DefaultMethodTimeout;
+ // Audit Log #23 (ParentExecutionId): the inbound request's per-request
+ // ExecutionId was minted early by AuditWriteMiddleware and stashed on
+ // HttpContext.Items. Thread it into the executor so a routed
+ // Route.To(...).Call(...) carries it as RouteToCallRequest.ParentExecutionId
+ // — the spawned site script execution points back at this inbound request.
+ var parentExecutionId =
+ httpContext.Items.TryGetValue(
+ AuditWriteMiddleware.InboundExecutionIdItemKey, out var stashedExecutionId)
+ && stashedExecutionId is Guid inboundExecutionId
+ ? inboundExecutionId
+ : (Guid?)null;
+
var scriptResult = await executor.ExecuteAsync(
- method, paramResult.Parameters, routeHelper, timeout, httpContext.RequestAborted);
+ method, paramResult.Parameters, routeHelper, timeout,
+ httpContext.RequestAborted, parentExecutionId);
if (!scriptResult.Success)
{
diff --git a/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs b/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs
index ad969f2..2a68abe 100644
--- a/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs
+++ b/src/ScadaLink.InboundAPI/InboundScriptExecutor.cs
@@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.InboundApi;
+using ScadaLink.Commons.Messages.InboundApi;
using ScadaLink.Commons.Types;
namespace ScadaLink.InboundAPI;
@@ -156,12 +157,22 @@ public class InboundScriptExecutor
///
/// Executes the script for the given method with the provided context.
///
+ ///
+ /// Audit Log #23 (ParentExecutionId): the inbound API request's per-request
+ /// ExecutionId (minted early by AuditWriteMiddleware and stashed
+ /// on HttpContext.Items). When supplied, a routed
+ /// Route.To(...).Call(...) inside the script carries it as
+ /// so the spawned site
+ /// script execution points back at this inbound request. Null when the script
+ /// runs outside an inbound API request flow.
+ ///
public async Task ExecuteAsync(
ApiMethod method,
IReadOnlyDictionary parameters,
RouteHelper route,
TimeSpan timeout,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken = default,
+ Guid? parentExecutionId = null)
{
// InboundAPI-004: keep the timeout source and the request-abort source
// separable. A single linked CTS makes a genuine client disconnect
@@ -177,7 +188,14 @@ public class InboundScriptExecutor
// InboundAPI-016: bind the route helper to the method deadline so a
// routed Route.To(...).Call(...) inherits the method-level timeout
// without the script having to thread the context token by hand.
- var context = new InboundScriptContext(parameters, route.WithDeadline(cts.Token), cts.Token);
+ //
+ // Audit Log #23 (ParentExecutionId): also bind the inbound request's
+ // ExecutionId so a routed call carries it as ParentExecutionId — the
+ // spawned site script execution points back at this inbound request.
+ var context = new InboundScriptContext(
+ parameters,
+ route.WithDeadline(cts.Token).WithParentExecutionId(parentExecutionId),
+ cts.Token);
if (!_scriptHandlers.TryGetValue(method.Name, out var handler))
{
diff --git a/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs b/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs
index b4b8410..71ce437 100644
--- a/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs
+++ b/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs
@@ -59,6 +59,18 @@ public sealed class AuditWriteMiddleware
///
public const string AuditActorItemKey = "ScadaLink.InboundAPI.AuditActor";
+ ///
+ /// Audit Log #23 (ParentExecutionId): key under
+ /// which this middleware stashes the inbound request's per-request
+ /// ExecutionId (a ) at the very start of the request.
+ /// The id is minted ONCE and shared: the endpoint handler reads it to thread it
+ /// onto a routed RouteToCallRequest.ParentExecutionId, and the
+ /// middleware's own inbound audit row uses the same id for its
+ /// . Exposed as a constant so the handler
+ /// and middleware share a single source of truth (no stringly-typed coupling).
+ ///
+ public const string InboundExecutionIdItemKey = "ScadaLink.InboundAPI.InboundExecutionId";
+
private readonly RequestDelegate _next;
private readonly ICentralAuditWriter _auditWriter;
private readonly ILogger _logger;
@@ -77,6 +89,17 @@ public sealed class AuditWriteMiddleware
{
var sw = Stopwatch.StartNew();
+ // Audit Log #23 (ParentExecutionId): mint the inbound request's per-request
+ // ExecutionId ONCE, here at the start of the request, and stash it on
+ // HttpContext.Items. Two consumers share this single id:
+ // (a) the endpoint handler reads it to thread onto a routed
+ // RouteToCallRequest.ParentExecutionId, so a spawned site script
+ // execution points back at this inbound request;
+ // (b) the inbound audit row this middleware emits uses it as its own
+ // ExecutionId (the row stays top-level — its ParentExecutionId is
+ // never set).
+ ctx.Items[InboundExecutionIdItemKey] = Guid.NewGuid();
+
// Buffer the request body up front so we can both audit it and let the
// downstream handler still parse it. EnableBuffering swaps the request
// stream for a seekable wrapper that the framework rewinds at the end
@@ -145,17 +168,14 @@ public sealed class AuditWriteMiddleware
OccurredAtUtc = DateTime.UtcNow,
Channel = AuditChannel.ApiInbound,
Kind = kind,
- // Audit Log #23: a fresh per-request execution id so the
- // inbound row carries a request identifier (closes the design
- // gap that inbound rows should be correlatable).
- //
- // This id is intentionally request-local: it is NOT bridged to
- // RouteHelper's routed-call correlation id or to
- // HttpContext.TraceIdentifier. Threading an inbound request's
- // execution id through to the routed script execution (so an
- // inbound call and the outbound API/DB rows it triggers share
- // one id) is a deliberate future follow-up, out of scope here.
- ExecutionId = Guid.NewGuid(),
+ // Audit Log #23: the per-request execution id minted ONCE at the
+ // start of the request (InvokeAsync) and stashed on
+ // HttpContext.Items. The same id is threaded onto a routed
+ // RouteToCallRequest.ParentExecutionId by the endpoint handler,
+ // so an inbound request and the site script it routes to share
+ // one correlation point. This inbound row stays top-level — its
+ // own ParentExecutionId is never set (see below).
+ ExecutionId = ResolveInboundExecutionId(ctx),
// CorrelationId is purely the per-operation-lifecycle id; an
// inbound request is a one-shot from the audit row's
// perspective with no multi-row operation to correlate.
@@ -225,6 +245,24 @@ public sealed class AuditWriteMiddleware
}
}
+ ///
+ /// Audit Log #23 (ParentExecutionId): reads the inbound request's per-request
+ /// ExecutionId that minted and stashed on
+ /// under .
+ /// Falls back to a fresh id only if the slot is somehow absent — the inbound
+ /// audit row must always carry an execution id.
+ ///
+ private static Guid ResolveInboundExecutionId(HttpContext ctx)
+ {
+ if (ctx.Items.TryGetValue(InboundExecutionIdItemKey, out var stashed)
+ && stashed is Guid id)
+ {
+ return id;
+ }
+
+ return Guid.NewGuid();
+ }
+
///
/// Reads the API key name the endpoint handler stashed on
/// after successful auth. Falls back to
diff --git a/src/ScadaLink.InboundAPI/RouteHelper.cs b/src/ScadaLink.InboundAPI/RouteHelper.cs
index dc83d63..6380d13 100644
--- a/src/ScadaLink.InboundAPI/RouteHelper.cs
+++ b/src/ScadaLink.InboundAPI/RouteHelper.cs
@@ -19,22 +19,25 @@ public class RouteHelper
private readonly IInstanceLocator _instanceLocator;
private readonly IInstanceRouter _instanceRouter;
private readonly CancellationToken _deadlineToken;
+ private readonly Guid? _parentExecutionId;
public RouteHelper(
IInstanceLocator instanceLocator,
IInstanceRouter instanceRouter)
- : this(instanceLocator, instanceRouter, CancellationToken.None)
+ : this(instanceLocator, instanceRouter, CancellationToken.None, parentExecutionId: null)
{
}
private RouteHelper(
IInstanceLocator instanceLocator,
IInstanceRouter instanceRouter,
- CancellationToken deadlineToken)
+ CancellationToken deadlineToken,
+ Guid? parentExecutionId)
{
_instanceLocator = instanceLocator;
_instanceRouter = instanceRouter;
_deadlineToken = deadlineToken;
+ _parentExecutionId = parentExecutionId;
}
///
@@ -45,14 +48,27 @@ public class RouteHelper
/// requires.
///
public RouteHelper WithDeadline(CancellationToken deadlineToken) =>
- new(_instanceLocator, _instanceRouter, deadlineToken);
+ new(_instanceLocator, _instanceRouter, deadlineToken, _parentExecutionId);
+
+ ///
+ /// Audit Log #23 (ParentExecutionId): returns a whose
+ /// routed requests carry
+ /// as .
+ /// For an inbound API request this is the inbound request's own per-request
+ /// execution id, so the routed site script records the inbound request as its
+ /// parent. calls this when it builds the
+ /// script context.
+ ///
+ public RouteHelper WithParentExecutionId(Guid? parentExecutionId) =>
+ new(_instanceLocator, _instanceRouter, _deadlineToken, parentExecutionId);
///
/// Creates a route target for the specified instance.
///
public RouteTarget To(string instanceCode)
{
- return new RouteTarget(instanceCode, _instanceLocator, _instanceRouter, _deadlineToken);
+ return new RouteTarget(
+ instanceCode, _instanceLocator, _instanceRouter, _deadlineToken, _parentExecutionId);
}
}
@@ -65,17 +81,20 @@ public class RouteTarget
private readonly IInstanceLocator _instanceLocator;
private readonly IInstanceRouter _instanceRouter;
private readonly CancellationToken _deadlineToken;
+ private readonly Guid? _parentExecutionId;
internal RouteTarget(
string instanceCode,
IInstanceLocator instanceLocator,
IInstanceRouter instanceRouter,
- CancellationToken deadlineToken)
+ CancellationToken deadlineToken,
+ Guid? parentExecutionId)
{
_instanceCode = instanceCode;
_instanceLocator = instanceLocator;
_instanceRouter = instanceRouter;
_deadlineToken = deadlineToken;
+ _parentExecutionId = parentExecutionId;
}
///
@@ -96,8 +115,13 @@ public class RouteTarget
var siteId = await ResolveSiteAsync(token);
var correlationId = Guid.NewGuid().ToString();
+ // Audit Log #23 (ParentExecutionId): stamp the spawning execution's id
+ // (the inbound API request's ExecutionId) so the routed site script
+ // records this call's parent. CorrelationId above is a separate concern
+ // — the per-operation lifecycle id, freshly minted per routed call.
var request = new RouteToCallRequest(
- correlationId, _instanceCode, scriptName, ScriptArgs.Normalize(parameters), DateTimeOffset.UtcNow);
+ correlationId, _instanceCode, scriptName, ScriptArgs.Normalize(parameters),
+ DateTimeOffset.UtcNow, _parentExecutionId);
var response = await _instanceRouter.RouteToCallAsync(siteId, request, token);
diff --git a/tests/ScadaLink.InboundAPI.Tests/InboundScriptExecutorTests.cs b/tests/ScadaLink.InboundAPI.Tests/InboundScriptExecutorTests.cs
index 5fbc6ee..e8cbf85 100644
--- a/tests/ScadaLink.InboundAPI.Tests/InboundScriptExecutorTests.cs
+++ b/tests/ScadaLink.InboundAPI.Tests/InboundScriptExecutorTests.cs
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Services;
+using ScadaLink.Commons.Messages.InboundApi;
namespace ScadaLink.InboundAPI.Tests;
@@ -427,6 +428,71 @@ public class InboundScriptExecutorTests
Assert.Contains("whatever", result.ResultJson!);
}
+ // --- Audit Log #23 (ParentExecutionId, T3): the inbound request's
+ // ExecutionId is threaded through ExecuteAsync onto routed calls ---
+
+ [Fact]
+ public async Task ExecuteAsync_WithParentExecutionId_RoutedCallCarriesItAsParentExecutionId()
+ {
+ // The endpoint hands ExecuteAsync the inbound request's ExecutionId; a
+ // routed Route.To(...).Call(...) inside the script must stamp that id onto
+ // the RouteToCallRequest as ParentExecutionId.
+ var inboundExecutionId = Guid.NewGuid();
+
+ var locator = Substitute.For();
+ locator.GetSiteIdForInstanceAsync("inst-1", Arg.Any()).Returns("SiteA");
+ var router = Substitute.For();
+ RouteToCallRequest? captured = null;
+ router.RouteToCallAsync("SiteA", Arg.Do(r => captured = r), Arg.Any())
+ .Returns(ci => new RouteToCallResponse(
+ ((RouteToCallRequest)ci[1]).CorrelationId, true, null, null, DateTimeOffset.UtcNow));
+ var route = new RouteHelper(locator, router);
+
+ var method = new ApiMethod("routes", "return 1;") { Id = 1, TimeoutSeconds = 10 };
+ _executor.RegisterHandler("routes", async ctx =>
+ {
+ await ctx.Route.To("inst-1").Call("doWork");
+ return 1;
+ });
+
+ var result = await _executor.ExecuteAsync(
+ method, new Dictionary(), route, TimeSpan.FromSeconds(10),
+ parentExecutionId: inboundExecutionId);
+
+ Assert.True(result.Success, result.ErrorMessage);
+ Assert.NotNull(captured);
+ Assert.Equal(inboundExecutionId, captured!.ParentExecutionId);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithoutParentExecutionId_RoutedCallHasNullParentExecutionId()
+ {
+ // ExecuteAsync called without a parent execution id (the default) routes
+ // calls with ParentExecutionId null.
+ var locator = Substitute.For();
+ locator.GetSiteIdForInstanceAsync("inst-1", Arg.Any()).Returns("SiteA");
+ var router = Substitute.For();
+ RouteToCallRequest? captured = null;
+ router.RouteToCallAsync("SiteA", Arg.Do(r => captured = r), Arg.Any())
+ .Returns(ci => new RouteToCallResponse(
+ ((RouteToCallRequest)ci[1]).CorrelationId, true, null, null, DateTimeOffset.UtcNow));
+ var route = new RouteHelper(locator, router);
+
+ var method = new ApiMethod("routes2", "return 1;") { Id = 1, TimeoutSeconds = 10 };
+ _executor.RegisterHandler("routes2", async ctx =>
+ {
+ await ctx.Route.To("inst-1").Call("doWork");
+ return 1;
+ });
+
+ var result = await _executor.ExecuteAsync(
+ method, new Dictionary(), route, TimeSpan.FromSeconds(10));
+
+ Assert.True(result.Success, result.ErrorMessage);
+ Assert.NotNull(captured);
+ Assert.Null(captured!.ParentExecutionId);
+ }
+
private sealed class CompileLogCounter
{
public int CompilationFailures;
diff --git a/tests/ScadaLink.InboundAPI.Tests/Middleware/AuditWriteMiddlewareTests.cs b/tests/ScadaLink.InboundAPI.Tests/Middleware/AuditWriteMiddlewareTests.cs
index 436507f..a28a6f6 100644
--- a/tests/ScadaLink.InboundAPI.Tests/Middleware/AuditWriteMiddlewareTests.cs
+++ b/tests/ScadaLink.InboundAPI.Tests/Middleware/AuditWriteMiddlewareTests.cs
@@ -395,6 +395,78 @@ public class AuditWriteMiddlewareTests
Assert.NotEqual(writer.Events[0].ExecutionId, writer.Events[1].ExecutionId);
}
+ // ---------------------------------------------------------------------
+ // ParentExecutionId — Audit Log #23 (ParentExecutionId feature, T3): the
+ // inbound request's ExecutionId is minted ONCE, early, and stashed on
+ // HttpContext.Items so the endpoint handler can carry it onto the routed
+ // RouteToCallRequest as ParentExecutionId. The inbound row that the
+ // middleware itself emits stays top-level — its own ParentExecutionId is
+ // NEVER set.
+ // ---------------------------------------------------------------------
+
+ [Fact]
+ public async Task InboundExecutionId_IsStashedOnHttpItems_BeforeEndpointRuns()
+ {
+ var writer = new RecordingAuditWriter();
+ var ctx = BuildContext();
+ object? observedDuringHandler = null;
+ var mw = CreateMiddleware(hc =>
+ {
+ // The endpoint handler must be able to read the early-minted id —
+ // it is stashed before _next so a downstream reader sees it.
+ hc.Items.TryGetValue(AuditWriteMiddleware.InboundExecutionIdItemKey, out observedDuringHandler);
+ hc.Response.StatusCode = 200;
+ return Task.CompletedTask;
+ }, writer);
+
+ await mw.InvokeAsync(ctx);
+
+ var stashed = Assert.IsType(observedDuringHandler);
+ Assert.NotEqual(Guid.Empty, stashed);
+ }
+
+ [Fact]
+ public async Task InboundRow_ExecutionId_Equals_TheEarlyMintedStashedId()
+ {
+ var writer = new RecordingAuditWriter();
+ var ctx = BuildContext();
+ Guid stashedDuringHandler = Guid.Empty;
+ var mw = CreateMiddleware(hc =>
+ {
+ stashedDuringHandler =
+ (Guid)hc.Items[AuditWriteMiddleware.InboundExecutionIdItemKey]!;
+ hc.Response.StatusCode = 200;
+ return Task.CompletedTask;
+ }, writer);
+
+ await mw.InvokeAsync(ctx);
+
+ // The inbound audit row's ExecutionId must be the SAME id minted early
+ // and shared with the endpoint handler — not a second, late mint.
+ var evt = Assert.Single(writer.Events);
+ Assert.Equal(stashedDuringHandler, evt.ExecutionId);
+ }
+
+ [Fact]
+ public async Task InboundRow_OwnParentExecutionId_StaysNull()
+ {
+ var writer = new RecordingAuditWriter();
+ var ctx = BuildContext();
+ var mw = CreateMiddleware(_ =>
+ {
+ ctx.Response.StatusCode = 200;
+ return Task.CompletedTask;
+ }, writer);
+
+ await mw.InvokeAsync(ctx);
+
+ // The inbound request is itself top-level — only the spawn id flows
+ // OUT on RouteToCallRequest. The inbound row's own ParentExecutionId
+ // is never set.
+ var evt = Assert.Single(writer.Events);
+ Assert.Null(evt.ParentExecutionId);
+ }
+
[Fact]
public async Task DurationMs_IsRecorded()
{
diff --git a/tests/ScadaLink.InboundAPI.Tests/RouteHelperTests.cs b/tests/ScadaLink.InboundAPI.Tests/RouteHelperTests.cs
index 3feaaaf..8dabaa7 100644
--- a/tests/ScadaLink.InboundAPI.Tests/RouteHelperTests.cs
+++ b/tests/ScadaLink.InboundAPI.Tests/RouteHelperTests.cs
@@ -233,6 +233,71 @@ public class RouteHelperTests
Assert.Equal(explicitCts.Token, seen);
}
+ // --- Audit Log #23 (ParentExecutionId, T3): a routed call carries the
+ // inbound request's ExecutionId as RouteToCallRequest.ParentExecutionId ---
+
+ [Fact]
+ public async Task Call_WithoutParentExecutionId_LeavesParentExecutionIdNull()
+ {
+ // A RouteHelper not bound to an inbound execution id (e.g. the Central UI
+ // sandbox path) builds requests with ParentExecutionId null.
+ SiteResolves("inst-1", "SiteA");
+ RouteToCallRequest? captured = null;
+ _router.RouteToCallAsync("SiteA", Arg.Do(r => captured = r), Arg.Any())
+ .Returns(ci => new RouteToCallResponse(
+ ((RouteToCallRequest)ci[1]).CorrelationId, true, null, null, DateTimeOffset.UtcNow));
+
+ await CreateHelper().To("inst-1").Call("doWork");
+
+ Assert.NotNull(captured);
+ Assert.Null(captured!.ParentExecutionId);
+ }
+
+ [Fact]
+ public async Task Call_WithParentExecutionId_CarriesItOnRouteToCallRequest()
+ {
+ // A RouteHelper bound to the inbound request's ExecutionId must stamp that
+ // id onto the routed RouteToCallRequest so the site script records it as
+ // its ParentExecutionId.
+ SiteResolves("inst-1", "SiteA");
+ var inboundExecutionId = Guid.NewGuid();
+ RouteToCallRequest? captured = null;
+ _router.RouteToCallAsync("SiteA", Arg.Do(r => captured = r), Arg.Any())
+ .Returns(ci => new RouteToCallResponse(
+ ((RouteToCallRequest)ci[1]).CorrelationId, true, null, null, DateTimeOffset.UtcNow));
+
+ var bound = CreateHelper().WithParentExecutionId(inboundExecutionId);
+ await bound.To("inst-1").Call("doWork");
+
+ Assert.NotNull(captured);
+ Assert.Equal(inboundExecutionId, captured!.ParentExecutionId);
+ // ParentExecutionId is a separate concern from the per-op CorrelationId —
+ // the helper still mints its own routed-call correlation id.
+ Assert.True(Guid.TryParse(captured.CorrelationId, out _));
+ }
+
+ [Fact]
+ public async Task WithParentExecutionId_PreservesDeadlineToken()
+ {
+ // The two builder methods compose — binding a parent execution id must
+ // not drop a previously-bound deadline token.
+ SiteResolves("inst-1", "SiteA");
+ using var deadline = new CancellationTokenSource();
+ CancellationToken seen = default;
+ RouteToCallRequest? captured = null;
+ _router.RouteToCallAsync("SiteA", Arg.Do(r => captured = r), Arg.Do(t => seen = t))
+ .Returns(ci => new RouteToCallResponse(
+ ((RouteToCallRequest)ci[1]).CorrelationId, true, null, null, DateTimeOffset.UtcNow));
+
+ var bound = CreateHelper()
+ .WithDeadline(deadline.Token)
+ .WithParentExecutionId(Guid.NewGuid());
+ await bound.To("inst-1").Call("doWork");
+
+ Assert.Equal(deadline.Token, seen);
+ Assert.NotNull(captured!.ParentExecutionId);
+ }
+
[Fact]
public async Task GetAttributes_WithNoExplicitToken_InheritsMethodDeadlineToken()
{