Skip to content

Commit ff4b7fc

Browse files
HandyS11claude
andcommitted
refactor(alarms): reuse open scope for coordinator culture read + assert prompt replacement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b82c320 commit ff4b7fc

2 files changed

Lines changed: 37 additions & 39 deletions

File tree

src/RustPlusBot.Features.Alarms/Pairing/AlarmPairingCoordinator.cs

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,30 @@ internal sealed class AlarmPairingCoordinator(
3737
public async Task HandlePairedAsync(AlarmPairedEvent evt, CancellationToken cancellationToken)
3838
{
3939
ArgumentNullException.ThrowIfNull(evt);
40-
if (await ExistsAsync(evt.GuildId, evt.ServerId, evt.EntityId, cancellationToken).ConfigureAwait(false))
41-
{
42-
return;
43-
}
4440

45-
var channelId = await locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, cancellationToken)
46-
.ConfigureAwait(false);
47-
if (channelId is not { } channel)
41+
var scope = scopeFactory.CreateAsyncScope();
42+
await using (scope.ConfigureAwait(false))
4843
{
49-
return;
50-
}
44+
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
45+
if (await store.ExistsAsync(evt.GuildId, evt.ServerId, evt.EntityId, cancellationToken).ConfigureAwait(false))
46+
{
47+
return;
48+
}
5149

52-
var culture = await GetCultureAsync(evt.GuildId, cancellationToken).ConfigureAwait(false);
53-
var defaultName = $"Alarm {evt.EntityId}";
54-
var (embed, components) = renderer.RenderPrompt(evt.ServerId, evt.EntityId, defaultName, culture);
55-
var messageId = await poster.EnsureAsync(channel, null, embed, components, cancellationToken)
56-
.ConfigureAwait(false);
57-
_pending[(evt.GuildId, evt.ServerId, evt.EntityId)] = new Pending(defaultName, messageId);
50+
var channelId = await locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, cancellationToken)
51+
.ConfigureAwait(false);
52+
if (channelId is not { } channel)
53+
{
54+
return;
55+
}
56+
57+
var culture = await GetCultureAsync(scope.ServiceProvider, evt.GuildId, cancellationToken).ConfigureAwait(false);
58+
var defaultName = $"Alarm {evt.EntityId}";
59+
var (embed, components) = renderer.RenderPrompt(evt.ServerId, evt.EntityId, defaultName, culture);
60+
var messageId = await poster.EnsureAsync(channel, null, embed, components, cancellationToken)
61+
.ConfigureAwait(false);
62+
_pending[(evt.GuildId, evt.ServerId, evt.EntityId)] = new Pending(defaultName, messageId);
63+
}
5864
}
5965

6066
/// <summary>Accepts a pending pairing: persist + replace prompt with the alarm embed. Race-guarded.</summary>
@@ -71,26 +77,26 @@ public async Task<bool> TryAcceptAsync(
7177
ulong acceptingUserId,
7278
CancellationToken cancellationToken)
7379
{
74-
if (await ExistsAsync(guildId, serverId, entityId, cancellationToken).ConfigureAwait(false))
75-
{
76-
_pending.TryRemove((guildId, serverId, entityId), out _);
77-
return false;
78-
}
79-
8080
_pending.TryGetValue((guildId, serverId, entityId), out var pending);
8181
var name = pending?.DefaultName ?? $"Alarm {entityId}";
8282

8383
var scope = scopeFactory.CreateAsyncScope();
8484
await using (scope.ConfigureAwait(false))
8585
{
8686
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
87+
if (await store.ExistsAsync(guildId, serverId, entityId, cancellationToken).ConfigureAwait(false))
88+
{
89+
_pending.TryRemove((guildId, serverId, entityId), out _);
90+
return false;
91+
}
92+
8793
var added = await store.AddAsync(guildId, serverId, entityId, name, acceptingUserId, cancellationToken)
8894
.ConfigureAwait(false);
8995

9096
var channelId = await locator.GetChannelIdAsync(guildId, serverId, cancellationToken).ConfigureAwait(false);
9197
if (channelId is { } channel)
9298
{
93-
var culture = await GetCultureAsync(guildId, cancellationToken).ConfigureAwait(false);
99+
var culture = await GetCultureAsync(scope.ServiceProvider, guildId, cancellationToken).ConfigureAwait(false);
94100

95101
// The alarm is freshly accepted; unreachable is false (it just paired).
96102
// The supervisor's prime path will re-render real state shortly.
@@ -118,24 +124,10 @@ await store.SetMessageIdAsync(guildId, serverId, entityId, mid, cancellationToke
118124
public bool TryDismiss(ulong guildId, Guid serverId, ulong entityId) =>
119125
_pending.TryRemove((guildId, serverId, entityId), out _);
120126

121-
private async Task<bool> ExistsAsync(ulong guildId, Guid serverId, ulong entityId, CancellationToken ct)
122-
{
123-
var scope = scopeFactory.CreateAsyncScope();
124-
await using (scope.ConfigureAwait(false))
125-
{
126-
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
127-
return await store.ExistsAsync(guildId, serverId, entityId, ct).ConfigureAwait(false);
128-
}
129-
}
130-
131-
private async Task<string> GetCultureAsync(ulong guildId, CancellationToken ct)
127+
private static async Task<string> GetCultureAsync(IServiceProvider provider, ulong guildId, CancellationToken ct)
132128
{
133-
var scope = scopeFactory.CreateAsyncScope();
134-
await using (scope.ConfigureAwait(false))
135-
{
136-
var store = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
137-
return await store.GetCultureAsync(guildId, ct).ConfigureAwait(false);
138-
}
129+
var store = provider.GetRequiredService<IWorkspaceStore>();
130+
return await store.GetCultureAsync(guildId, ct).ConfigureAwait(false);
139131
}
140132

141133
private sealed record Pending(string DefaultName, ulong? MessageId);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ public async Task Accept_persists_alarm_and_replaces_prompt()
9191
Assert.True(ok);
9292
await h.Store.Received(1).AddAsync(10UL, serverId, 42UL, "Alarm 42", 5UL, Arg.Any<CancellationToken>());
9393
Assert.Null(h.Coordinator.PendingName(10UL, serverId, 42UL)); // pending cleared
94+
// The prompt (posted by HandlePairedAsync) must be replaced by the alarm embed (posted by TryAcceptAsync).
95+
// EnsureAsync is called twice: once for the prompt (messageId=null), once for the embed (messageId=900 from prompt).
96+
await h.Poster.Received(2).EnsureAsync(777UL, Arg.Any<ulong?>(), Arg.Any<global::Discord.Embed>(),
97+
Arg.Any<global::Discord.MessageComponent>(), Arg.Any<CancellationToken>());
98+
await h.Poster.Received(1).EnsureAsync(777UL, 900UL, Arg.Any<global::Discord.Embed>(),
99+
Arg.Any<global::Discord.MessageComponent>(), Arg.Any<CancellationToken>());
94100
}
95101

96102
[Fact]

0 commit comments

Comments
 (0)