Files
wx-win-agent/tests/node-agent/WxAgent.Core.Tests/RemoteDataSyncTests.cs
T

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);
}