Skip to content

Commit 92434a2

Browse files
HandyS11claude
andcommitted
feat(switches): sweep embeds unreachable only on a drop from Connected
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 964578c commit 92434a2

3 files changed

Lines changed: 41 additions & 42 deletions

File tree

src/RustPlusBot.Features.Switches/Relaying/SwitchStateRelay.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
using Microsoft.Extensions.DependencyInjection;
2-
using RustPlusBot.Abstractions.Connections;
32
using RustPlusBot.Abstractions.Events;
4-
using RustPlusBot.Domain.Connections;
53
using RustPlusBot.Domain.Switches;
64
using RustPlusBot.Features.Switches.Posting;
75
using RustPlusBot.Features.Switches.Rendering;
86
using RustPlusBot.Features.Workspace.Locating;
9-
using RustPlusBot.Persistence.Connections;
107
using RustPlusBot.Persistence.Switches;
118
using RustPlusBot.Persistence.Workspace;
129

1310
namespace RustPlusBot.Features.Switches.Relaying;
1411

15-
/// <summary>Keeps switch embeds in sync: live state changes re-render; a non-Connected server marks them unreachable.</summary>
12+
/// <summary>Keeps switch embeds in sync: live state changes re-render; a drop from Connected marks them unreachable.</summary>
1613
/// <param name="scopeFactory">Opens scopes for the scoped stores.</param>
1714
/// <param name="locator">Resolves the #switches channel id.</param>
1815
/// <param name="poster">Posts/edits switch embeds.</param>
@@ -119,7 +116,7 @@ await RenderAsync(store, sw, sw.LastIsActive, evt.GuildId, evt.ServerId, culture
119116
}
120117
}
121118

