using w4c_workflows.Services; using w4c_workflows.Services.Messaging; namespace w4c_workflows.Tests; /// In-memory capturing enqueued task.run fields. internal sealed class FakeJobQueue : IJobQueue { public List<(string TenantId, IReadOnlyDictionary Fields)> Enqueued { get; } = new(); public List<(string TenantId, IReadOnlyDictionary Fields, string Reason)> DeadLettered { get; } = new(); public Task EnsureGroupAsync(string tenantId, CancellationToken ct) => Task.CompletedTask; public Task EnqueueAsync(string tenantId, IReadOnlyDictionary fields, CancellationToken ct) { Enqueued.Add((tenantId, fields)); return Task.FromResult(Enqueued.Count.ToString()); } public Task> ReadGroupAsync(string tenantId, string consumer, int count, CancellationToken ct) => Task.FromResult>(Array.Empty()); public Task> ClaimPendingAsync(string tenantId, string consumer, TimeSpan minIdle, int count, CancellationToken ct) => Task.FromResult>(Array.Empty()); public Task AckAsync(string tenantId, string messageId, CancellationToken ct) => Task.FromResult(0L); public Task DeadLetterAsync(string tenantId, string messageId, IReadOnlyDictionary fields, string reason, CancellationToken ct) { DeadLettered.Add((tenantId, fields, reason)); return Task.CompletedTask; } public Task PublishDeadLetterAsync(string tenantId, IReadOnlyDictionary fields, string reason, CancellationToken ct) { DeadLettered.Add((tenantId, fields, reason)); return Task.CompletedTask; } public List> EnqueuedFor(string tenantId) => Enqueued.Where(x => x.TenantId == tenantId).Select(x => x.Fields).ToList(); public List<(IReadOnlyDictionary Fields, string Reason)> DeadLetteredFor(string tenantId) => DeadLettered.Where(x => x.TenantId == tenantId).Select(x => (x.Fields, x.Reason)).ToList(); } /// /// In-memory . Stream names resolve the {tenant} /// placeholder exactly like the real Redis transport; tests seed result messages /// with and the engine reads + acks them. /// internal sealed class FakeEventBus : IEventBus { private readonly Dictionary> _messages = new(); private int _nextId = 1; public List Acked { get; } = new(); public List<(string TenantId, string Stream, IReadOnlyDictionary Fields)> Published { get; } = new(); public void SeedResult(string tenantId, IReadOnlyDictionary fields) => Seed(Streams.Results.Replace("{tenant}", tenantId), fields); public void Seed(string streamKey, IReadOnlyDictionary fields) { if (!_messages.TryGetValue(streamKey, out var queue)) { queue = new Queue(); _messages[streamKey] = queue; } queue.Enqueue(new StreamMessage((_nextId++).ToString(), fields)); } private static string Resolve(string stream, string tenantId) => stream.Replace("{tenant}", tenantId); public Task PublishAsync(string tenantId, string stream, IReadOnlyDictionary fields, CancellationToken ct) { Published.Add((tenantId, stream, fields)); return Task.FromResult((_nextId++).ToString()); } public Task EnsureGroupAsync(string tenantId, string stream, CancellationToken ct) => Task.CompletedTask; public Task> ReadGroupAsync(string tenantId, string stream, string consumer, int count, CancellationToken ct) { var key = Resolve(stream, tenantId); var list = new List(); if (_messages.TryGetValue(key, out var queue)) while (list.Count < count && queue.Count > 0) list.Add(queue.Dequeue()); return Task.FromResult>(list); } public Task> ClaimPendingAsync(string tenantId, string stream, string consumer, TimeSpan minIdle, int count, CancellationToken ct) => Task.FromResult>(Array.Empty()); public Task AckAsync(string tenantId, string stream, string messageId, CancellationToken ct) { Acked.Add(messageId); return Task.FromResult(1L); } } /// In-memory keyed by tenant + run id. internal sealed class FakeLeaseService : ILeaseService { private readonly Dictionary _held = new(); public Task AcquireAsync(string tenantId, string runId, string owner, TimeSpan ttl) { var key = $"{tenantId}:{runId}"; if (_held.ContainsKey(key)) return Task.FromResult(false); _held[key] = owner; return Task.FromResult(true); } public Task RenewAsync(string tenantId, string runId, string owner, TimeSpan ttl) => Task.FromResult(_held.TryGetValue($"{tenantId}:{runId}", out var held) && held == owner); public Task ReleaseAsync(string tenantId, string runId, string owner) { var key = $"{tenantId}:{runId}"; if (_held.TryGetValue(key, out var held) && held == owner) { _held.Remove(key); return Task.FromResult(true); } return Task.FromResult(false); } }