diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs
index 87f953af..0b9d4208 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IBrowsableDataConnection.cs
@@ -10,21 +10,28 @@ public interface IBrowsableDataConnection
{
///
/// Returns the immediate children of , or
- /// the server's root-level nodes when null.
+ /// the server's root-level nodes when null. Pageable: when a prior call
+ /// returned a non-null ,
+ /// pass it back via to fetch the next
+ /// page; a null/empty token starts a fresh browse of .
///
/// Node id whose children to browse, or null for the server root (OPC UA ObjectsFolder).
+ /// Opaque token from a prior to fetch the next page; null/empty starts a fresh browse. The token is implementation-private (e.g. a Base64 OPC UA continuation point) and must not be interpreted by callers.
/// Cancellation token; on cancellation the implementation should throw .
- /// A task that resolves to the child nodes and a flag indicating whether results were truncated.
+ /// A task that resolves to the child nodes, a flag indicating whether more results remain, and a continuation token to fetch the next page (null when exhausted).
Task BrowseChildrenAsync(
string? parentNodeId,
+ string? continuationToken = null,
CancellationToken cancellationToken = default);
}
/// Child nodes returned by the server in browse order.
-/// True when the server reported more children than the per-call cap; remaining children must be discovered via manual entry.
+/// True when more children remain beyond this page; fetch them by passing back to .
+/// Opaque token to fetch the next page (echo it back via the continuationToken parameter), or null when the browse is exhausted. Implementation-private — callers must treat it as opaque.
public record BrowseChildrenResult(
IReadOnlyList Children,
- bool Truncated);
+ bool Truncated,
+ string? ContinuationToken = null);
/// Server-issued node identifier (e.g. "ns=2;s=Devices.Pump1.Speed").
/// Human-readable display name from the server's DisplayName attribute.
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs
index e4cc7665..eda70ce8 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs
@@ -132,15 +132,21 @@ public interface IOpcUaClient : IAsyncDisposable
///
/// Enumerates the immediate children of
- /// (or the server's ObjectsFolder when null). Throws
+ /// (or the server's ObjectsFolder when null). Pageable via the OPC UA
+ /// continuation-point mechanism: when a prior call returned a non-null
+ /// , pass it back via
+ /// to fetch the next page (BrowseNext);
+ /// a null/empty token starts a fresh browse. Throws
/// when the session is not
/// currently up.
///
/// Node id whose children to browse, or null for the server root.
+ /// Opaque token from a prior to fetch the next page via BrowseNext; null/empty starts a fresh browse.
/// A cancellation token that can be used to cancel the operation.
- /// A task that completes with the immediate children of the requested node.
+ /// A task that completes with the immediate children of the requested node and a continuation token for the next page (null when exhausted).
Task BrowseChildrenAsync(
string? parentNodeId,
+ string? continuationToken = null,
CancellationToken cancellationToken = default);
}
@@ -235,8 +241,48 @@ internal class StubOpcUaClient : IOpcUaClient
///
public Task BrowseChildrenAsync(
- string? parentNodeId, CancellationToken cancellationToken = default)
- => throw new NotImplementedException();
+ string? parentNodeId, string? continuationToken = null, CancellationToken cancellationToken = default)
+ {
+ // Canned address-space tree so DCL browse/paging flows can be exercised
+ // without a live OPC UA server (T15):
+ // (root)
+ // ├─ Folder1 (Object, HasChildren=true)
+ // │ ├─ Tag1 (Variable) ← page 1
+ // │ └─ Tag2 (Variable) ← page 2 (continuation token "STUB_PAGE2")
+ // └─ Folder2 (Object, HasChildren=false, leaf)
+ // Folder1 fakes BrowseNext continuation paging so the continuation-token
+ // round-trip is testable; everything else is a single, exhausted page.
+ if (string.IsNullOrEmpty(parentNodeId))
+ {
+ return Task.FromResult(new BrowseChildrenResult(
+ new[]
+ {
+ new BrowseNode("Folder1", "Folder1", BrowseNodeClass.Object, HasChildren: true),
+ new BrowseNode("Folder2", "Folder2", BrowseNodeClass.Object, HasChildren: false),
+ },
+ Truncated: false));
+ }
+
+ if (parentNodeId == "Folder1")
+ {
+ // Page 2: the caller passed back the continuation token from page 1.
+ if (continuationToken == "STUB_PAGE2")
+ {
+ return Task.FromResult(new BrowseChildrenResult(
+ new[] { new BrowseNode("Tag2", "Tag2", BrowseNodeClass.Variable, HasChildren: false) },
+ Truncated: false));
+ }
+
+ // Page 1: return only Tag1 and a continuation token for the next page.
+ return Task.FromResult(new BrowseChildrenResult(
+ new[] { new BrowseNode("Tag1", "Tag1", BrowseNodeClass.Variable, HasChildren: false) },
+ Truncated: true,
+ ContinuationToken: "STUB_PAGE2"));
+ }
+
+ // Leaves (Folder2, Variable nodes, anything unknown) have no children.
+ return Task.FromResult(new BrowseChildrenResult(Array.Empty(), Truncated: false));
+ }
/// Disposes this stub client and marks the connection as closed.
/// A completed .
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs
index 2e6ec9d4..571c74f4 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs
@@ -254,11 +254,17 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
}
///
- public async Task BrowseChildrenAsync(string? parentNodeId, CancellationToken cancellationToken = default)
+ public async Task BrowseChildrenAsync(
+ string? parentNodeId,
+ string? continuationToken = null,
+ CancellationToken cancellationToken = default)
{
if (_status != ConnectionHealth.Connected || _client is null)
throw new ConnectionNotConnectedException($"MxGateway connection is not connected (status: {_status}).");
+ // MxGateway browse is not pageable — the gateway returns a single,
+ // already-bounded child set per node. Any continuation token is ignored
+ // and no continuation is ever surfaced (ContinuationToken stays null).
var (children, truncated) = await _client.BrowseChildrenAsync(parentNodeId, cancellationToken);
var nodes = children
.Select(c => new BrowseNode(c.NodeId, c.DisplayName, c.NodeClass, c.HasChildren))
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs
index b8451e56..cf580725 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs
@@ -298,8 +298,9 @@ public class OpcUaDataConnection : IDataConnection, IBrowsableDataConnection, IA
///
public Task BrowseChildrenAsync(
string? parentNodeId,
+ string? continuationToken = null,
CancellationToken cancellationToken = default)
- => _client!.BrowseChildrenAsync(parentNodeId, cancellationToken);
+ => _client!.BrowseChildrenAsync(parentNodeId, continuationToken, cancellationToken);
///
public async Task WriteBatchAndWaitAsync(
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs
index aa65b2c0..c0a781e1 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs
@@ -701,9 +701,22 @@ public class RealOpcUaClient : IOpcUaClient
? Path.Combine(Path.GetTempPath(), "ScadaBridge", "pki", fallbackLeaf)
: configured;
+ // requestedMaxReferencesPerNode: cap the server's per-call references so a
+ // huge flat folder cannot return an unbounded set. 500 leaves headroom for
+ // the downstream frame-size budget (DataConnectionActor.CapBrowseChildren)
+ // even with long string NodeIds; a non-empty continuation point surfaces as
+ // a ContinuationToken so the caller can page via BrowseNext (T15).
+ private const uint BrowseMaxReferencesPerNode = 500u;
+
+ // NodeClassMask intentionally excludes ReferenceType, View, Variable-
+ // Type, ObjectType, DataType. UI only needs Objects (navigable),
+ // Variables (selectable), Methods (display-only).
+ private const uint BrowseNodeClassMask =
+ (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method);
+
///
public async Task BrowseChildrenAsync(
- string? parentNodeId, CancellationToken cancellationToken = default)
+ string? parentNodeId, string? continuationToken = null, CancellationToken cancellationToken = default)
{
// Mirror the SubscribeAsync/ReadAsync wrap idiom: snapshot the session
// reference once, fail fast with a typed exception if the link is
@@ -723,27 +736,83 @@ public class RealOpcUaClient : IOpcUaClient
? ObjectIds.ObjectsFolder
: NodeId.Parse(parentNodeId);
- // NodeClassMask intentionally excludes ReferenceType, View, Variable-
- // Type, ObjectType, DataType. UI only needs Objects (navigable),
- // Variables (selectable), Methods (display-only).
- var nodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method);
+ // No token → fresh browse of the node. A non-empty token → continue a
+ // prior browse via BrowseNext, falling back to a fresh browse if the
+ // server has invalidated the continuation point.
+ if (string.IsNullOrEmpty(continuationToken))
+ {
+ return await FreshBrowseAsync(session, nodeToBrowse, cancellationToken).ConfigureAwait(false);
+ }
- // requestedMaxReferencesPerNode: cap the server's per-call references so a
- // huge flat folder cannot return an unbounded set. 500 leaves headroom for
- // the downstream frame-size budget (DataConnectionActor.CapBrowseChildren)
- // even with long string NodeIds; a non-empty continuation point surfaces as
- // Truncated, prompting manual entry rather than auto-paging.
+ try
+ {
+ // SDK overload: BrowseNextAsync(session, requestHeader, ByteStringCollection
+ // continuationPoints, bool releaseContinuationPoint, ct) → returns
+ // (ResponseHeader, ByteStringCollection revisedContinuationPoints,
+ // IList results, IList errors).
+ // releaseContinuationPoint:false keeps the point alive so the next page
+ // can be fetched; we pass back exactly the one point we were handed.
+ var (_, revisedPoints, results, _) = await session.BrowseNextAsync(
+ null,
+ new ByteStringCollection { Convert.FromBase64String(continuationToken) },
+ false,
+ cancellationToken).ConfigureAwait(false);
+
+ var nextPoint = revisedPoints is { Count: > 0 } ? revisedPoints[0] : null;
+ var refs = results is { Count: > 0 } ? results[0] : null;
+ return await BuildResultAsync(session, refs, nextPoint, cancellationToken).ConfigureAwait(false);
+ }
+ catch (ServiceResultException ex) when (
+ ex.StatusCode == StatusCodes.BadContinuationPointInvalid ||
+ ex.StatusCode == StatusCodes.BadInvalidArgument)
+ {
+ // The continuation point expired or was rejected (e.g. a fresh
+ // session, or the server timed it out). Recover by re-browsing the
+ // parent from the start and returning its first page.
+ _logger.LogDebug(ex,
+ "OPC UA BrowseNext rejected the continuation point (status {Status:X8}); " +
+ "falling back to a fresh browse of {Node}.", ex.StatusCode, nodeToBrowse);
+ return await FreshBrowseAsync(session, nodeToBrowse, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Issues a fresh of
+ /// and returns its first page, surfacing any
+ /// continuation point as a token for paging.
+ ///
+ private async Task FreshBrowseAsync(
+ ISession session, NodeId nodeToBrowse, CancellationToken cancellationToken)
+ {
var (_, continuationPoint, references) = await session.BrowseAsync(
null,
null,
nodeToBrowse,
- 500u,
+ BrowseMaxReferencesPerNode,
BrowseDirection.Forward,
ReferenceTypeIds.HierarchicalReferences,
true,
- nodeClassMask,
+ BrowseNodeClassMask,
cancellationToken).ConfigureAwait(false);
+ return await BuildResultAsync(session, references, continuationPoint, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Shared "build children + B1 type enrichment + continuation token" logic
+ /// used by both the initial browse and the BrowseNext paths so the
+ /// enrichment is never duplicated (T15). A non-empty
+ /// is surfaced as a Base64
+ /// ContinuationToken with Truncated=true; otherwise the result
+ /// is exhausted (ContinuationToken=null, Truncated=false).
+ ///
+ private async Task BuildResultAsync(
+ ISession session,
+ ReferenceDescriptionCollection? references,
+ byte[]? continuationPoint,
+ CancellationToken cancellationToken)
+ {
var refs = references ?? new ReferenceDescriptionCollection();
var children = new List(refs.Count);
foreach (var r in refs)
@@ -755,22 +824,22 @@ public class RealOpcUaClient : IOpcUaClient
HasChildren: r.NodeClass == NodeClass.Object));
}
- // T16 type-info: enrich Variable rows with DataType / ValueRank /
+ // B1 type-info: enrich Variable rows with DataType / ValueRank /
// Writable so the node picker can show types. Best-effort: ONE batched
// ReadAsync over (DataType, ValueRank, UserAccessLevel) for every
// Variable child; on ANY failure we leave the three fields null and
// return the children exactly as built above. Non-Variable nodes are
- // never read and keep null type info.
+ // never read and keep null type info. Runs identically on the browse
+ // and browse-next pages (T15 shares this path).
children = await EnrichVariableTypeInfoAsync(session, refs, children, cancellationToken)
.ConfigureAwait(false);
- // A non-empty continuation point means the server had more refs than
- // our requestedMaxReferencesPerNode cap. The UI surfaces a "more
- // children, type the node id manually" hint rather than auto-paging;
- // BrowseNext is not invoked here. Discarding the continuation point
- // is acceptable because the server expires it on session close.
- var truncated = continuationPoint != null && continuationPoint.Length > 0;
- return new Commons.Interfaces.Protocol.BrowseChildrenResult(children, truncated);
+ // A non-empty continuation point means the server has more refs than
+ // were returned on this page. Surface it as an opaque Base64 token the
+ // caller passes back to fetch the next page via BrowseNext (T15).
+ var hasMore = continuationPoint != null && continuationPoint.Length > 0;
+ var token = hasMore ? Convert.ToBase64String(continuationPoint!) : null;
+ return new Commons.Interfaces.Protocol.BrowseChildrenResult(children, hasMore, token);
}
private static Commons.Interfaces.Protocol.BrowseNodeClass MapNodeClass(NodeClass nc) => nc switch
diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionManagerBrowseHandlerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionManagerBrowseHandlerTests.cs
index b88ad9b0..34526b2d 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionManagerBrowseHandlerTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Actors/DataConnectionManagerBrowseHandlerTests.cs
@@ -105,7 +105,7 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
new BrowseNode("ns=2;s=B", "B", BrowseNodeClass.Object, HasChildren: true),
};
((IBrowsableDataConnection)adapter)
- .BrowseChildrenAsync(null, Arg.Any())
+ .BrowseChildrenAsync(null, Arg.Any(), Arg.Any())
.Returns(new BrowseChildrenResult(children, Truncated: false));
_factory.Create("OpcUa", Arg.Any>())
@@ -139,7 +139,7 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
((IDataConnection)adapter).Status.Returns(ConnectionHealth.Connected);
((IBrowsableDataConnection)adapter)
- .BrowseChildrenAsync(Arg.Any(), Arg.Any())
+ .BrowseChildrenAsync(Arg.Any(), Arg.Any(), Arg.Any())
.Returns(Task.FromException(
new ConnectionNotConnectedException("OPC UA session is not connected.")));
@@ -183,7 +183,7 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
((IDataConnection)adapter).Status.Returns(ConnectionHealth.Connected);
((IBrowsableDataConnection)adapter)
- .BrowseChildrenAsync(Arg.Any(), Arg.Any())
+ .BrowseChildrenAsync(Arg.Any(), Arg.Any(), Arg.Any())
.Returns(Task.FromException(new NotSupportedException(reason)));
_factory.Create("MxGateway", Arg.Any>())
@@ -226,7 +226,7 @@ public class DataConnectionManagerBrowseHandlerTests : TestKit
.Returns(Task.CompletedTask);
((IDataConnection)adapter).Status.Returns(ConnectionHealth.Connected);
((IBrowsableDataConnection)adapter)
- .BrowseChildrenAsync(null, Arg.Any())
+ .BrowseChildrenAsync(null, Arg.Any(), Arg.Any())
.Returns(new BrowseChildrenResult(bigList, Truncated: false));
_factory.Create("OpcUa", Arg.Any>())
diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/OpcUaDataConnectionBrowseTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/OpcUaDataConnectionBrowseTests.cs
index 0d0e06fd..9f29d947 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/OpcUaDataConnectionBrowseTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/OpcUaDataConnectionBrowseTests.cs
@@ -25,7 +25,7 @@ public class OpcUaDataConnectionBrowseTests
var expected = new BrowseChildrenResult(
new[] { new BrowseNode("ns=2;s=X", "X", BrowseNodeClass.Variable, false) },
Truncated: false);
- client.BrowseChildrenAsync("ns=2;s=Parent", Arg.Any())
+ client.BrowseChildrenAsync("ns=2;s=Parent", Arg.Any(), Arg.Any())
.Returns(expected);
var adapter = new OpcUaDataConnection(factory, NullLogger.Instance);
@@ -36,6 +36,6 @@ public class OpcUaDataConnectionBrowseTests
var actual = await adapter.BrowseChildrenAsync("ns=2;s=Parent");
Assert.Same(expected, actual);
- await client.Received(1).BrowseChildrenAsync("ns=2;s=Parent", Arg.Any());
+ await client.Received(1).BrowseChildrenAsync("ns=2;s=Parent", Arg.Any(), Arg.Any());
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/StubOpcUaClientBrowseTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/StubOpcUaClientBrowseTests.cs
new file mode 100644
index 00000000..986d2bee
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/StubOpcUaClientBrowseTests.cs
@@ -0,0 +1,92 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
+using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
+
+namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
+
+///
+/// M7-B2 (T15): serves a small canned address-space
+/// tree so DCL browse/paging flows can be exercised without a live OPC UA server.
+/// The stub also fakes BrowseNext continuation paging on Folder1 so the
+/// continuation-token round-trip is testable end-to-end against the contract.
+///
+public class StubOpcUaClientBrowseTests
+{
+ // StubOpcUaClient is internal; reached here via InternalsVisibleTo on the
+ // DataConnectionLayer assembly (the test project is already a friend, since
+ // other tests in this project new up internal adapter types).
+ private static async Task ConnectedStubAsync()
+ {
+ var client = new StubOpcUaClient();
+ await client.ConnectAsync("opc.tcp://stub", null, CancellationToken.None);
+ return client;
+ }
+
+ [Fact]
+ public async Task BrowseRoot_ReturnsTwoFolders_NoContinuation()
+ {
+ await using var client = (StubOpcUaClient)await ConnectedStubAsync();
+
+ var result = await client.BrowseChildrenAsync(parentNodeId: null);
+
+ Assert.False(result.Truncated);
+ Assert.Null(result.ContinuationToken);
+ Assert.Collection(
+ result.Children,
+ n =>
+ {
+ Assert.Equal("Folder1", n.DisplayName);
+ Assert.Equal(BrowseNodeClass.Object, n.NodeClass);
+ Assert.True(n.HasChildren);
+ },
+ n =>
+ {
+ Assert.Equal("Folder2", n.DisplayName);
+ Assert.Equal(BrowseNodeClass.Object, n.NodeClass);
+ Assert.False(n.HasChildren);
+ });
+ }
+
+ [Fact]
+ public async Task BrowseFolder1_NoToken_ReturnsTag1_AndContinuationToken()
+ {
+ await using var client = (StubOpcUaClient)await ConnectedStubAsync();
+
+ var result = await client.BrowseChildrenAsync(parentNodeId: "Folder1");
+
+ Assert.True(result.Truncated);
+ Assert.Equal("STUB_PAGE2", result.ContinuationToken);
+ var only = Assert.Single(result.Children);
+ Assert.Equal("Tag1", only.DisplayName);
+ Assert.Equal(BrowseNodeClass.Variable, only.NodeClass);
+ Assert.False(only.HasChildren);
+ }
+
+ [Fact]
+ public async Task BrowseFolder1_WithContinuationToken_ReturnsTag2_AndNoFurtherContinuation()
+ {
+ await using var client = (StubOpcUaClient)await ConnectedStubAsync();
+
+ var result = await client.BrowseChildrenAsync(
+ parentNodeId: "Folder1",
+ continuationToken: "STUB_PAGE2");
+
+ Assert.False(result.Truncated);
+ Assert.Null(result.ContinuationToken);
+ var only = Assert.Single(result.Children);
+ Assert.Equal("Tag2", only.DisplayName);
+ Assert.Equal(BrowseNodeClass.Variable, only.NodeClass);
+ Assert.False(only.HasChildren);
+ }
+
+ [Fact]
+ public async Task BrowseLeaf_ReturnsEmpty_NoContinuation()
+ {
+ await using var client = (StubOpcUaClient)await ConnectedStubAsync();
+
+ var result = await client.BrowseChildrenAsync(parentNodeId: "Folder2");
+
+ Assert.False(result.Truncated);
+ Assert.Null(result.ContinuationToken);
+ Assert.Empty(result.Children);
+ }
+}