Isolate MAX chat commands from outbox sends
This commit is contained in:
@@ -753,6 +753,78 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OutboxSendsQueuedMessagesBeforePendingChatActions()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient();
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IMaxBridgeClient>();
|
||||
services.AddSingleton<IMaxBridgeClient>(bridge);
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var deleteChat = await CreateDirectChatAsync(client, "delete-after-send-chat", "Delete After Send");
|
||||
var deleteResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([deleteChat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
|
||||
var sendChat = await CreateDirectChatAsync(client, "send-before-delete-chat", "Send Before Delete");
|
||||
var messageResponse = await client.PostAsJsonAsync(
|
||||
$"/api/chats/{sendChat.Id}/messages",
|
||||
new SendMessageRequest("send before chat action"),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.OK, messageResponse);
|
||||
|
||||
Assert.Equal(2, await ProcessOutboxAsync(factory));
|
||||
Assert.Equal(["send:text", "delete"], bridge.OperationOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FailedPendingChatActionWaitsBeforeImmediateRetry()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient { AlwaysFailDelete = true };
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IMaxBridgeClient>();
|
||||
services.AddSingleton<IMaxBridgeClient>(bridge);
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var chat = await CreateDirectChatAsync(client, "retry-delay-delete-chat", "Retry Delay Delete");
|
||||
var deleteResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
|
||||
Assert.True(await ProcessOutboxAsync(factory) >= 1);
|
||||
Assert.Single(bridge.DeleteChatUrls);
|
||||
|
||||
await ProcessOutboxAsync(factory);
|
||||
Assert.Single(bridge.DeleteChatUrls);
|
||||
|
||||
await using var scope = factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.SingleAsync(x => x.Id == chat.Id);
|
||||
Assert.Equal(ChatPendingMaxAction.DeleteChat, stored.PendingMaxAction);
|
||||
Assert.Equal(1, stored.PendingMaxActionAttempts);
|
||||
Assert.NotNull(stored.PendingMaxActionLastAttemptAt);
|
||||
Assert.Equal("Simulated MAX delete failure.", stored.PendingMaxActionError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkClearHistoryQueuesMaxActionAndClearsLocalMessages()
|
||||
{
|
||||
@@ -1662,6 +1734,10 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
public List<string?> DeleteChatUrls { get; } = [];
|
||||
public string? ClearChatUrl { get; private set; }
|
||||
public List<string?> ClearChatUrls { get; } = [];
|
||||
public List<string> OperationOrder { get; } = [];
|
||||
public List<string> SentTexts { get; } = [];
|
||||
public bool AlwaysFailDelete { get; set; }
|
||||
public bool AlwaysFailClear { get; set; }
|
||||
public IReadOnlyList<MaxChatUpdate> Updates { get; set; } = [];
|
||||
|
||||
public Task<MaxBridgeStatus> GetStatusAsync(CancellationToken cancellationToken)
|
||||
@@ -1707,8 +1783,14 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
|
||||
public Task<MaxActionResult> ClearChatHistoryAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
OperationOrder.Add("clear");
|
||||
ClearChatUrls.Add(chatUrl);
|
||||
ClearChatUrl = chatUrl;
|
||||
if (AlwaysFailClear)
|
||||
{
|
||||
return Task.FromResult(new MaxActionResult(false, "Simulated MAX clear failure."));
|
||||
}
|
||||
|
||||
if (string.Equals(chatUrl, _staleUrlToReject, StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(new MaxActionResult(false, "MAX chat menu was not opened."));
|
||||
@@ -1721,8 +1803,14 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
|
||||
public Task<MaxActionResult> DeleteChatAsync(string externalChatId, string? chatUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
OperationOrder.Add("delete");
|
||||
DeleteChatUrls.Add(chatUrl);
|
||||
DeleteChatUrl = chatUrl;
|
||||
if (AlwaysFailDelete)
|
||||
{
|
||||
return Task.FromResult(new MaxActionResult(false, "Simulated MAX delete failure."));
|
||||
}
|
||||
|
||||
if (string.Equals(chatUrl, _staleUrlToReject, StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult(new MaxActionResult(false, "MAX chat menu was not opened."));
|
||||
@@ -1735,11 +1823,14 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
|
||||
public Task<MaxSendResult> SendTextAsync(string externalChatId, string? chatUrl, string text, CancellationToken cancellationToken)
|
||||
{
|
||||
OperationOrder.Add("send:text");
|
||||
SentTexts.Add(text);
|
||||
return Task.FromResult(new MaxSendResult(true, $"external-{Guid.NewGuid():N}", null));
|
||||
}
|
||||
|
||||
public Task<MaxSendResult> SendAttachmentAsync(string externalChatId, string? chatUrl, string path, string caption, CancellationToken cancellationToken)
|
||||
{
|
||||
OperationOrder.Add("send:attachment");
|
||||
return Task.FromResult(new MaxSendResult(true, $"external-file-{Guid.NewGuid():N}", null));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user