feat(historian): page within oversized tie clusters (#400) instead of loud-failing
This commit is contained in:
@@ -146,6 +146,112 @@ public sealed class HistoryPagingTests
|
||||
trimmed[0].Value.ShouldBe(11.0);
|
||||
}
|
||||
|
||||
// --- SliceTieCluster ------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void SliceTieCluster_mid_cluster_emits_cp_at_same_timestamp()
|
||||
{
|
||||
// 10 ties at T; already emitted 2; cap 3. Slice [2,5) → next page resumes AT T, skip 5.
|
||||
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
|
||||
var end = t.AddHours(1);
|
||||
|
||||
HistoryPaging.SliceTieCluster(
|
||||
clusterCount: 10, skip: 2, cap: 3, boundaryT: t, endUtc: end,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip);
|
||||
|
||||
sliceStart.ShouldBe(2);
|
||||
sliceCount.ShouldBe(3);
|
||||
nextStartUtc.ShouldBe(t); // still draining the cluster ⇒ resume AT T
|
||||
nextSkip.ShouldBe(5); // 2 already emitted + 3 just emitted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SliceTieCluster_exact_drain_advances_one_tick_when_window_remains()
|
||||
{
|
||||
// 6 ties at T; skip 3; cap 3. Slice [3,6) drains the cluster exactly ⇒ advance to T+1tick.
|
||||
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
|
||||
var end = t.AddHours(1);
|
||||
|
||||
HistoryPaging.SliceTieCluster(
|
||||
clusterCount: 6, skip: 3, cap: 3, boundaryT: t, endUtc: end,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip);
|
||||
|
||||
sliceStart.ShouldBe(3);
|
||||
sliceCount.ShouldBe(3);
|
||||
nextStartUtc.ShouldBe(t.AddTicks(1)); // cluster drained, window remains ⇒ next tick, fresh skip
|
||||
nextSkip.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SliceTieCluster_short_final_slice_still_emits_cp_when_window_remains()
|
||||
{
|
||||
// 5 ties at T; skip 3; cap 10. Slice [3,5) is SHORT (2 < cap) but it fully drains the cluster
|
||||
// and the window still extends past T ⇒ we MUST emit a CP to read the rest of the window even
|
||||
// though this page is short of the cap.
|
||||
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
|
||||
var end = t.AddHours(1);
|
||||
|
||||
HistoryPaging.SliceTieCluster(
|
||||
clusterCount: 5, skip: 3, cap: 10, boundaryT: t, endUtc: end,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip);
|
||||
|
||||
sliceStart.ShouldBe(3);
|
||||
sliceCount.ShouldBe(2); // short slice
|
||||
nextStartUtc.ShouldBe(t.AddTicks(1)); // but CP emitted so the rest of the window is read
|
||||
nextSkip.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SliceTieCluster_drained_at_window_end_terminates()
|
||||
{
|
||||
// The cluster ends exactly AT the window end (endUtc == T). Draining it cannot advance past the
|
||||
// window ⇒ terminal (no CP).
|
||||
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
|
||||
|
||||
HistoryPaging.SliceTieCluster(
|
||||
clusterCount: 4, skip: 0, cap: 10, boundaryT: t, endUtc: t,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip);
|
||||
|
||||
sliceStart.ShouldBe(0);
|
||||
sliceCount.ShouldBe(4);
|
||||
nextStartUtc.ShouldBeNull(); // T+1tick > endUtc ⇒ window exhausted ⇒ terminal
|
||||
nextSkip.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SliceTieCluster_self_heals_when_skip_exceeds_cluster()
|
||||
{
|
||||
// Defensive: a stale skip points past the (re-read, possibly shrunk) cluster. The slice is empty
|
||||
// (count 0) and, since emitted == clusterCount, the cursor advances/terminates rather than looping.
|
||||
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
|
||||
var end = t.AddHours(1);
|
||||
|
||||
HistoryPaging.SliceTieCluster(
|
||||
clusterCount: 4, skip: 4, cap: 10, boundaryT: t, endUtc: end,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip);
|
||||
|
||||
sliceStart.ShouldBe(4);
|
||||
sliceCount.ShouldBe(0); // nothing left in the cluster to emit
|
||||
nextStartUtc.ShouldBe(t.AddTicks(1)); // emitted (4) == clusterCount (4) ⇒ advance, don't loop
|
||||
nextSkip.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SliceTieCluster_self_heals_to_terminal_at_window_end()
|
||||
{
|
||||
// Same stale-skip self-heal, but the window ends at T ⇒ advancing terminates instead of looping.
|
||||
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
|
||||
|
||||
HistoryPaging.SliceTieCluster(
|
||||
clusterCount: 4, skip: 9, cap: 10, boundaryT: t, endUtc: t,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip);
|
||||
|
||||
sliceStart.ShouldBe(4); // clamped to clusterCount
|
||||
sliceCount.ShouldBe(0);
|
||||
nextStartUtc.ShouldBeNull();
|
||||
nextSkip.ShouldBe(0);
|
||||
}
|
||||
|
||||
// --- InMemoryHistoryContinuationStore (mirrors the production session store contract) --------
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user