Skip to content

Commit 2440c68

Browse files
improve: add GetStringArrayArg helper and expand test coverage
- Add GetStringArrayArg protected helper to NodeCapabilityBase that extracts a string array from JsonElement args, filtering out null and whitespace-only strings; returns Array.Empty when absent or wrong type. - Simplify SystemCapability.HandleWhich to use GetStringArrayArg instead of 22 lines of manual JSON array parsing with fully-qualified System.Text.Json.JsonValueKind references. - Add 6 GetStringArrayArg tests to NodeCapabilitiesTests covering: present array, missing property, non-array type, whitespace filtering, default JsonElement, and mixed element types. - Add 8 new WindowsNodeClient tests covering: SetPermission (set/ overwrite), DisconnectAsync raises Disconnected status, ProcessMessageAsync edge cases (invalid JSON, no type field, unknown type), GatewayUrl, NodeId null before connection. Test results: 573 passed, 20 skipped (Shared.Tests); 122 passed (Tray.Tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dfec59d commit 2440c68

4 files changed

Lines changed: 275 additions & 20 deletions

File tree

src/OpenClaw.Shared/Capabilities/SystemCapability.cs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,34 +91,19 @@ private Task<NodeInvokeResponse> HandleNotifyAsync(NodeInvokeRequest request)
9191

9292
private NodeInvokeResponse HandleWhich(NodeInvokeRequest request)
9393
{
94-
var bins = new List<string>();
95-
if (request.Args.ValueKind != System.Text.Json.JsonValueKind.Undefined &&
96-
request.Args.TryGetProperty("bins", out var binsEl) &&
97-
binsEl.ValueKind == System.Text.Json.JsonValueKind.Array)
98-
{
99-
foreach (var item in binsEl.EnumerateArray())
100-
{
101-
if (item.ValueKind == System.Text.Json.JsonValueKind.String)
102-
{
103-
var bin = item.GetString()?.Trim();
104-
if (!string.IsNullOrEmpty(bin))
105-
bins.Add(bin);
106-
}
107-
}
108-
}
109-
110-
if (bins.Count == 0)
94+
var bins = GetStringArrayArg(request.Args, "bins");
95+
if (bins.Length == 0)
11196
return Error("Missing bins parameter");
112-
97+
11398
var found = new Dictionary<string, string>();
11499
foreach (var bin in bins)
115100
{
116101
var resolved = ResolveExecutable(bin);
117102
if (resolved != null)
118103
found[bin] = resolved;
119104
}
120-
121-
Logger.Info($"system.which: queried {bins.Count} bins, found {found.Count}");
105+
106+
Logger.Info($"system.which: queried {bins.Length} bins, found {found.Count}");
122107
return Success(new { bins = found });
123108
}
124109

src/OpenClaw.Shared/NodeCapabilities.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,31 @@ protected bool GetBoolArg(JsonElement args, string name, bool defaultValue = fal
138138
}
139139
return defaultValue;
140140
}
141+
142+
/// <summary>
143+
/// Get a string array from a JSON array property. Elements that are not strings or are
144+
/// whitespace-only are excluded. Returns an empty array if the property is absent,
145+
/// not an array, or the args element is missing.
146+
/// </summary>
147+
protected string[] GetStringArrayArg(JsonElement args, string name)
148+
{
149+
if (args.ValueKind == JsonValueKind.Undefined || args.ValueKind == JsonValueKind.Null)
150+
return Array.Empty<string>();
151+
if (!args.TryGetProperty(name, out var prop) || prop.ValueKind != JsonValueKind.Array)
152+
return Array.Empty<string>();
153+
154+
var list = new List<string>();
155+
foreach (var item in prop.EnumerateArray())
156+
{
157+
if (item.ValueKind == JsonValueKind.String)
158+
{
159+
var s = item.GetString();
160+
if (!string.IsNullOrWhiteSpace(s))
161+
list.Add(s);
162+
}
163+
}
164+
return list.Count > 0 ? list.ToArray() : Array.Empty<string>();
165+
}
141166
}
142167

143168
/// <summary>

tests/OpenClaw.Shared.Tests/NodeCapabilitiesTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public override Task<NodeInvokeResponse> ExecuteAsync(NodeInvokeRequest request)
2626
public string? PubGetStringArg(JsonElement args, string name, string? def = null) => GetStringArg(args, name, def);
2727
public int PubGetIntArg(JsonElement args, string name, int def = 0) => GetIntArg(args, name, def);
2828
public bool PubGetBoolArg(JsonElement args, string name, bool def = false) => GetBoolArg(args, name, def);
29+
public string[] PubGetStringArrayArg(JsonElement args, string name) => GetStringArrayArg(args, name);
2930
}
3031