122-
/// <summary>Handles a connection-status change: a non-Connected server marks its switch embeds unreachable.</summary>
119+
/// <summary>Handles a connection-status change: a drop from Connected marks its switch embeds unreachable.</summary>
123120
/// <param name="evt">The connection-status change.</param>
124121
/// <param name="cancellationToken">A cancellation token.</param>
125122
/// <returns>A task that completes when every affected embed has been re-rendered.</returns>
@@ -128,18 +125,17 @@ public async Task HandleConnectionStatusAsync(
128125
CancellationToken cancellationToken)
129126
{
130127
ArgumentNullException.ThrowIfNull(evt);
128+
if (evt.IsConnected || !evt.WasConnected)
129+
{
130+
// Connected: the supervisor's prime path republishes real state — nothing to do.
131+
// Never-connected in this process (boot, reconnect-loop repeats): keep the last-run
132+
// embeds; only a drop from Connected sweeps them to unreachable.
133+
return;
134+
}
135+
131136
var scope = scopeFactory.CreateAsyncScope();
132137
await using (scope.ConfigureAwait(false))
133138
{
134-
var connections = scope.ServiceProvider.GetRequiredService<IConnectionStore>();
135-
var state = await connections.GetStateAsync(evt.GuildId, evt.ServerId, cancellationToken)
136-
.ConfigureAwait(false);
137-
if (state is { Status: ConnectionStatus.Connected })
138-
{
139-
// The supervisor's prime path republishes real state on connect; nothing to do here.
140-
return;
141-
}
142-
143139
var store = scope.ServiceProvider.GetRequiredService<ISwitchStore>();
144140
var switches = await store.ListByServerAsync(evt.GuildId, evt.ServerId, cancellationToken)
145141
.ConfigureAwait(false);

tests/RustPlusBot.Features.Switches.Tests/Hosting/SwitchesHostedServiceTests.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using NSubstitute;
44
using RustPlusBot.Abstractions.Connections;
55
using RustPlusBot.Abstractions.Events;
6-
using RustPlusBot.Domain.Connections;
76
using RustPlusBot.Domain.Switches;
87
using RustPlusBot.Features.Switches.Hosting;
98
using RustPlusBot.Features.Switches.Pairing;
@@ -12,7 +11,6 @@
1211
using RustPlusBot.Features.Switches.Rendering;
1312
using RustPlusBot.Features.Workspace.Locating;
1413
using RustPlusBot.Localization;
15-
using RustPlusBot.Persistence.Connections;
1614
using RustPlusBot.Persistence.Switches;
1715
using RustPlusBot.Persistence.Workspace;
1816

@@ -23,13 +21,11 @@ public sealed class SwitchesHostedServiceTests
2321
private static Harness Create()
2422
{
2523
var store = Substitute.For<ISwitchStore>();
26-
var connections = Substitute.For<IConnectionStore>();
2724
var workspace = Substitute.For<IWorkspaceStore>();
2825
workspace.GetCultureAsync(Arg.Any<ulong>(), Arg.Any<CancellationToken>()).Returns("en");
2926

3027
var services = new ServiceCollection();
3128
services.AddScoped(_ => store);
32-
services.AddScoped(_ => connections);
3329
services.AddScoped(_ => workspace);
3430
var provider = services.BuildServiceProvider();
3531
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
@@ -60,7 +56,7 @@ private static Harness Create()
6056
relay,
6157
NullLogger<SwitchesHostedService>.Instance);
6258

63-
return new Harness(service, bus, store, connections, relayPoster, relayLocator, pairingLocator, pairingPoster);
59+
return new Harness(service, bus, store, relayPoster, relayLocator, pairingLocator, pairingPoster);
6460
}
6561

6662
[Fact]
@@ -129,11 +125,6 @@ public async Task ConnectionStatusChangedEvent_non_connected_routes_to_relay_and
129125
await h.Service.StartAsync(default);
130126

131127
var serverId = Guid.NewGuid();
132-
h.Connections.GetStateAsync(10UL, serverId, Arg.Any<CancellationToken>())
133-
.Returns(new ConnectionState
134-
{
135-
GuildId = 10UL, RustServerId = serverId, Status = ConnectionStatus.Unreachable,
136-
});
137128
h.Store.ListByServerAsync(10UL, serverId, Arg.Any<CancellationToken>())
138129
.Returns(
139130
[
@@ -266,7 +257,6 @@ private sealed record Harness(
266257
SwitchesHostedService Service,
267258
InMemoryEventBus Bus,
268259
ISwitchStore Store,
269-
IConnectionStore Connections,
270260
ISwitchChannelPoster Poster,
271261
ISwitchChannelLocator Locator,
272262
ISwitchChannelLocator PairingLocator,

tests/RustPlusBot.Features.Switches.Tests/SwitchStateRelayTests.cs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22
using NSubstitute;
33
using RustPlusBot.Abstractions.Connections;
44
using RustPlusBot.Abstractions.Events;
5-
using RustPlusBot.Domain.Connections;
65
using RustPlusBot.Domain.Switches;
76
using RustPlusBot.Features.Switches.Posting;
87
using RustPlusBot.Features.Switches.Relaying;
98
using RustPlusBot.Features.Switches.Rendering;
109
using RustPlusBot.Features.Workspace.Locating;
1110
using RustPlusBot.Localization;
12-
using RustPlusBot.Persistence.Connections;
1311
using RustPlusBot.Persistence.Switches;
1412
using RustPlusBot.Persistence.Workspace;
1513

@@ -20,13 +18,11 @@ public sealed class SwitchStateRelayTests
2018
private static Harness Create()
2119
{
2220
var store = Substitute.For<ISwitchStore>();
23-
var connections = Substitute.For<IConnectionStore>();
2421
var workspace = Substitute.For<IWorkspaceStore>();
2522
workspace.GetCultureAsync(Arg.Any<ulong>(), Arg.Any<CancellationToken>()).Returns("en");
2623

2724
var services = new ServiceCollection();
2825
services.AddScoped(_ => store);
29-
services.AddScoped(_ => connections);
3026
services.AddScoped(_ => workspace);
3127
var provider = services.BuildServiceProvider();
3228

@@ -39,7 +35,7 @@ private static Harness Create()
3935

4036
var relay = new SwitchStateRelay(provider.GetRequiredService<IServiceScopeFactory>(), locator, poster,
4137
renderer);
42-
return new Harness(relay, store, poster, connections);
38+
return new Harness(relay, store, poster);
4339
}
4440

4541
[Fact]
@@ -70,11 +66,6 @@ public async Task ConnectionStatus_not_connected_marks_switches_unreachable()
7066
{
7167
var h = Create();
7268
var serverId = Guid.NewGuid();
73-
h.Connections.GetStateAsync(10UL, serverId, Arg.Any<CancellationToken>())
74-
.Returns(new ConnectionState
75-
{
76-
GuildId = 10UL, RustServerId = serverId, Status = ConnectionStatus.Unreachable
77-
});
7869
h.Store.ListByServerAsync(10UL, serverId, Arg.Any<CancellationToken>())
7970
.Returns(
8071
[
@@ -101,11 +92,6 @@ public async Task ConnectionStatus_connected_does_nothing()
10192
{
10293
var h = Create();
10394
var serverId = Guid.NewGuid();
104-
h.Connections.GetStateAsync(10UL, serverId, Arg.Any<CancellationToken>())
105-
.Returns(new ConnectionState
106-
{
107-
GuildId = 10UL, RustServerId = serverId, Status = ConnectionStatus.Connected
108-
});
10995

11096
await h.Relay.HandleConnectionStatusAsync(
11197
new ConnectionStatusChangedEvent(10UL, serverId, IsConnected: true, WasConnected: true),
@@ -116,6 +102,34 @@ await h.Poster.DidNotReceive().EnsureAsync(Arg.Any<ulong>(), Arg.Any<ulong?>(),
116102
Arg.Any<global::Discord.MessageComponent>(), Arg.Any<CancellationToken>());
117103
}
118104

105+
/// <summary>Boot/reconnect-loop statuses (never Connected in this process) must not sweep — embeds keep their last-run state until the prime republishes.</summary>
106+
[Fact]
107+
public async Task ConnectionStatus_boot_without_prior_connection_does_not_sweep()
108+
{
109+
var h = Create();
110+
var serverId = Guid.NewGuid();
111+
h.Store.ListByServerAsync(10UL, serverId, Arg.Any<CancellationToken>())
112+
.Returns(
113+
[
114+
new SmartSwitch
115+
{
116+
GuildId = 10UL,
117+
ServerId = serverId,
118+
EntityId = 42UL,
119+
Name = "G",
120+
MessageId = 900UL
121+
}
122+
]);
123+
124+
await h.Relay.HandleConnectionStatusAsync(
125+
new ConnectionStatusChangedEvent(10UL, serverId, IsConnected: false, WasConnected: false),
126+
CancellationToken.None);
127+
128+
await h.Poster.DidNotReceive().EnsureAsync(Arg.Any<ulong>(), Arg.Any<ulong?>(),
129+
Arg.Any<global::Discord.Embed>(),
130+
Arg.Any<global::Discord.MessageComponent>(), Arg.Any<CancellationToken>());
131+
}
132+
119133
[Fact]
120134
public async Task DeviceTriggered_managed_switch_updates_store_and_rerenders()
121135
{
@@ -205,6 +219,5 @@ await h.Poster.Received(1).EnsureAsync(777UL, 900UL, Arg.Any<global::Discord.Emb
205219
private sealed record Harness(
206220
SwitchStateRelay Relay,
207221
ISwitchStore Store,
208-
ISwitchChannelPoster Poster,
209-
IConnectionStore Connections);
222+
ISwitchChannelPoster Poster);
210223
}

0 commit comments

Comments
 (0)