feat(S2-b): connect durable MQTT sync to capture workflow

This commit is contained in:
Brent Perteet
2026-08-20 15:22:29 -05:00
parent a788cebea3
commit 71fcb9b63f
23 changed files with 1680 additions and 22 deletions

View File

@@ -0,0 +1,61 @@
using FieldLogger.Sync;
namespace FieldLogger.Sync.Tests;
/// <summary>
/// In-memory stand-in for the broker. Records published payloads, lets a test toggle
/// connectivity, force publish failures, and inject application-level acks — so the engine's
/// durability/ack/backoff behaviour is exercised without MQTTnet or a live broker.
/// </summary>
public sealed class FakeMqttTransport : IMqttTransport
{
public bool IsConnected { get; set; } = true;
public bool FailNextPublish { get; set; }
public List<(string Topic, byte[] Payload)> Published { get; } = new();
public event Func<AckBatch, Task>? AckReceived;
public Task ConnectAsync(string ackTopic, CancellationToken ct = default)
{
IsConnected = true;
return Task.CompletedTask;
}
public Task DisconnectAsync()
{
IsConnected = false;
return Task.CompletedTask;
}
public Task PublishAsync(string topic, byte[] payload, string schemaVersion, CancellationToken ct = default)
{
if (!IsConnected) throw new InvalidOperationException("not connected");
if (FailNextPublish)
{
FailNextPublish = false;
throw new InvalidOperationException("simulated broker publish failure");
}
Published.Add((topic, payload));
return Task.CompletedTask;
}
/// <summary>Simulate the cloud emitting an application-level ack for these pointIds.</summary>
public async Task InjectAckAsync(params AckItem[] items)
{
if (AckReceived is { } handlers)
{
var batch = new AckBatch { Results = items.ToList() };
foreach (Func<AckBatch, Task> handler in handlers.GetInvocationList())
await handler(batch);
}
}
public Task InjectAcceptAsync(string pointId) =>
InjectAckAsync(new AckItem { PointId = pointId, Outcome = AckOutcome.ACCEPTED });
public Task InjectDuplicateAsync(string pointId) =>
InjectAckAsync(new AckItem { PointId = pointId, Outcome = AckOutcome.DUPLICATE });
public Task InjectRejectAsync(string pointId, string reasonCode) =>
InjectAckAsync(new AckItem { PointId = pointId, Outcome = AckOutcome.REJECTED, ReasonCode = reasonCode });
}