fix(A.5 T10-T12): Start() race + null mesh test + real mesh stub

Code review on T10-T12 bundle (commits 0cf86bb/00bb030/0405947 + audit
fix 76e1a64) found 3 Important issues:

1. LandblockStreamer.Start() had an idempotency race — the XML doc
   claimed thread-safety but the implementation checked _worker != null
   before assigning, allowing two callers to both pass the check and
   spawn duplicate worker threads. Fixed via Interlocked.CompareExchange.

2. No test verified the worker emits Failed when buildMeshOrNull returns
   null. Added Load_WhenBuildMeshReturnsNull_ReportsFailed.

3. StreamingControllerTests.cs:81 used MeshData: default! when
   constructing a Loaded result. If a future test flows MeshData
   through the apply callback, the null reference would NRE rather
   than producing a meaningful assertion failure. Replaced with a real
   empty LandblockMeshData instance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-10 07:49:14 +02:00
parent 76e1a64d78
commit 774a7070a8
3 changed files with 52 additions and 6 deletions

View file

@ -66,6 +66,39 @@ public class LandblockStreamerTests
Assert.IsType<LandblockStreamResult.Failed>(result);
}
[Fact]
public async Task Load_WhenBuildMeshReturnsNull_ReportsFailed()
{
// Phase A.5 T10-T12 follow-up: the mesh-build factory may return
// null (e.g., LandBlock dat missing or corrupt). The worker must
// emit Failed in that case instead of constructing Loaded with a
// null MeshData (which would NRE downstream).
var stubLandblock = new LoadedLandblock(
0xABCDFFFEu,
new LandBlock(),
System.Array.Empty<WorldEntity>());
using var streamer = new LandblockStreamer(
loadLandblock: _ => stubLandblock,
buildMeshOrNull: (_, _) => null); // mesh-build returns null
streamer.Start();
streamer.EnqueueLoad(0xABCDFFFEu);
LandblockStreamResult? result = null;
for (int i = 0; i < SpinMaxIterations && result is null; i++)
{
var drained = streamer.DrainCompletions(LandblockStreamer.DefaultDrainBatchSize);
if (drained.Count > 0) result = drained[0];
else await Task.Delay(SpinStepMs);
}
Assert.NotNull(result);
var failed = Assert.IsType<LandblockStreamResult.Failed>(result);
Assert.Equal(0xABCDFFFEu, failed.LandblockId);
Assert.Contains("mesh", failed.Error, System.StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Load_WhenLoaderThrows_ReportsFailedWithMessage()
{