Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Terminal.Gui/App/ApplicationImpl.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public void Invoke (Action action)
TopRunnable = runnable;

// Update cached state atomically - IsRunning and IsModal are now consistent
SessionBegun?.Invoke (this, new SessionTokenEventArgs (token));
// (CWP: state mutations happen BEFORE the SessionBegun notification below)
runnable.SetIsRunning (true);
runnable.SetIsModal (true);

Expand All @@ -161,6 +161,10 @@ public void Invoke (Action action)

// END CRITICAL SECTION - IsRunning/IsModal now thread-safe

// CWP: Raise SessionBegun AFTER all state mutations so subscribers
// observe a fully-consistent session token (IsRunning/IsModal == true).
SessionBegun?.Invoke (this, new SessionTokenEventArgs (token));

// Fire events AFTER lock released (avoid deadlocks in event handlers)
previousTop?.RaiseIsModalChangedEvent (false);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#nullable enable
namespace UnitTests.NonParallelizable.ApplicationTests;

/// <summary>
/// Regression tests for issue #5162. Ensures that <see cref="IApplication.SessionBegun"/>
/// is raised AFTER all session state mutations, per the Cancellable Workflow Pattern (CWP):
/// subscribers must observe a fully-consistent <see cref="SessionToken"/> with
/// <see cref="IRunnable.IsRunning"/> and <see cref="IRunnable.IsModal"/> already set to
/// <see langword="true"/>.
/// </summary>
public class SessionBegunCwpTests
{
// Claude - Opus 4.7
[Fact]
public void SessionBegun_Raised_After_IsRunning_And_IsModal_Set_True ()
{
ApplicationImpl.ResetModelUsageTracking ();

IApplication app = Application.Create ();
Runnable? runnable = null;

bool? observedIsRunning = null;
bool? observedIsModal = null;

try
{
runnable = new ();

EventHandler<SessionTokenEventArgs> handler = (_, e) =>
{
IRunnable? r = e.State.Runnable;

if (r is null) { return; }

observedIsRunning = r.IsRunning;
observedIsModal = r.IsModal;
};

app.SessionBegun += handler;

SessionToken? token = app.Begin (runnable);

app.SessionBegun -= handler;

Assert.NotNull (token);
Assert.True (observedIsRunning, "SessionBegun fired before SetIsRunning(true).");
Assert.True (observedIsModal, "SessionBegun fired before SetIsModal(true).");

app.End (token);
}
finally
{
runnable?.Dispose ();
app.Dispose ();
ApplicationImpl.ResetModelUsageTracking ();
}
}
}
Loading