164 lines
6.9 KiB
C#
164 lines
6.9 KiB
C#
using WxAgent.Core;
|
|
using Xunit;
|
|
|
|
namespace WxAgent.Core.Tests;
|
|
|
|
public sealed class RemoteDataSyncTests
|
|
{
|
|
[Fact]
|
|
public void QueuePersistsAuthorizedBatchesAndAcksIdempotently()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "wxagent-sync-" + Guid.NewGuid().ToString("N"), "queue.json");
|
|
var config = ConfigFor("chat-a");
|
|
var batch = Batch("batch-1", "chat-a");
|
|
try
|
|
{
|
|
var queue = new RemoteDataBatchQueue(path, maxItems: 2, maxBytes: 1024 * 1024);
|
|
var accepted = queue.Enqueue(config, batch);
|
|
Assert.True(accepted.Accepted);
|
|
Assert.False(accepted.Duplicate);
|
|
Assert.Equal(1, queue.PendingCount);
|
|
|
|
var reopened = new RemoteDataBatchQueue(path, maxItems: 2, maxBytes: 1024 * 1024);
|
|
var duplicate = reopened.Enqueue(config, batch);
|
|
Assert.True(duplicate.Accepted);
|
|
Assert.True(duplicate.Duplicate);
|
|
Assert.Equal(1, reopened.PendingCount);
|
|
|
|
var ack = new RemoteDataBatchAck(true, false, batch.BatchId, batch.Sequence, batch.CursorEnd);
|
|
Assert.True(reopened.MarkConfirmed(ack));
|
|
Assert.False(reopened.MarkConfirmed(ack));
|
|
Assert.Equal(0, reopened.PendingCount);
|
|
}
|
|
finally
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (directory is not null && Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void QueueDropsUnauthorizedRecordsAndRejectsFullyUnauthorizedBatch()
|
|
{
|
|
var config = ConfigFor("chat-a");
|
|
var mixed = Batch("mixed", "chat-a") with
|
|
{
|
|
Messages = [Message("message-a", "chat-a"), Message("message-b", "chat-b")],
|
|
};
|
|
var filtered = RemoteDataBatchAuthorization.Filter(config, mixed, out var reason);
|
|
Assert.NotNull(filtered);
|
|
Assert.Equal("SomeRecordsDroppedByReporting", reason);
|
|
Assert.Single(filtered!.Messages);
|
|
Assert.Equal("chat-a", filtered.Messages[0].ChatId);
|
|
Assert.Equal("partial", filtered.CoverageState);
|
|
|
|
var denied = RemoteDataBatchAuthorization.Filter(config, Batch("denied", "chat-b"), out reason);
|
|
Assert.Null(denied);
|
|
Assert.Equal("AllRecordsDroppedByReporting", reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void SyncStatePersistsConfirmedCursorAndResetsOnSourceGenerationChange()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "wxagent-sync-state-" + Guid.NewGuid().ToString("N"), "state.json");
|
|
try
|
|
{
|
|
var state = new RemoteDataSyncStateStore(path);
|
|
var checkpoint = state.GetLatest("account-a", "messages");
|
|
Assert.Equal(0, checkpoint.ConfirmedSequence);
|
|
var batch = Batch("batch-state", "chat-a") with
|
|
{
|
|
CursorStart = "{}",
|
|
CursorEnd = "{\"chat-a\\u001fmessage/a.db\":7}",
|
|
Sequence = 1,
|
|
};
|
|
state.MarkCollected(batch, []);
|
|
state.MarkConfirmed(batch);
|
|
|
|
var reopened = new RemoteDataSyncStateStore(path);
|
|
var confirmed = reopened.GetLatest("account-a", "messages");
|
|
Assert.Equal(1, confirmed.ConfirmedSequence);
|
|
Assert.Equal(7, confirmed.ConfirmedCursors["chat-a\u001fmessage/a.db"]);
|
|
var reset = reopened.Get("account-a", "messages", "new-source-generation");
|
|
Assert.Equal(0, reset.ConfirmedSequence);
|
|
Assert.Empty(reset.ConfirmedCursors);
|
|
}
|
|
finally
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (directory is not null && Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ReconcileResetsLocalCheckpointAfterPlatformRollbackAndDropsPendingBatches()
|
|
{
|
|
var statePath = Path.Combine(Path.GetTempPath(), "wxagent-sync-state-" + Guid.NewGuid().ToString("N"), "state.json");
|
|
var queuePath = Path.Combine(Path.GetTempPath(), "wxagent-sync-" + Guid.NewGuid().ToString("N"), "queue.json");
|
|
try
|
|
{
|
|
var config = ConfigFor("chat-a");
|
|
var state = new RemoteDataSyncStateStore(statePath);
|
|
var queue = new RemoteDataBatchQueue(queuePath);
|
|
var batch = Batch("rollback-batch", "chat-a") with { CursorEnd = "{\"chat-a\\u001fmessage/a.db\":9}" };
|
|
state.MarkCollected(batch, []);
|
|
state.MarkConfirmed(batch);
|
|
Assert.True(queue.Enqueue(config, batch).Accepted);
|
|
|
|
Assert.True(state.Reconcile(new RemoteDataSyncStatus("unknown", "generation-a", 0, "", null), "account-a", "messages"));
|
|
Assert.Equal(0, state.GetLatest("account-a", "messages").ConfirmedSequence);
|
|
Assert.Equal(1, queue.DropForAccount("account-a", "messages"));
|
|
Assert.Empty(queue.Pending());
|
|
}
|
|
finally
|
|
{
|
|
foreach (var path in new[] { statePath, queuePath })
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (directory is not null && Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void QueueReportsBackpressureWithoutDroppingExistingBatches()
|
|
{
|
|
var config = ConfigFor("chat-a");
|
|
var path = Path.Combine(Path.GetTempPath(), "wxagent-sync-" + Guid.NewGuid().ToString("N"), "queue.json");
|
|
var queue = new RemoteDataBatchQueue(path, maxItems: 1, maxBytes: 1024 * 1024);
|
|
try
|
|
{
|
|
Assert.True(queue.Enqueue(config, Batch("batch-1", "chat-a")).Accepted);
|
|
var rejected = queue.Enqueue(config, Batch("batch-2", "chat-a"));
|
|
Assert.False(rejected.Accepted);
|
|
Assert.Equal("SyncQueueFull", rejected.Reason);
|
|
Assert.Equal(1, queue.PendingCount);
|
|
}
|
|
finally
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (directory is not null && Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
private static ReportingConfig ConfigFor(string chatId) => new()
|
|
{
|
|
Enabled = true,
|
|
ConfigVersion = 1,
|
|
Accounts = [new AccountReportingConfig
|
|
{
|
|
AccountId = "account-a",
|
|
Enabled = true,
|
|
AllowedChats = [new AllowedChat { Type = ReportingChatType.Private, ChatId = chatId, Enabled = true, IdentityVerified = true }]
|
|
}]
|
|
};
|
|
|
|
private static RemoteSyncBatch Batch(string batchId, string chatId) => new(
|
|
"node-a", "account-a", batchId, "generation-a", "messages", 1, "0", batchId,
|
|
"hash-" + batchId, "complete", [], [Message("message-" + batchId, chatId)]);
|
|
|
|
private static RemoteSyncMessage Message(string messageId, string chatId) => new(
|
|
messageId, chatId, ReportingChatType.Private, "source-" + messageId, "incoming", "text", "hello",
|
|
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, "wx-1", "payload-" + messageId);
|
|
}
|