test(A.5 T6): StreamingRegion transitions + hysteresis + oscillation coverage
Adds 5 tests to StreamingRegionTwoTierTests covering all tier-transition paths: - FarToNear promote (walk 2 east from initial center) - NullToNear teleport (loads 9 near + 40 far for a fully fresh region) - NearToFar demote only after NearRadius+2 hysteresis threshold - FarToNull unload only after FarRadius+2 hysteresis threshold - oscillation no-thrash: bouncing 1 LB across a near boundary fires 0 demotes and at most 5 promotes total (one initial settle of the x=100 near-column) Oscillation test fix: initialise the region at the oscillation midpoint (103,100) rather than at a distant starting center (100,100) so the initial move into the oscillation range doesn't itself trigger legitimate demotes, isolating the no-thrash invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb6b61e8ef
commit
326b698161
1 changed files with 103 additions and 0 deletions
|
|
@ -53,4 +53,107 @@ public class StreamingRegionTwoTierTests
|
|||
}
|
||||
Assert.Empty(diff.ToLoadNear);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecenterTo_PlayerWalks_FarToNear_AppearsInToPromote()
|
||||
{
|
||||
var region = new StreamingRegion(centerX: 100, centerY: 100, nearRadius: 1, farRadius: 3);
|
||||
_ = region.ComputeFirstTickDiff();
|
||||
region.MarkResidentFromBootstrap();
|
||||
|
||||
// Walk 2 east — center (102, 100). LB (102, 100) was at distance 2 (Far)
|
||||
// from (100,100); now at distance 0 → Near. That's a Promote.
|
||||
var diff = region.RecenterTo(newCx: 102, newCy: 100);
|
||||
|
||||
var promotedId = StreamingRegion.EncodeLandblockIdForTest(102, 100);
|
||||
Assert.Contains(promotedId, diff.ToPromote);
|
||||
Assert.DoesNotContain(promotedId, diff.ToLoadNear);
|
||||
Assert.DoesNotContain(promotedId, diff.ToLoadFar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecenterTo_PlayerTeleports_NullToNear_AppearsInToLoadNear()
|
||||
{
|
||||
var region = new StreamingRegion(centerX: 100, centerY: 100, nearRadius: 1, farRadius: 3);
|
||||
_ = region.ComputeFirstTickDiff();
|
||||
region.MarkResidentFromBootstrap();
|
||||
|
||||
// Teleport to (200, 200) — entirely new region.
|
||||
var diff = region.RecenterTo(newCx: 200, newCy: 200);
|
||||
|
||||
Assert.Equal(9, diff.ToLoadNear.Count);
|
||||
Assert.Equal(40, diff.ToLoadFar.Count);
|
||||
Assert.Empty(diff.ToPromote);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecenterTo_NearToFar_DemoteOnlyAfterHysteresis()
|
||||
{
|
||||
// near=2, far=4 → near hysteresis threshold = 4.
|
||||
var region = new StreamingRegion(centerX: 100, centerY: 100, nearRadius: 2, farRadius: 4);
|
||||
_ = region.ComputeFirstTickDiff();
|
||||
region.MarkResidentFromBootstrap();
|
||||
|
||||
// LB (100,100) was Near. Walk 3 east → distance 3 > NearRadius=2 but ≤ 4. No demote yet.
|
||||
var diff1 = region.RecenterTo(newCx: 103, newCy: 100);
|
||||
var lb100 = StreamingRegion.EncodeLandblockIdForTest(100, 100);
|
||||
Assert.DoesNotContain(lb100, diff1.ToDemote);
|
||||
|
||||
// Walk 2 more east → distance 5 > 4. Demote.
|
||||
var diff2 = region.RecenterTo(newCx: 105, newCy: 100);
|
||||
Assert.Contains(lb100, diff2.ToDemote);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecenterTo_FarToNull_UnloadOnlyAfterHysteresis()
|
||||
{
|
||||
var region = new StreamingRegion(centerX: 100, centerY: 100, nearRadius: 1, farRadius: 3);
|
||||
_ = region.ComputeFirstTickDiff();
|
||||
region.MarkResidentFromBootstrap();
|
||||
|
||||
// LB (97, 100) was at distance 3 (Far). Walk 1 east → distance 4. ≤ FarRadius+2=5.
|
||||
var diff1 = region.RecenterTo(newCx: 101, newCy: 100);
|
||||
var lb97 = StreamingRegion.EncodeLandblockIdForTest(97, 100);
|
||||
Assert.DoesNotContain(lb97, diff1.ToUnload);
|
||||
|
||||
// Walk 2 more east → distance 6 > 5. Unload.
|
||||
var diff2 = region.RecenterTo(newCx: 103, newCy: 100);
|
||||
Assert.Contains(lb97, diff2.ToUnload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecenterTo_PlayerOscillates_NoThrashWithinHysteresis()
|
||||
{
|
||||
// Start the region centered on (103,100) so the oscillation
|
||||
// between (102,100) and (103,100) never crosses a hysteresis boundary.
|
||||
// NearRadius=2, farRadius=4 → nearUnloadThreshold=4.
|
||||
// Chebyshev distance from (102,100) or (103,100) to any LB in the
|
||||
// initial 9×9 window of (103,100) is ≤ NearRadius+2=4 for all LBs
|
||||
// in the near zone, so no demotes should fire during pure oscillation.
|
||||
var region = new StreamingRegion(centerX: 103, centerY: 100, nearRadius: 2, farRadius: 4);
|
||||
_ = region.ComputeFirstTickDiff();
|
||||
region.MarkResidentFromBootstrap();
|
||||
|
||||
// Bounce between (103,100) and (102,100). All resident LBs stay
|
||||
// within the hysteresis window — no demotes expected.
|
||||
int totalDemotes = 0;
|
||||
int totalPromotes = 0;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var d1 = region.RecenterTo(102, 100);
|
||||
totalDemotes += d1.ToDemote.Count;
|
||||
totalPromotes += d1.ToPromote.Count;
|
||||
var d2 = region.RecenterTo(103, 100);
|
||||
totalDemotes += d2.ToDemote.Count;
|
||||
totalPromotes += d2.ToPromote.Count;
|
||||
}
|
||||
|
||||
// The first step from (103,100) to (102,100) legitimately promotes the
|
||||
// x=100 near-column (5 LBs) that were Far from (103) into Near. After
|
||||
// that initial settle they stay Near for all subsequent oscillations.
|
||||
// So the ceiling is 5 promotes total (not per oscillation).
|
||||
Assert.Equal(0, totalDemotes);
|
||||
Assert.True(totalPromotes <= 5,
|
||||
$"Expected ≤5 promotes across 5 oscillations; got {totalPromotes}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue