From 864154bc2dc9dd590c9689e41b7ea63fd3dbd425 Mon Sep 17 00:00:00 2001 From: Scott Hanselman Date: Sun, 26 Apr 2026 19:51:15 -0700 Subject: [PATCH] test: add system capability coverage Add coverage for system.run.prepare, exec approval get/set, and system.run environment sanitization from Repo Assist PR #209. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../OpenClaw.Shared.Tests/CapabilityTests.cs | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/tests/OpenClaw.Shared.Tests/CapabilityTests.cs b/tests/OpenClaw.Shared.Tests/CapabilityTests.cs index c9f554e..565061d 100644 --- a/tests/OpenClaw.Shared.Tests/CapabilityTests.cs +++ b/tests/OpenClaw.Shared.Tests/CapabilityTests.cs @@ -306,6 +306,216 @@ public class SystemCapabilityTests Assert.Null(SystemCapability.ResolveExecutable("C:\\Windows\\cmd")); } + [Fact] + public async Task RunPrepare_ReturnsCommandText_ForArray() + { + var cap = new SystemCapability(NullLogger.Instance); + var req = new NodeInvokeRequest + { + Id = "p1", + Command = "system.run.prepare", + Args = Parse("""{"command":["echo","hello world"]}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + var payload = JsonSerializer.Deserialize(JsonSerializer.Serialize(res.Payload)); + Assert.True(payload.TryGetProperty("cmdText", out var cmdText)); + Assert.Contains("echo", cmdText.GetString()); + } + + [Fact] + public async Task RunPrepare_ReturnsCommandText_ForString() + { + var cap = new SystemCapability(NullLogger.Instance); + var req = new NodeInvokeRequest + { + Id = "p2", + Command = "system.run.prepare", + Args = Parse("""{"command":"hostname","rawCommand":"hostname"}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + var payload = JsonSerializer.Deserialize(JsonSerializer.Serialize(res.Payload)); + Assert.True(payload.TryGetProperty("cmdText", out var cmdText)); + Assert.Equal("hostname", cmdText.GetString()); + } + + [Fact] + public async Task RunPrepare_ReturnsPlan_WithArgvAndCwd() + { + var cap = new SystemCapability(NullLogger.Instance); + var req = new NodeInvokeRequest + { + Id = "p3", + Command = "system.run.prepare", + Args = Parse("""{"command":["ls","-la"],"cwd":"/tmp","agentId":"agent1","sessionKey":"sk1"}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + var payload = JsonSerializer.Deserialize(JsonSerializer.Serialize(res.Payload)); + Assert.True(payload.TryGetProperty("plan", out var plan)); + Assert.True(plan.TryGetProperty("argv", out var argv)); + Assert.Equal(2, argv.GetArrayLength()); + Assert.True(plan.TryGetProperty("cwd", out var cwd)); + Assert.Equal("/tmp", cwd.GetString()); + Assert.True(plan.TryGetProperty("agentId", out var agentId)); + Assert.Equal("agent1", agentId.GetString()); + } + + [Fact] + public async Task RunPrepare_ReturnsError_WhenMissingCommand() + { + var cap = new SystemCapability(NullLogger.Instance); + var req = new NodeInvokeRequest + { + Id = "p4", + Command = "system.run.prepare", + Args = Parse("""{}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.False(res.Ok); + Assert.Contains("Missing command", res.Error); + } + + [Fact] + public async Task ExecApprovalsGet_WhenNoPolicyConfigured_ReturnsDisabled() + { + var cap = new SystemCapability(NullLogger.Instance); + var req = new NodeInvokeRequest + { + Id = "ea1", + Command = "system.execApprovals.get", + Args = Parse("""{}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + var payload = JsonSerializer.Deserialize(JsonSerializer.Serialize(res.Payload)); + Assert.True(payload.TryGetProperty("enabled", out var enabled)); + Assert.False(enabled.GetBoolean()); + } + + [Fact] + public async Task ExecApprovalsGet_WhenPolicySet_ReturnsRules() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + try + { + var cap = new SystemCapability(NullLogger.Instance); + var policy = new ExecApprovalPolicy(tempDir, NullLogger.Instance); + cap.SetApprovalPolicy(policy); + + var req = new NodeInvokeRequest + { + Id = "ea2", + Command = "system.execApprovals.get", + Args = Parse("""{}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + var payload = JsonSerializer.Deserialize(JsonSerializer.Serialize(res.Payload)); + Assert.True(payload.TryGetProperty("enabled", out var enabled)); + Assert.True(enabled.GetBoolean()); + Assert.True(payload.TryGetProperty("rules", out _)); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task ExecApprovalsSet_WhenNoPolicyConfigured_ReturnsError() + { + var cap = new SystemCapability(NullLogger.Instance); + var req = new NodeInvokeRequest + { + Id = "ea3", + Command = "system.execApprovals.set", + Args = Parse("""{"rules":[]}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.False(res.Ok); + Assert.Contains("No exec policy configured", res.Error); + } + + [Fact] + public async Task ExecApprovalsSet_UpdatesRules() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + try + { + var cap = new SystemCapability(NullLogger.Instance); + var policy = new ExecApprovalPolicy(tempDir, NullLogger.Instance); + cap.SetApprovalPolicy(policy); + + var req = new NodeInvokeRequest + { + Id = "ea4", + Command = "system.execApprovals.set", + Args = Parse("""{"rules":[{"pattern":"git *","action":"allow","description":"Allow git","enabled":true}],"defaultAction":"deny"}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + var payload = JsonSerializer.Deserialize(JsonSerializer.Serialize(res.Payload)); + Assert.True(payload.TryGetProperty("updated", out var updated)); + Assert.True(updated.GetBoolean()); + Assert.True(payload.TryGetProperty("ruleCount", out var ruleCount)); + Assert.Equal(1, ruleCount.GetInt32()); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task Run_BlockedEnvVar_ReturnsError() + { + var cap = new SystemCapability(NullLogger.Instance); + cap.SetCommandRunner(new FakeCommandRunner()); + + var req = new NodeInvokeRequest + { + Id = "e1", + Command = "system.run", + Args = Parse("""{"command":["echo","test"],"env":{"PATH":"C:\\evil"}}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.False(res.Ok); + Assert.Contains("PATH", res.Error); + } + + [Fact] + public async Task Run_AllowedEnvVar_Passes() + { + var cap = new SystemCapability(NullLogger.Instance); + var runner = new FakeCommandRunner(); + cap.SetCommandRunner(runner); + + var req = new NodeInvokeRequest + { + Id = "e2", + Command = "system.run", + Args = Parse("""{"command":["echo","test"],"env":{"MY_CUSTOM_VAR":"hello"}}""") + }; + + var res = await cap.ExecuteAsync(req); + Assert.True(res.Ok); + Assert.NotNull(runner.LastRequest!.Env); + Assert.True(runner.LastRequest.Env!.ContainsKey("MY_CUSTOM_VAR")); + } + private class FakeCommandRunner : ICommandRunner { public string Name => "fake";