test(localdb): convergence polish — provable LWW race arbitration, collection serialization, dead-letter diagnostics

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
Joseph Doherty
2026-07-18 01:06:00 -04:00
parent c5352f3ed8
commit b2345c32d4
3 changed files with 69 additions and 8 deletions
@@ -15,6 +15,15 @@ using ZB.MOM.WW.LocalDb.Replication.Internal;
namespace ZB.MOM.WW.LocalDb.Tests.Convergence;
/// <summary>
/// Serializes the convergence test classes against EACH OTHER (no shared class fixture — each test
/// still builds its own <see cref="ConvergenceFixture"/>): both classes stand up real Kestrel
/// listeners plus four SQLite files per test, and running the two heavy classes concurrently under
/// CI contention is a flakiness risk. The rest of the assembly still parallelizes normally.
/// </summary>
[CollectionDefinition("Convergence")]
public sealed class ConvergenceCollection;
/// <summary>
/// Two FULL replication stacks over a REAL loopback gRPC transport (Kestrel h2c on 127.0.0.1),
/// proving end-to-end bidirectional convergence. Node A is the initiator (client:
@@ -183,9 +192,25 @@ public sealed class ConvergenceFixture : IAsyncDisposable
sb.AppendLine("--- B.orders ---").AppendLine(await DumpOrdersAsync(_dbB));
sb.AppendLine("--- A.row_version ---").AppendLine(await DumpRowVersionAsync(_dbA));
sb.AppendLine("--- B.row_version ---").AppendLine(await DumpRowVersionAsync(_dbB));
sb.AppendLine("--- A.dead_letter ---").AppendLine(await DumpDeadLettersAsync(_dbA));
sb.AppendLine("--- B.dead_letter ---").AppendLine(await DumpDeadLettersAsync(_dbB));
return sb.ToString();
}
// Dead-lettered entries are a prime suspect when convergence stalls (a poison row silently
// diverts instead of applying), so the timeout dump surfaces the count + a small sample.
private static async Task<string> DumpDeadLettersAsync(ILocalDb db)
{
var count = (await db.QueryAsync(
"SELECT COUNT(*) FROM __localdb_dead_letter", static r => r.GetInt64(0)))[0];
if (count == 0)
return "count: 0";
var sample = await db.QueryAsync(
"SELECT table_name, pk_json, hlc, node_id, error FROM __localdb_dead_letter ORDER BY id LIMIT 5",
static r => $"{r.GetString(0)}|{r.GetString(1)}|{r.GetInt64(2)}|{r.GetString(3)}|{r.GetString(4)}");
return $"count: {count} (first {sample.Count})\n" + string.Join("\n", sample);
}
// ---- test helpers -----------------------------------------------------------------------
/// <summary>