using Akka.Actor;
using Akka.TestKit.Xunit2;
namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests;
///
/// Pins the Akka behaviour that depends on: on a thread with
/// no actor context, Self and Context cannot be read.
///
///
///
/// This is here because assuming otherwise produced a real bug. Self resolves through
/// Context, which is [ThreadStatic]; a continuation that resumes on a thread without
/// actor context throws when reading either.
///
///
/// What made it dangerous is that it does not reliably throw. When the awaited operation
/// completes synchronously — which a local SQLite store usually does — the continuation stays on the
/// actor's mailbox thread and the context is still intact, so the illegal read succeeds. Correctness
/// then depends on store latency and thread-pool timing. The rule the production code follows is
/// therefore unconditional: capture Self (and Sender) into locals while still on the
/// actor thread, and never touch actor context after an await.
///
///
/// The illegal reads run on a dedicated new thread, not directly in the pool continuation.
/// An earlier revision read from the ConfigureAwait(false) continuation itself and flaked
/// (observed 2026-07-18 under parallel suite load): shared thread-pool threads are exactly where
/// Akka and the TestKit legitimately install the ThreadStatic cell during mailbox runs and async
/// continuations, so "no context here" is a timing accident, not a guarantee — worse, a cell whose
/// actor was already cleared makes Context return null and Self return the
/// cleared ref without throwing. The very unreliability this test documents made its old
/// formulation unsound. A fresh thread is the one place the no-context state is guaranteed.
///
///
public sealed class ActorContextAfterAwaitTests : TestKit
{
private sealed record Probe;
private sealed record Outcome(
string? SelfError, string? ContextError, string? CapturedSelfPath,
string? StaleSelfPath, string? StaleContextPath);
private sealed class ProbeActor : ReceiveActor
{
public ProbeActor()
{
ReceiveAsync(async probe =>
{
IActorRef replyTo = Sender;
// The correct pattern: capture while the context is still current.
IActorRef capturedSelf = Self;
// The realistic escape: after this, the handler is off the mailbox thread.
await Task.Delay(50).ConfigureAwait(false);
string? selfError = null;
string? contextError = null;
string? staleSelfPath = null;
string? staleContextPath = null;
// LongRunning = a brand-new thread, where no actor cell can ever have been
// installed. Reading Self/Context THERE is the deterministic form of the hazard.
await Task.Factory.StartNew(
() =>
{
try
{
// If this ever succeeds the failure message needs to say whose
// context the thread was carrying — that is the whole diagnosis.
staleSelfPath = Self.Path.ToString();
}
catch (Exception ex)
{
selfError = ex.GetType().Name;
}
try
{
staleContextPath = Context.Self.Path.ToString();
}
catch (Exception ex)
{
contextError = ex.GetType().Name;
}
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default).ConfigureAwait(false);
replyTo.Tell(new Outcome(
selfError, contextError, capturedSelf.Path.ToString(),
staleSelfPath, staleContextPath));
});
}
}
[Fact]
public void Self_and_Context_are_both_unreadable_on_a_thread_without_actor_context()
{
IActorRef probe = Sys.ActorOf(Props.Create(() => new ProbeActor()), "context-probe");
probe.Tell(new Probe());
Outcome outcome = ExpectMsg(TimeSpan.FromSeconds(10));
// Both throw — Self is not a cached field, it goes through the ThreadStatic context.
Assert.True(
outcome.SelfError == "NotSupportedException",
$"Self read did not throw NotSupportedException (got: {outcome.SelfError ?? "no exception"}; " +
$"stale Self path: '{outcome.StaleSelfPath}').");
Assert.True(
outcome.ContextError == "NotSupportedException",
$"Context read did not throw NotSupportedException (got: {outcome.ContextError ?? "no exception"}; " +
$"stale Context path: '{outcome.StaleContextPath}').");
// ...but a reference captured beforehand stays valid, which is what the actor relies on.
Assert.EndsWith("/user/context-probe", outcome.CapturedSelfPath, StringComparison.Ordinal);
}
}