Skip to content
Merged
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
37 changes: 37 additions & 0 deletions PolyPilot.Tests/VolatileFieldGuardTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using PolyPilot.Services;

namespace PolyPilot.Tests;

/// <summary>
/// Guards that fields accessed from multiple threads are declared volatile.
/// Prevents regressions where someone removes the volatile modifier.
/// </summary>
public class VolatileFieldGuardTests
{
[Fact]
public void ActiveSessionName_IsDeclaredVolatile_OnCopilotService()
{
// _activeSessionName is read by WsBridge background threads (SyncRemoteSessions),
// restore background threads, and written by UI thread (SetActiveSession, CloseSession).
// Must be volatile for cross-thread visibility on ARM (iOS/Android).
var field = typeof(CopilotService)
.GetField("_activeSessionName", BindingFlags.NonPublic | BindingFlags.Instance)!;
Assert.NotNull(field);
Assert.True(
field.GetRequiredCustomModifiers().Any(m => m == typeof(IsVolatile)),
"_activeSessionName must be declared volatile for cross-thread visibility");
}

[Fact]
public void ActiveSessionName_IsDeclaredVolatile_OnDemoService()
{
var field = typeof(DemoService)
.GetField("_activeSessionName", BindingFlags.NonPublic | BindingFlags.Instance)!;
Assert.NotNull(field);
Assert.True(
field.GetRequiredCustomModifiers().Any(m => m == typeof(IsVolatile)),
"DemoService._activeSessionName must be declared volatile for consistency");
}
}
2 changes: 1 addition & 1 deletion PolyPilot/Services/CopilotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public partial class CopilotService : IAsyncDisposable
// Cached dotfiles status — checked once when first SetupRequired state is encountered
private CodespaceService.DotfilesStatus? _dotfilesStatus;
private ConnectionSettings? _currentSettings;
private string? _activeSessionName;
private volatile string? _activeSessionName;
private SynchronizationContext? _syncContext;
// Serializes the IsConnectionError reconnect path so concurrent workers
// don't destroy each other's freshly-created client (thundering herd fix).
Expand Down
2 changes: 1 addition & 1 deletion PolyPilot/Services/DemoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace PolyPilot.Services;
public class DemoService : IDemoService
{
private readonly ConcurrentDictionary<string, AgentSessionInfo> _sessions = new();
private string? _activeSessionName;
private volatile string? _activeSessionName;
private int _sessionCounter;

public event Action? OnStateChanged;
Expand Down