3132
private static JsonElement Parse(string json)
@@ -161,6 +162,57 @@ public void GetStringArg_HandlesDefaultJsonElement()
161162
Assert.Null(cap.PubGetStringArg(args, "url"));
162163
Assert.Equal("def", cap.PubGetStringArg(args, "url", "def"));
163164
}
165+
166+
[Fact]
167+
public void GetStringArrayArg_ReturnsValues_WhenPresent()
168+
{
169+
var cap = new TestCapability();
170+
var args = Parse("""{"bins":["echo","hostname","curl"]}""");
171+
var result = cap.PubGetStringArrayArg(args, "bins");
172+
Assert.Equal(new[] { "echo", "hostname", "curl" }, result);
173+
}
174+
175+
[Fact]
176+
public void GetStringArrayArg_ReturnsEmpty_WhenPropertyMissing()
177+
{
178+
var cap = new TestCapability();
179+
var args = Parse("""{}""");
180+
Assert.Empty(cap.PubGetStringArrayArg(args, "bins"));
181+
}
182+
183+
[Fact]
184+
public void GetStringArrayArg_ReturnsEmpty_WhenNotAnArray()
185+
{
186+
var cap = new TestCapability();
187+
var args = Parse("""{"bins":"echo"}""");
188+
Assert.Empty(cap.PubGetStringArrayArg(args, "bins"));
189+
}
190+
191+
[Fact]
192+
public void GetStringArrayArg_ExcludesWhitespaceOnlyElements()
193+
{
194+
var cap = new TestCapability();
195+
var args = Parse("""{"bins":["echo"," ","hostname",""]}""");
196+
var result = cap.PubGetStringArrayArg(args, "bins");
197+
Assert.Equal(new[] { "echo", "hostname" }, result);
198+
}
199+
200+
[Fact]
201+
public void GetStringArrayArg_ReturnsEmpty_WhenArgsIsDefaultElement()
202+
{
203+
var cap = new TestCapability();
204+
JsonElement args = default;
205+
Assert.Empty(cap.PubGetStringArrayArg(args, "bins"));
206+
}
207+
208+
[Fact]
209+
public void GetStringArrayArg_IgnoresNonStringArrayElements()
210+
{
211+
var cap = new TestCapability();
212+
var args = Parse("""{"bins":["echo",42,true,"hostname",null]}""");
213+
var result = cap.PubGetStringArrayArg(args, "bins");
214+
Assert.Equal(new[] { "echo", "hostname" }, result);
215+
}
164216
}
165217

166218
public class NodeInvokeResponseTests

