using w4c_workflows.Services; using Xunit; namespace w4c_workflows.Tests; /// /// P1-9: two overlapping syncs for the same (tenant, repo) must not run /// concurrently, or they insert the same deterministic workflow/task ids and /// collide on the primary key. The gate serializes that pair only — syncs for a /// different repo/tenant stay parallel. /// public class SyncGateTests { [Fact] public async Task Same_tenant_and_repo_waits_for_the_holder() { var gate = new SyncGate(); var held = await gate.AcquireAsync("tenant-a", "repo", default); var waiter = gate.AcquireAsync("tenant-a", "repo", default); Assert.False(waiter.IsCompleted); held.Dispose(); using var acquired = await waiter.WaitAsync(TimeSpan.FromSeconds(2)); Assert.True(waiter.IsCompletedSuccessfully); } [Fact] public async Task Different_keys_do_not_block_each_other() { var gate = new SyncGate(); using var first = await gate.AcquireAsync("tenant-a", "repo", default); // Another repo for the same tenant (and another tenant for the same repo) // must not wait behind the held gate. using var otherRepo = await gate.AcquireAsync("tenant-a", "other-repo", default).WaitAsync(TimeSpan.FromSeconds(2)); using var otherTenant = await gate.AcquireAsync("tenant-b", "repo", default).WaitAsync(TimeSpan.FromSeconds(2)); Assert.NotNull(otherRepo); Assert.NotNull(otherTenant); } [Fact] public async Task Dispose_is_idempotent_and_releases_once() { var gate = new SyncGate(); var held = await gate.AcquireAsync("tenant-a", "repo", default); held.Dispose(); held.Dispose(); // A second acquisition after the (double) release must still succeed. using var next = await gate.AcquireAsync("tenant-a", "repo", default).WaitAsync(TimeSpan.FromSeconds(2)); Assert.NotNull(next); } }