Skip to content

Commit ae24b34

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

3 files changed

Lines changed: 38 additions & 47 deletions

File tree

src/RustPlusBot.Features.Alarms/Relaying/AlarmStateRelay.cs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
using Microsoft.Extensions.Logging;
33
using RustPlusBot.Abstractions.Events;
44
using RustPlusBot.Abstractions.Time;
5-
using RustPlusBot.Domain.Connections;
65
using RustPlusBot.Features.Alarms.Posting;
76
using RustPlusBot.Features.Alarms.Rendering;
87
using RustPlusBot.Features.Connections.Listening;
98
using RustPlusBot.Features.Workspace.Locating;
109
using RustPlusBot.Localization;
1110
using RustPlusBot.Persistence.Alarms;
12-
using RustPlusBot.Persistence.Connections;
13-
using RustPlusBot.Abstractions.Connections;
1411
using RustPlusBot.Persistence.Workspace;
1512

1613
namespace RustPlusBot.Features.Alarms.Relaying;
@@ -26,7 +23,7 @@ internal sealed record AlarmRelayChannels(
2623

2724
/// <summary>
2825
/// Keeps alarm embeds in sync with live socket events: updates state and re-renders on trigger; marks
29-
/// alarms unreachable when the server goes non-Connected.
26+
/// alarms unreachable only on a drop from Connected.
3027
/// </summary>
3128
/// <param name="scopeFactory">Opens scopes for the scoped stores.</param>
3229
/// <param name="refresher">Re-renders a single alarm embed on demand.</param>
@@ -104,27 +101,24 @@ await refresher.RefreshAsync(evt.GuildId, evt.ServerId, evt.EntityId, unreachabl
104101
}
105102
}
106103