tests/OpenClaw.Shared.Tests/WindowsNodeClientTests.cs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,4 +438,197 @@ public void FullDeviceId_IsNonEmpty()
438438
Directory.Delete(dataPath, true);
439439
}
440440
}
441+
442+
[Fact]
443+
public void SetPermission_UpdatesRegistrationPermissions()
444+
{
445+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
446+
Directory.CreateDirectory(dataPath);
447+
448+
try
449+
{
450+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
451+
452+
client.SetPermission("camera.capture", true);
453+
client.SetPermission("screen.record", false);
454+
455+
var registrationField = typeof(WindowsNodeClient).GetField(
456+
"_registration",
457+
BindingFlags.NonPublic | BindingFlags.Instance);
458+
var reg = (NodeRegistration)registrationField!.GetValue(client)!;
459+
460+
Assert.True(reg.Permissions.ContainsKey("camera.capture"));
461+
Assert.True(reg.Permissions["camera.capture"]);
462+
Assert.True(reg.Permissions.ContainsKey("screen.record"));
463+
Assert.False(reg.Permissions["screen.record"]);
464+
}
465+
finally
466+
{
467+
if (Directory.Exists(dataPath))
468+
Directory.Delete(dataPath, true);
469+
}
470+
}
471+
472+
[Fact]
473+
public void SetPermission_OverwritesPreviousValue()
474+
{
475+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
476+
Directory.CreateDirectory(dataPath);
477+
478+
try
479+
{
480+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
481+
482+
client.SetPermission("camera.capture", true);
483+
client.SetPermission("camera.capture", false);
484+
485+
var registrationField = typeof(WindowsNodeClient).GetField(
486+
"_registration",
487+
BindingFlags.NonPublic | BindingFlags.Instance);
488+
var reg = (NodeRegistration)registrationField!.GetValue(client)!;
489+
490+
Assert.False(reg.Permissions["camera.capture"]);
491+
}
492+
finally
493+
{
494+
if (Directory.Exists(dataPath))
495+
Directory.Delete(dataPath, true);
496+
}
497+
}
498+
499+
[Fact]
500+
public async Task DisconnectAsync_RaisesDisconnectedStatus()
501+
{
502+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
503+
Directory.CreateDirectory(dataPath);
504+
505+
try
506+
{
507+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
508+
509+
var statusChanges = new List<ConnectionStatus>();
510+
client.StatusChanged += (_, s) => statusChanges.Add(s);
511+
512+
await client.DisconnectAsync();
513+
514+
Assert.Contains(ConnectionStatus.Disconnected, statusChanges);
515+
}
516+
finally
517+
{
518+
if (Directory.Exists(dataPath))
519+
Directory.Delete(dataPath, true);
520+
}
521+
}
522+
523+
[Fact]
524+
public async Task ProcessMessageAsync_InvalidJson_DoesNotThrow()
525+
{
526+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
527+
Directory.CreateDirectory(dataPath);
528+
529+
try
530+
{
531+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
532+
533+
var processMethod = typeof(WindowsNodeClient).GetMethod(
534+
"ProcessMessageAsync",
535+
BindingFlags.NonPublic | BindingFlags.Instance);
536+
Assert.NotNull(processMethod);
537+
538+
var task = (Task)processMethod!.Invoke(client, ["not-valid-json!!"])!;
539+
var ex = await Record.ExceptionAsync(() => task);
540+
Assert.Null(ex);
541+
}
542+
finally
543+
{
544+
if (Directory.Exists(dataPath))
545+
Directory.Delete(dataPath, true);
546+
}
547+
}
548+
549+
[Fact]
550+
public async Task ProcessMessageAsync_NoTypeField_DoesNotThrow()
551+
{
552+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
553+
Directory.CreateDirectory(dataPath);
554+
555+
try
556+
{
557+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
558+
559+
var processMethod = typeof(WindowsNodeClient).GetMethod(
560+
"ProcessMessageAsync",
561+
BindingFlags.NonPublic | BindingFlags.Instance);
562+
563+
var task = (Task)processMethod!.Invoke(client, ["""{"ok":true}"""])!;
564+
var ex = await Record.ExceptionAsync(() => task);
565+
Assert.Null(ex);
566+
}
567+
finally
568+
{
569+
if (Directory.Exists(dataPath))
570+
Directory.Delete(dataPath, true);
571+
}
572+
}
573+
574+
[Fact]
575+
public async Task ProcessMessageAsync_UnknownMessageType_DoesNotThrow()
576+
{
577+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
578+
Directory.CreateDirectory(dataPath);
579+
580+
try
581+
{
582+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
583+
584+
var processMethod = typeof(WindowsNodeClient).GetMethod(
585+
"ProcessMessageAsync",
586+
BindingFlags.NonPublic | BindingFlags.Instance);
587+
588+
var task = (Task)processMethod!.Invoke(client, ["""{"type":"unknown_msg_type"}"""])!;
589+
var ex = await Record.ExceptionAsync(() => task);
590+
Assert.Null(ex);
591+
}
592+
finally
593+
{
594+
if (Directory.Exists(dataPath))
595+
Directory.Delete(dataPath, true);
596+
}
597+
}
598+
599+
[Fact]
600+
public void GatewayUrl_ReturnsDisplayUrl()
601+
{
602+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
603+
Directory.CreateDirectory(dataPath);
604+
605+
try
606+
{
607+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
608+
Assert.Equal("ws://localhost:18789", client.GatewayUrl);
609+
}
610+
finally
611+
{
612+
if (Directory.Exists(dataPath))
613+
Directory.Delete(dataPath, true);
614+
}
615+
}
616+
617+
[Fact]
618+
public void NodeId_IsNullBeforeConnection()
619+
{
620+
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
621+
Directory.CreateDirectory(dataPath);
622+
623+
try
624+
{
625+
using var client = new WindowsNodeClient("ws://localhost:18789", "test-token", dataPath);
626+
Assert.Null(client.NodeId);
627+
}
628+
finally
629+
{
630+
if (Directory.Exists(dataPath))
631+
Directory.Delete(dataPath, true);
632+
}
633+
}
441634
}

0 commit comments

Comments
 (0)