using Akka.Actor;
using Akka.TestKit.Xunit2;
namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests;
///
/// Pins the Akka behaviour that depends on: after a genuine
/// thread hop, actor context is gone, so Self 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-pool
/// thread has none, and reading it throws .
///
///
/// 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.
///
///
public sealed class ActorContextAfterAwaitTests : TestKit
{
private sealed record Probe;
private sealed record Outcome(string? SelfError, string? ContextError, string? CapturedSelfPath);
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;
// Force a real hop off the mailbox thread.
await Task.Delay(50).ConfigureAwait(false);
await Task.Yield();
string? selfError = null;
string? contextError = null;
try
{
ActorPath path = Self.Path;
GC.KeepAlive(path);
}
catch (Exception ex)
{
selfError = ex.GetType().Name;
}
try
{
IActorRef fromContext = Context.Self;
GC.KeepAlive(fromContext);
}
catch (Exception ex)
{
contextError = ex.GetType().Name;
}
replyTo.Tell(new Outcome(selfError, contextError, capturedSelf.Path.ToString()));
});
}
}
[Fact]
public void Self_and_Context_are_both_unreadable_after_a_real_thread_hop()
{
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.Equal("NotSupportedException", outcome.SelfError);
Assert.Equal("NotSupportedException", outcome.ContextError);
// ...but a reference captured beforehand stays valid, which is what the actor relies on.
Assert.EndsWith("/user/context-probe", outcome.CapturedSelfPath, StringComparison.Ordinal);
}
}