using WxAgent.Core; using Xunit; namespace WxAgent.Core.Tests; public sealed class WechatStabilityTests { [Fact] public void Deduper_HandlesTenThousandMessagesAndEvictsOldEntries() { var deduper = new BoundedMessageDeduper(2048); for (var index = 0; index < 10_000; index++) Assert.True(deduper.TryAdd($"message-{index}")); Assert.False(deduper.TryAdd("message-9999")); Assert.True(deduper.TryAdd("message-0")); } [Theory] [InlineData("Qt51514QWindowIcon", "微信", true)] [InlineData("Qt6QWindowIcon", "微信", true)] [InlineData("Qt51514QWindowIcon", "文件传输助手", false)] [InlineData("Qt51514WxTrayIconMessageWindowClass", "微信", false)] [InlineData("Notepad", "微信", false)] public void NativeWindowMatching_RejectsSubwindowsAndOtherApplications(string className, string title, bool expected) { Assert.Equal(expected, WechatLocators.IsNativeMainWindow(className, title)); } [Theory] [InlineData("微信", true)] [InlineData("微信\n收到新消息", true)] [InlineData("WeChat", true)] [InlineData("企业微信", false)] [InlineData("微信测试脚本", false)] public void TrayMatching_IsExplicit(string name, bool expected) => Assert.Equal(expected, WechatLocators.IsWechatTrayName(name)); [Fact] public void StabilityAnalyzer_DetectsAgentLeakAndFinalError() { var now = DateTimeOffset.UtcNow; var first = Sample(now, 100, 10, 1); var last = first with { Timestamp = now.AddHours(1), AgentHandleCount = 101, Errors = [WxAgentErrorCode.SessionLocked] }; var report = WechatStabilityAnalyzer.Analyze(now, last.Timestamp, [first, last], 0, 0); Assert.False(report.Healthy); Assert.Contains("agent-handle-growth-over-100", report.Findings); Assert.Contains("final-error:SessionLocked", report.Findings); } [Fact] public void Deduper_RejectsInvalidCapacity() { Assert.Throws(() => new BoundedMessageDeduper(0)); } [Fact] public void StabilityAnalyzer_ReportsHealthyBoundedGrowth() { var started = DateTimeOffset.UtcNow; var samples = new[] { Sample(started, 100_000_000, 100, 20), Sample(started.AddMinutes(1), 110_000_000, 120, 22) }; var report = WechatStabilityAnalyzer.Analyze(started, started.AddMinutes(1), samples, 5, 1); Assert.True(report.Healthy); Assert.Equal(10_000_000, report.WorkingSetGrowthBytes); Assert.Empty(report.Findings); } [Fact] public void StabilityAnalyzer_AllowsTransientWindowLossAfterReconnect() { var started = DateTimeOffset.UtcNow; var samples = new[] { Sample(started, 100_000_000, 100, 20), Sample(started.AddMinutes(1), 0, 0, 0, windowFound: false), Sample(started.AddMinutes(2), 105_000_000, 110, 21) }; var report = WechatStabilityAnalyzer.Analyze(started, started.AddMinutes(2), samples, 0, 1); Assert.True(report.Healthy); Assert.DoesNotContain("wechat-window-unavailable", report.Findings); } [Fact] public void StabilityAnalyzer_FlagsWindowLossAndResourceGrowth() { var started = DateTimeOffset.UtcNow; var samples = new[] { Sample(started, 100_000_000, 100, 20), Sample(started.AddHours(1), 400_000_000, 700, 150, windowFound: false) }; var report = WechatStabilityAnalyzer.Analyze(started, started.AddHours(1), samples, 0, 0); Assert.False(report.Healthy); Assert.Contains("wechat-window-unavailable", report.Findings); Assert.Contains("working-set-growth-over-256mb", report.Findings); Assert.Contains("handle-growth-over-500", report.Findings); Assert.Contains("thread-growth-over-100", report.Findings); } private static WechatStabilitySample Sample( DateTimeOffset timestamp, long memory, int handles, int threads, bool windowFound = true) => new(timestamp, 1, memory, handles, threads, windowFound, [], 0, 0); }