diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/PagingRegressionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/PagingRegressionTests.cs
new file mode 100644
index 00000000..642c6b8f
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/PagingRegressionTests.cs
@@ -0,0 +1,118 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.Historian.Conformance;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests;
+
+///
+/// C1 (T7 review I-1): the facts the server's HistoryRead-Raw paging relies on, pinned against the REAL QuestDB data
+/// source (the node-manager walk in NodeManagerHistoryReadPagingTests runs on a fake that models them).
+/// HistoryPaging is internal to OpcUaServer and not visible from this project, so the end-to-end walk below
+/// re-states the node manager's cursor arithmetic (resume inclusive + boundary trim; stalled ⇒ point-read the
+/// cluster and step off by ) over the real reads.
+///
+[Collection(QuestDbCollection.Name)]
+public sealed class PagingRegressionTests(QuestDbFixture fx) : IAsyncLifetime
+{
+ private static readonly CancellationToken Ct = CancellationToken.None;
+ private static readonly DateTime T0 = new(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc); // µs-aligned
+ private QuestDbConformanceBackend _b = null!;
+
+ /// Sample i at T0 + i µs (adjacent microseconds: a one-tick step lands strictly between two of them).
+ private static DateTime S(int i) => T0.AddTicks(10L * i);
+
+ public async ValueTask InitializeAsync()
+ {
+ _b = await QuestDbConformanceBackend.CreateConcreteAsync(fx);
+ await _b.SeedAsync("P", Enumerable.Range(0, 6)
+ .Select(i => (S(i), HistorianValue.FromDouble(i), ConformanceData.Good)).ToArray());
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_b is not null) await _b.DisposeAsync();
+ }
+
+ private async Task RawAsync(IHistorianDataSource ds, DateTime start, DateTime end, uint max) =>
+ (await ds.ReadRawAsync("P", start, end, max, Ct)).Samples.Select(s => s.SourceTimestampUtc).ToArray();
+
+ [Fact]
+ public async Task Forward_raw_reads_truncate_one_tick_but_advance_by_one_resolution_step()
+ {
+ var ds = _b.CreateDataSource();
+ var end = T0.AddHours(1);
+ var res = ds.TimestampResolutionTicks;
+
+ (await RawAsync(ds, S(2).AddTicks(1), end, 1)).ShouldBe([S(2)]); // T + 1 tick ⇒ back to T (the C1 loop)
+ (await RawAsync(ds, S(2).AddTicks(res), end, 1)).ShouldBe([S(3)]); // T + resolution ⇒ the next sample
+ (await RawAsync(ds, S(2), S(2), 10)).ShouldBe([S(2)]); // point read at T ⇒ T
+ }
+
+ [Fact]
+ public async Task Reverse_raw_reads_step_down_by_one_resolution_step()
+ {
+ var ds = _b.CreateDataSource();
+ var end = T0.AddHours(-1);
+ var res = ds.TimestampResolutionTicks;
+
+ (await RawAsync(ds, S(3).AddTicks(-1), end, 1)).ShouldBe([S(2)]); // T − 1 tick truncates DOWN ⇒ previous
+ (await RawAsync(ds, S(3).AddTicks(-res), end, 1)).ShouldBe([S(2)]); // T − resolution ⇒ previous
+ (await RawAsync(ds, S(3), end, 1)).ShouldBe([S(3)]); // reverse window (end, start] includes start
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task One_value_per_page_walk_returns_every_sample_once_and_terminates(bool reverse)
+ {
+ var ds = _b.CreateDataSource();
+ var (start, end) = reverse ? (T0.AddHours(1), T0.AddHours(-1)) : (T0.AddHours(-1), T0.AddHours(1));
+ var collected = await WalkAsync(ds, start, end);
+ var expected = Enumerable.Range(0, 6).Select(i => (DateTime?)S(i));
+ collected.ShouldBe(reverse ? expected.Reverse() : expected);
+ }
+
+ /// The node manager's Raw paging loop at NumValuesPerNode = 1 (see ServeRawPagedAsync).
+ private static async Task> WalkAsync(IHistorianDataSource ds, DateTime start, DateTime end)
+ {
+ const uint cap = 1;
+ var res = ds.TimestampResolutionTicks;
+ var reverse = start > end;
+ var collected = new List();
+ DateTime? cursor = start;
+ var skip = 0;
+ var resumed = false;
+ for (var pages = 0; cursor is { } from; pages++)
+ {
+ pages.ShouldBeLessThan(60, "paging must terminate, not loop");
+ var raw = (await ds.ReadRawAsync("P", from, end, cap, Ct)).Samples;
+ var full = raw.Count >= cap;
+ var page = raw.ToList();
+ if (resumed)
+ for (var dropped = 0; dropped < skip && page.Count > 0 && page[0].SourceTimestampUtc == from; dropped++)
+ page.RemoveAt(0);
+
+ if (resumed && full && (page.Count == 0 || page[^1].SourceTimestampUtc == from))
+ {
+ // Stalled on the boundary "cluster": point-read it, emit what is left, step off by the resolution.
+ var cluster = (await ds.ReadRawAsync("P", from, from, 1000, Ct)).Samples;
+ var sliceStart = Math.Min(skip, cluster.Count);
+ var slice = cluster.Skip(sliceStart).Take((int)cap).ToList();
+ collected.AddRange(slice.Select(s => s.SourceTimestampUtc));
+ if (sliceStart + slice.Count < cluster.Count) { skip = sliceStart + slice.Count; continue; }
+ var next = from.AddTicks(reverse ? -res : res);
+ cursor = reverse ? (next > end ? next : null) : (next <= end ? next : null);
+ skip = 0;
+ continue;
+ }
+
+ collected.AddRange(page.Select(s => s.SourceTimestampUtc));
+ if (!full || page.Count == 0) break;
+ cursor = page[^1].SourceTimestampUtc;
+ skip = page.Count(s => s.SourceTimestampUtc == cursor);
+ resumed = true;
+ }
+ return collected;
+ }
+}