107-
/// <summary>
108-
/// Handles a connection-status change: if the server is no longer Connected, marks every managed
109-
/// alarm's embed as unreachable. Connected → no-op (the supervisor's prime republishes real state).
110-
/// </summary>
104+
/// <summary>Handles a connection-status change: a drop from Connected marks its alarm embeds unreachable.</summary>
111105
/// <param name="evt">The connection-status change.</param>
112106
/// <param name="ct">A cancellation token.</param>
113107
/// <returns>A task that completes when every affected embed has been re-rendered.</returns>
114108
public async Task HandleConnectionStatusAsync(ConnectionStatusChangedEvent evt, CancellationToken ct)
115109
{
116110
ArgumentNullException.ThrowIfNull(evt);
111+
if (evt.IsConnected || !evt.WasConnected)
112+
{
113+
// Connected: the supervisor's prime path republishes real state — nothing to do.
114+
// Never-connected in this process (boot, reconnect-loop repeats): keep the last-run
115+
// embeds; only a drop from Connected sweeps them to unreachable.
116+
return;
117+
}
118+
117119
var scope = scopeFactory.CreateAsyncScope();
118120
await using (scope.ConfigureAwait(false))
119121
{
120-
var connections = scope.ServiceProvider.GetRequiredService<IConnectionStore>();
121-
var state = await connections.GetStateAsync(evt.GuildId, evt.ServerId, ct).ConfigureAwait(false);
122-
if (state is { Status: ConnectionStatus.Connected })
123-
{
124-
// The supervisor's prime path republishes real state on connect; nothing to do here.
125-
return;
126-
}
127-
128122
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
129123
var alarms = await store.ListByServerAsync(evt.GuildId, evt.ServerId, ct).ConfigureAwait(false);
130124
foreach (var alarm in alarms)

tests/RustPlusBot.Features.Alarms.Tests/AlarmStateRelayTests.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66
using RustPlusBot.Abstractions.Events;
77
using RustPlusBot.Abstractions.Time;
88
using RustPlusBot.Domain.Alarms;
9-
using RustPlusBot.Domain.Connections;
109
using RustPlusBot.Features.Alarms.Posting;
1110
using RustPlusBot.Features.Alarms.Relaying;
1211
using RustPlusBot.Features.Alarms.Rendering;
1312
using RustPlusBot.Features.Connections.Listening;
1413
using RustPlusBot.Features.Workspace.Locating;
1514
using RustPlusBot.Localization;
1615
using RustPlusBot.Persistence.Alarms;
17-
using RustPlusBot.Persistence.Connections;
1816
using RustPlusBot.Persistence.Workspace;
1917

2018
namespace RustPlusBot.Features.Alarms.Tests;
@@ -27,13 +25,11 @@ public sealed class AlarmStateRelayTests
2725
private static Harness Create(SmartAlarm? alarm = null, ulong? channelId = 777UL)
2826
{
2927
var store = Substitute.For<IAlarmStore>();
30-
var connections = Substitute.For<IConnectionStore>();
3128
var workspace = Substitute.For<IWorkspaceStore>();
3229
workspace.GetCultureAsync(Arg.Any<ulong>(), Arg.Any<CancellationToken>()).Returns("en");
3330

3431
var services = new ServiceCollection();
3532
services.AddScoped(_ => store);
36-
services.AddScoped(_ => connections);
3733
services.AddScoped(_ => workspace);
3834
var provider = services.BuildServiceProvider();
3935
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
@@ -67,7 +63,7 @@ private static Harness Create(SmartAlarm? alarm = null, ulong? channelId = 777UL
6763
clock,
6864
NullLogger<AlarmStateRelay>.Instance);
6965

70-
return new Harness(relay, store, refresher, poster, teamChatSender, connections);
66+
return new Harness(relay, store, refresher, poster, teamChatSender);
7167
}
7268

7369
// ──────────────────────────────────────────────────────────────────────────
@@ -353,19 +349,13 @@ await h.Refresher.Received(1)
353349
// Connection status
354350
// ──────────────────────────────────────────────────────────────────────────
355351

356-
/// <summary>Non-Connected server marks each alarm's embed as unreachable.</summary>
352+
/// <summary>Drop from Connected marks each alarm's embed as unreachable.</summary>
357353
[Fact]
358354
public async Task ConnectionStatus_not_connected_refreshes_all_alarms_unreachable()
359355
{
360356
var serverId = Guid.NewGuid();
361357
var h = Create();
362358

363-
h.Connections.GetStateAsync(10UL, serverId, Arg.Any<CancellationToken>())
364-
.Returns(new ConnectionState
365-
{
366-
GuildId = 10UL, RustServerId = serverId, Status = ConnectionStatus.Unreachable,
367-
});
368-
369359
h.Store.ListByServerAsync(10UL, serverId, Arg.Any<CancellationToken>())
370360
.Returns(
371361
[
@@ -396,12 +386,6 @@ public async Task ConnectionStatus_connected_does_nothing()
396386
var serverId = Guid.NewGuid();
397387
var h = Create();
398388

399-
h.Connections.GetStateAsync(10UL, serverId, Arg.Any<CancellationToken>())
400-
.Returns(new ConnectionState
401-
{
402-
GuildId = 10UL, RustServerId = serverId, Status = ConnectionStatus.Connected,
403-
});
404-
405389
await h.Relay.HandleConnectionStatusAsync(
406390
new ConnectionStatusChangedEvent(10UL, serverId, IsConnected: true, WasConnected: true),
407391
CancellationToken.None);
@@ -412,6 +396,30 @@ await h.Refresher.DidNotReceive()
412396
.RefreshAsync(Arg.Any<SmartAlarm>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
413397
}
414398

399+
/// <summary>Boot/reconnect-loop statuses (never Connected in this process) must not sweep — embeds keep their last-run state until the prime republishes.</summary>
400+
[Fact]
401+
public async Task ConnectionStatus_boot_without_prior_connection_does_not_sweep()
402+
{
403+
var serverId = Guid.NewGuid();
404+
var h = Create();
405+
406+
h.Store.ListByServerAsync(10UL, serverId, Arg.Any<CancellationToken>())
407+
.Returns(
408+
[
409+
new SmartAlarm
410+
{
411+
GuildId = 10UL, ServerId = serverId, EntityId = 42UL, Name = "A"
412+
},
413+
]);
414+
415+
await h.Relay.HandleConnectionStatusAsync(
416+
new ConnectionStatusChangedEvent(10UL, serverId, IsConnected: false, WasConnected: false),
417+
CancellationToken.None);
418+
419+
await h.Refresher.DidNotReceive()
420+
.RefreshAsync(Arg.Any<SmartAlarm>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
421+
}
422+
415423
// ──────────────────────────────────────────────────────────────────────────
416424
// Reachability changed
417425
// ──────────────────────────────────────────────────────────────────────────
@@ -464,6 +472,5 @@ private sealed record Harness(
464472
IAlarmStore Store,
465473
IAlarmRefresher Refresher,
466474
IAlarmChannelPoster Poster,
467-
IBotTeamChatSender TeamChatSender,
468-
IConnectionStore Connections);
475+
IBotTeamChatSender TeamChatSender);
469476
}

tests/RustPlusBot.Features.Alarms.Tests/Hosting/AlarmsHostedServiceTests.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using RustPlusBot.Abstractions.Events;
66
using RustPlusBot.Abstractions.Time;
77
using RustPlusBot.Domain.Alarms;
8-
using RustPlusBot.Domain.Connections;
98
using RustPlusBot.Features.Alarms.Hosting;
109
using RustPlusBot.Features.Alarms.Pairing;
1110
using RustPlusBot.Features.Alarms.Posting;
@@ -15,7 +14,6 @@
1514
using RustPlusBot.Features.Workspace.Locating;
1615
using RustPlusBot.Localization;
1716
using RustPlusBot.Persistence.Alarms;
18-
using RustPlusBot.Persistence.Connections;
1917
using RustPlusBot.Persistence.Workspace;
2018

2119
namespace RustPlusBot.Features.Alarms.Tests.Hosting;
@@ -27,13 +25,11 @@ public sealed class AlarmsHostedServiceTests
2725
private static Harness Create()
2826
{
2927
var store = Substitute.For<IAlarmStore>();
30-
var connections = Substitute.For<IConnectionStore>();
3128
var workspace = Substitute.For<IWorkspaceStore>();
3229
workspace.GetCultureAsync(Arg.Any<ulong>(), Arg.Any<CancellationToken>()).Returns("en");
3330

3431
var services = new ServiceCollection();
3532
services.AddScoped(_ => store);
36-
services.AddScoped(_ => connections);
3733
services.AddScoped(_ => workspace);
3834
var provider = services.BuildServiceProvider();
3935
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
@@ -76,7 +72,7 @@ private static Harness Create()
7672
relay,
7773
NullLogger<AlarmsHostedService>.Instance);
7874

79-
return new Harness(service, bus, store, connections, refresher, relayPoster, pairingLocator, pairingPoster);
75+
return new Harness(service, bus, store, refresher, relayPoster, pairingLocator, pairingPoster);
8076
}
8177

8278
[Fact]
@@ -178,11 +174,6 @@ public async Task ConnectionStatusChangedEvent_non_connected_routes_to_relay_and
178174
await h.Service.StartAsync(default);
179175

180176
var serverId = Guid.NewGuid();
181-
h.Connections.GetStateAsync(10UL, serverId, Arg.Any<CancellationToken>())
182-
.Returns(new ConnectionState
183-
{
184-
GuildId = 10UL, RustServerId = serverId, Status = ConnectionStatus.Unreachable,
185-
});
186177
h.Store.ListByServerAsync(10UL, serverId, Arg.Any<CancellationToken>())
187178
.Returns(
188179
[
@@ -264,7 +255,6 @@ private sealed record Harness(
264255
AlarmsHostedService Service,
265256
InMemoryEventBus Bus,
266257
IAlarmStore Store,
267-
IConnectionStore Connections,
268258
IAlarmRefresher Refresher,
269259
IAlarmChannelPoster Poster,
270260
IAlarmChannelLocator PairingLocator,

0 commit comments

Comments
 (0)