using System.Buffers.Binary; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text; using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using XFEToolBox.Core.Tools; using XFEToolBox.Tools.NetworkDoctor; namespace NetworkDoctor.Validation; internal static class Program { private static readonly string Workspace = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFEToolBox", "CrossVersion", "EditorWorkspaces", "NetworkDoctor"); private static readonly string Artifacts = Path.Combine(AppContext.BaseDirectory, "test-artifacts"); private static readonly List Results = []; private static int exitCode; [STAThread] private static int Main(string[] args) { Directory.CreateDirectory(Artifacts); var app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; app.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri("pack://application:,,,/XFEToolBox.WpfCore;component/Resources/Style/ToolThemeResources.xaml") }); app.Startup += async (_, _) => { try { await RunAsync(args); } catch (Exception e) { exitCode = 1; Console.Error.WriteLine(e); } finally { File.WriteAllText(Path.Combine(Artifacts, "results.json"), JsonSerializer.Serialize(new { passed = exitCode == 0, time = DateTimeOffset.Now, checks = Results }, new JsonSerializerOptions { WriteIndented = true })); app.Shutdown(); } }; app.Run(); return exitCode; } private static void Assert(bool value, string message) { if (!value) throw new InvalidOperationException("FAIL: " + message); Results.Add(message); Console.WriteLine("PASS: " + message); } private static NetworkSnapshot Fixture() => new() { Adapters = [new("7f975253-371a-4fe6-b12f-31baac0ccafa", "测试网卡(仅模拟)", "Test adapter", "Ethernet", true, 8, true, ["169.254.1.2"], ["127.0.0.1"], ["192.0.2.1"], 1000000000, 0)], System = new() { Services = [new("Dhcp", "Stopped", "Automatic"), new("Dnscache", "Stopped", "Disabled"), new("MpsSvc", "Running", "Automatic")], Addresses = [new("192.0.2.5", "Duplicate", 8)], Routes = [new("0.0.0.0/0", "192.0.2.1", 8, 20), new("0.0.0.0/0", "192.0.2.2", 9, 10)], Interfaces = [new(8,"IPv6",1000,10)], Firewall = [new("Public","False","Allow")], Nrpt = ["企业分流"], Events = ["历史 DNS 超时事件"] }, ProxyEnabled = true, ProxyServer = "127.0.0.1:7890", HostsEntries = ["0.0.0.0 www.microsoft.com"], WinsockReadable = true, ActiveProbing = false, TimeWait = 9000 }; private static async Task RunAsync(string[] args) { MakeIcon(); var s = Fixture(); var checks = NetworkScanner.Evaluate(s).ToList(); Assert(checks.Single(c => c.Code == "duplicate-ip").Health == Health.Error, "Windows Duplicate address classified as confirmed error"); Assert(checks.Single(c => c.Code.StartsWith("ip-")).Health == Health.Warning, "APIPA classified as clue, not blanket total connectivity failure"); Assert(checks.Single(c => c.Code == "route").Health == Health.Info, "multiple default routes not declared broken"); Assert(checks.Single(c => c.Code == "user-proxy").Health == Health.Info, "configured proxy not declared broken"); Assert(checks.Single(c => c.Code == "nrpt").Health == Health.Info, "enterprise DNS rules not declared broken"); Assert(checks.Single(c => c.Code == "events").Health == Health.Info, "historical errors not treated as current failures"); Assert(checks.Single(c => c.Code == "hosts").Health == Health.Warning, "target hosts override reported without deleting it"); Assert(checks.Single(c => c.Code == "mtu-config").Health == Health.Warning, "low IPv6 MTU flagged"); Assert(checks.Single(c => c.Code == "firewall").Health == Health.Warning, "disabled firewall reported without disabling security"); Assert(checks.Single(c => c.Code == "tcp-count").Health == Health.Warning, "high TIME_WAIT classified as clue"); Assert(NetworkScanner.Evaluate(new()).Single(c => c.Code == "duplicate-ip").Health == Health.Unknown, "missing IP data not reported healthy"); var v6 = Fixture(); v6.Adapters = [v6.Adapters[0] with { Dhcp = false, Addresses = ["2001:db8::1"], Gateways = ["fe80::1"] }]; Assert(NetworkScanner.Evaluate(v6).Single(c => c.Code.StartsWith("ip-")).Health == Health.Info, "IPv6-only network not treated as DHCP failure"); foreach (string host in new[] { "https://example.com", "example.com/path", "example.com:443", "a;whoami", "*.example.com" }) { bool rejected = false; try { NetworkScanner.ValidateHost(host); } catch { rejected = true; } Assert(rejected, "invalid target rejected: " + host); } Assert(NetworkScanner.ValidateHost("例子.测试").StartsWith("xn--"), "international DNS name converted through IDNA"); Assert(!SnapshotCollector.Redact("http://user:secret@host:8080/proxy?token=secret").Contains("secret"), "proxy credentials and query secrets redacted"); Assert(SnapshotCollector.Redact("") == "", "absent PAC not misreported as configured"); checks.Add(new("dns-system", "DNS", "系统 DNS", Health.Error, "解析失败", "模拟")); var plan = RepairEngine.Plan(s, checks); Assert(plan.Where(p => p.Selected).Select(p => p.Kind).ToHashSet().SetEquals([RepairKind.FlushDns, RepairKind.StartDhcp]), "only evidence-based low-impact repairs recommended"); Assert(plan.Where(p => p.Disruptive || p.RequiresRestart).All(p => !p.Selected), "all disruptive/reset repairs opt-in"); Assert(plan.All(p => p.Kind != RepairKind.StartDns), "disabled services excluded from recommendations"); Assert(RepairEngine.Plan(v6, checks).All(p => p.Kind != RepairKind.RenewDhcp), "static/IPv6-only adapter not offered DHCP renew"); var options = new RepairOption[] { new() { Kind=RepairKind.FlushDns, Title="flush", Description="" }, new() { Kind=RepairKind.ResetWinsock, Title="winsock", Description="" } }; foreach (var option in plan) { var command = RepairEngine.BuildCommand(option, s); Assert(command.Mutates && Path.IsPathFullyQualified(command.Executable), "allowlisted absolute command: " + option.Kind); } await ValidateScriptsAsync(plan, s); var renew = plan.Single(p => p.Kind == RepairKind.RenewDhcp); string script = Encoding.Unicode.GetString(Convert.FromBase64String(RepairEngine.BuildCommand(renew, s).Arguments[^1])); Assert(script.Contains("DHCPEnabled") && script.Contains(s.Adapters[0].Id) && !script.Contains("/release"), "DHCP renew revalidates exact GUID and avoids release-all"); await RejectRepairAsync(options, s, false, true, "repair rejects missing administrator permission"); await RejectRepairAsync(options, s, true, false, "repair rejects missing confirmation"); var stale = Fixture(); stale.CapturedAt = DateTimeOffset.Now.AddHours(-1); await RejectRepairAsync(options, stale, true, true, "stale snapshot rejected before mutation"); await RejectRepairAsync([new() { Kind = RepairKind.RestartAdapter, AdapterId="';evil",Title="bad",Description="" }], s, true, true, "invalid adapter identifier rejected before mutation"); await RejectRepairAsync([new() { Kind=(RepairKind)999,Title="bad",Description="" }],s,true,true,"unknown repair rejected"); var fake = new FakeRunner(); string repairRoot = Path.Combine(Artifacts, "repairs"); fake.OnRun = command => { if (command.Mutates) Assert(Directory.GetFiles(repairRoot, "before.json", SearchOption.AllDirectories).Length > 0, "backup exists before mutation"); return new(0,"simulated success",""); }; var repair = await RepairEngine.ExecuteAsync(options,s,fake,repairRoot,true,true,new InlineProgress(_=>{}),default); Assert(repair.Steps.All(x => x.CommandSucceeded) && repair.Steps[1].RestartRequired && File.Exists(Path.Combine(repair.BackupDirectory,"result.json")), "repair success journal and restart requirement retained"); fake = new FakeRunner { OnRun = _ => new(5,"","simulated access denied") }; var failed = await RepairEngine.ExecuteAsync([options[0]],s,fake,repairRoot,true,true,new InlineProgress(_=>{}),default); Assert(!failed.Steps[0].CommandSucceeded, "nonzero exit code never reported as repaired"); fake = new FakeRunner { OnRun = _ => new(-1,"","timeout",true) }; var timeout = await RepairEngine.ExecuteAsync([options[0]],s,fake,repairRoot,true,true,new InlineProgress(_=>{}),default); Assert(!timeout.Steps[0].CommandSucceeded, "timeout result not treated as success"); using (var cts = new CancellationTokenSource()) { fake = new FakeRunner { OnRun = _ => { cts.Cancel(); return new(0,"first step done",""); } }; var cancelled = await RepairEngine.ExecuteAsync(options,s,fake,repairRoot,true,true,new InlineProgress(_=>{}),cts.Token); Assert(cancelled.Cancelled && fake.Commands.Count == 1 && cancelled.Steps[0].CommandSucceeded, "cancellation finishes current step but skips later steps"); } fake = new FakeRunner { OnRun = command => new(1,"","backup unavailable") }; bool backupRejected=false; try { await RepairEngine.ExecuteAsync([plan.Single(p=>p.Kind==RepairKind.ResetWinHttp)],s,fake,repairRoot,true,true,new InlineProgress(_=>{}),default); } catch(IOException) {backupRejected=true;} Assert(backupRejected && fake.Commands.All(c=>!c.Mutates), "WinHTTP backup failure prevents mutation"); await ProbeTestsAsync(); await CommandTestsAsync(); await ViewTestsAsync(s,checks,plan); if(args.Contains("--live")) await LiveAsync(); await HostAsync(args); Console.WriteLine($"ALL {Results.Count} CHECKS PASSED. Artifacts: {Artifacts}"); } private static async Task RejectRepairAsync(RepairOption[] options,NetworkSnapshot s,bool admin,bool confirmed,string message) { var fake=new FakeRunner(); bool rejected=false; try {await RepairEngine.ExecuteAsync(options,s,fake,Path.Combine(Artifacts,"reject"),admin,confirmed,new InlineProgress(_=>{}),default);} catch {rejected=true;} Assert(rejected && fake.Commands.Count==0,message); } private static async Task ValidateScriptsAsync(List plan, NetworkSnapshot snapshot) { var commands = plan.Select(p => RepairEngine.BuildCommand(p, snapshot)).Append(WindowsCommands.PowerShell(SnapshotCollector.SystemScript)); foreach (var command in commands.Where(c => c.Executable.EndsWith("powershell.exe", StringComparison.OrdinalIgnoreCase))) { string code = "$src=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('" + command.Arguments[^1] + "')); $tokens=$null; $errors=$null; [void][System.Management.Automation.Language.Parser]::ParseInput($src,[ref]$tokens,[ref]$errors); if($errors.Count -gt 0){throw ($errors | Out-String)}; 'Parsed without executing the script'"; var result = await new WindowsCommands().RunAsync(WindowsCommands.PowerShell(code), default); Assert(result.Success, "Windows PowerShell production script syntax: " + result.Text); } } private static async Task ProbeTestsAsync() { byte[] query=NetworkProbes.DnsQuery("example.com",123), reply=Response(query); Assert(NetworkProbes.ValidateDns(query,reply).Answers==1,"valid compressed DNS answer parsed"); foreach(var invalid in new[]{reply[..10],reply[..^1],reply.Select((b,i)=>(byte)(i==0?b^1:b)).ToArray()}) {bool rejected=false;try{NetworkProbes.ValidateDns(query,invalid);}catch(InvalidDataException){rejected=true;}Assert(rejected,"malformed/mismatched DNS packet rejected");} using var udp=new UdpClient(new IPEndPoint(IPAddress.Loopback,0)); int port=((IPEndPoint)udp.Client.LocalEndPoint!).Port; var udpTask=Task.Run(async()=>{var request=await udp.ReceiveAsync();await udp.SendAsync(Response(request.Buffer),request.RemoteEndPoint);}); var dns=await NetworkProbes.DnsServerAsync(IPAddress.Loopback,"example.com",default,port);await udpTask; Assert(dns.Health==Health.Pass,"real UDP DNS exchange on loopback"); var fallbackServer = new TcpListener(IPAddress.Loopback, 0); fallbackServer.Start(); int fallbackPort = ((IPEndPoint)fallbackServer.LocalEndpoint).Port; using var fallbackUdp = new UdpClient(new IPEndPoint(IPAddress.Loopback, fallbackPort)); using var fallbackDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(6)); var fallbackServing = Task.Run(async () => { var request = await fallbackUdp.ReceiveAsync(fallbackDeadline.Token); byte[] tc = request.Buffer.ToArray(); tc[2] = 0x83; tc[3] = 0x80; await fallbackUdp.SendAsync(tc, request.RemoteEndPoint, fallbackDeadline.Token); using var socket = await fallbackServer.AcceptTcpClientAsync(fallbackDeadline.Token); using var stream = socket.GetStream(); byte[] length = new byte[2]; await stream.ReadExactlyAsync(length, fallbackDeadline.Token); byte[] tcpQuery = new byte[BinaryPrimitives.ReadUInt16BigEndian(length)]; await stream.ReadExactlyAsync(tcpQuery, fallbackDeadline.Token); byte[] answer = Response(tcpQuery); BinaryPrimitives.WriteUInt16BigEndian(length, (ushort)answer.Length); await stream.WriteAsync(length, fallbackDeadline.Token); await stream.WriteAsync(answer.AsMemory(0, 7), fallbackDeadline.Token); await Task.Delay(20, fallbackDeadline.Token); await stream.WriteAsync(answer.AsMemory(7), fallbackDeadline.Token); }); var fallbackResult = await NetworkProbes.DnsServerAsync(IPAddress.Loopback, "example.com", fallbackDeadline.Token, fallbackPort); await fallbackServing; fallbackServer.Stop(); Assert(fallbackResult.Health == Health.Pass, "truncated UDP DNS falls back to length-framed TCP with partial reads"); var tcp=new TcpListener(IPAddress.Loopback,0);tcp.Start();int tcpPort=((IPEndPoint)tcp.LocalEndpoint).Port; var accepted=tcp.AcceptTcpClientAsync();var success=await NetworkProbes.TcpAsync("test",IPAddress.Loopback,tcpPort,default);using(var socket=await accepted){} tcp.Stop();Assert(success.Health==Health.Pass,"real TCP open endpoint succeeds"); var refused=await NetworkProbes.TcpAsync("test",IPAddress.Loopback,tcpPort,default);Assert(refused.Health==Health.Warning,"real closed TCP endpoint reported without claiming total outage"); foreach(var item in new[]{(Status:200,Body:"Microsoft Connect Test",Portal:true,Expected:Health.Pass),(Status:302,Body:"login",Portal:true,Expected:Health.Warning),(Status:403,Body:"forbidden",Portal:false,Expected:Health.Pass),(Status:407,Body:"proxy auth",Portal:false,Expected:Health.Warning)}) { var server=new TcpListener(IPAddress.Loopback,0);server.Start();int p=((IPEndPoint)server.LocalEndpoint).Port; var serving=Task.Run(async()=>{using var socket=await server.AcceptTcpClientAsync();using var stream=socket.GetStream();byte[] buffer=new byte[4096];int received=await stream.ReadAsync(buffer);if(received==0)throw new IOException("Empty HTTP fixture request");byte[] body=Encoding.UTF8.GetBytes(item.Body);await stream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 {item.Status} Test\r\nContent-Length: {body.Length}\r\nConnection: close\r\n\r\n"));await stream.WriteAsync(body);}); var check=await NetworkProbes.HttpAsync(new Uri($"http://127.0.0.1:{p}/"),false,item.Portal,default);await serving;server.Stop(); Assert(check.Health==item.Expected,"HTTP/NCSI classification: "+item.Status+" portal="+item.Portal); } using var cancellation=new CancellationTokenSource();cancellation.Cancel();bool cancelled=false; try{await NetworkProbes.TcpAsync("cancel",IPAddress.Loopback,9,cancellation.Token);}catch(OperationCanceledException){cancelled=true;} Assert(cancelled,"probe cancellation propagates rather than false fault result"); } private static byte[] Response(byte[] query) { byte[] reply=new byte[query.Length+16];query.CopyTo(reply,0);reply[2]=0x81;reply[3]=0x80;reply[7]=1; new byte[]{0xc0,12,0,1,0,1,0,0,0,60,0,4,192,0,2,1}.CopyTo(reply,query.Length);return reply; } private static async Task CommandTestsAsync() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var oem = Encoding.GetEncoding(936); const string chinese = "当前 WinHTTP 代理服务器设置:直接访问"; Assert(WindowsCommands.DecodeOutput(Encoding.UTF8.GetBytes(chinese), oem) == chinese, "native UTF-8 Chinese output decoded"); Assert(WindowsCommands.DecodeOutput(oem.GetBytes(chinese), oem) == chinese, "native OEM Chinese output decoded"); Assert(WindowsCommands.DecodeOutput([255,254,..Encoding.Unicode.GetBytes(chinese)], oem) == chinese, "UTF-16 BOM output decoded"); var real = new WindowsCommands(); var unicode = await real.RunAsync(WindowsCommands.PowerShell("[Console]::WriteLine('网络测试'); [Console]::Error.WriteLine('错误流测试')"), default); Assert(unicode.Success && unicode.Output.Contains("网络测试") && unicode.Error.Contains("错误流测试"), "real command captures separate Unicode stdout and stderr"); var nonzero = await real.RunAsync(WindowsCommands.PowerShell("exit 7"), default); Assert(nonzero.ExitCode == 7 && !nonzero.Success, "real nonzero child exit preserved"); var watch = Stopwatch.StartNew(); var timed = await real.RunAsync(WindowsCommands.PowerShell("Start-Sleep -Seconds 10", timeout:1), default); Assert(timed.TimedOut && !timed.Success && watch.Elapsed < TimeSpan.FromSeconds(5), "real helper timeout bounded and own child stopped"); using var cancel = new CancellationTokenSource(TimeSpan.FromMilliseconds(300)); bool cancelled = false; try { await real.RunAsync(WindowsCommands.PowerShell("Start-Sleep -Seconds 10"), cancel.Token); } catch (OperationCanceledException) { cancelled = true; } Assert(cancelled, "real command cancellation propagates"); var big = await real.RunAsync(WindowsCommands.PowerShell("[Console]::Write([string]::new([char]'x',1100000)); [Console]::Error.Write('done')"), default); Assert(big.Success && big.Output.Length < 1050000 && big.Output.Contains("输出已截断") && big.Error == "done", "oversized output bounded while both pipes drain"); var unreadable = await SnapshotCollector.CaptureAsync(new FakeRunner { OnRun = _ => throw new IOException("simulated helper unavailable") }, default); Assert(unreadable.ReadErrors.Length >= 2 && !unreadable.WinsockReadable && unreadable.Adapters.Length > 0, "unavailable command preserves partial read-only snapshot"); } private static async Task ViewTestsAsync(NetworkSnapshot s,List checks,List plan) { var trace=new BindingTrace();PresentationTraceSources.DataBindingSource.Listeners.Add(trace);PresentationTraceSources.DataBindingSource.Switch.Level=SourceLevels.Error; var page=new MainPage();var vm=(MainPageViewModel)page.DataContext; foreach(var check in checks)vm.Checks.Add(check);foreach(var option in plan)vm.Repairs.Add(option);vm.SelectedCheck=checks.Single(c=>c.Code=="duplicate-ip");vm.ProgressValue=1;vm.Phase="测试数据预览";vm.Status="当前展示模拟故障,未修改网络。";vm.Report=MainPageViewModel.BuildReport(s,checks); var window=new Window{Content=page,Width=1180,Height=780,Left=-30000,Top=-30000,WindowStyle=WindowStyle.None,ShowInTaskbar=false};window.Show(); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);SaveView(page,"network-doctor.png"); vm.SelectedTab=1;await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);SaveView(page,"network-doctor-repairs.png"); window.Width=920;window.Height=650;await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);SaveView(page,"network-doctor-minimum.png"); Assert(page.ActualWidth<=920,"minimum layout rendered"); vm.IsRepairing=true;var closing=new System.ComponentModel.CancelEventArgs();vm.OnClosing(closing);Assert(closing.Cancel,"closing blocked during active repair to avoid half-finished adapter restart");vm.IsRepairing=false; vm.SelectedTab=2;await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);Assert(trace.Errors.Count==0,"no WPF binding errors: "+string.Join(" | ",trace.Errors)); window.Close();PresentationTraceSources.DataBindingSource.Listeners.Remove(trace); } private static async Task LiveAsync() { var runner=new ReadOnlyRunner(); var vm=new MainPageViewModel(runner); int ticks=0; var timer=new DispatcherTimer { Interval=TimeSpan.FromMilliseconds(20) }; timer.Tick+=(_,_)=>ticks++; timer.Start(); vm.Checks.CollectionChanged+=(_,e)=>{if(e.NewItems is not null)foreach(Check c in e.NewItems)Console.WriteLine("LIVE: "+c.Title+" => "+c.State+" / "+c.Summary);}; await vm.ScanCommand.ExecuteAsync(null).WaitAsync(TimeSpan.FromSeconds(80)); timer.Stop(); Assert(vm.Checks.Count>15 && vm.ProgressValue==1 && !vm.IsBusy && vm.Report.Contains("配置快照"),"real machine read-only scan returns layered results"); Assert(ticks>10, "UI dispatcher remains responsive during real scan"); Assert(vm.Repairs.Count>0 && vm.Repairs.Where(r=>r.Disruptive).All(r=>!r.Selected), "real scan populates guarded repair choices"); Assert(runner.Commands.All(c=>!c.Mutates),"live validation executed no repair or network configuration changes"); File.WriteAllText(Path.Combine(Artifacts,"live-report.txt"),vm.Report); } private static async Task HostAsync(string[] args) { var assembly=typeof(XFEToolBox.Client.Models.LauncherItem).Assembly;var service=assembly.GetType("XFEToolBox.Client.Utilities.ToolProjectRunService",true)!; var manifest=JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(Workspace,"manifest.json")),new JsonSerializerOptions(JsonSerializerDefaults.Web))!; async Task Build(string root){var task=(Task)service.GetMethod("BuildAsync",BindingFlags.Public|BindingFlags.Static)!.Invoke(null,[root,manifest,CancellationToken.None,null])!;await task.WaitAsync(TimeSpan.FromMinutes(3));var result=task.GetType().GetProperty("Result")!.GetValue(task)!;Assert((bool)result.GetType().GetProperty("Success")!.GetValue(result)!,"production host compilation: "+result.GetType().GetProperty("Message")!.GetValue(result));} await Build(Workspace); if(args.Contains("--check-package")){string package=Path.Combine(Path.GetDirectoryName(Workspace)!,"Packages",$"{manifest.Id}-{manifest.Version}.xfetool"),extracted=Path.Combine(Artifacts,"package-"+Guid.NewGuid().ToString("N"));await (Task)service.GetMethod("ExtractAndValidatePackageAsync",BindingFlags.NonPublic|BindingFlags.Static)!.Invoke(null,[package,extracted,manifest.Id,manifest.Version,CancellationToken.None])!;var files=Directory.GetFiles(extracted,"*",SearchOption.AllDirectories);Assert(files.Length==Directory.GetFiles(Workspace,"*",SearchOption.AllDirectories).Length&&files.All(f=>File.ReadAllBytes(f).SequenceEqual(File.ReadAllBytes(Path.Combine(Workspace,Path.GetRelativePath(extracted,f))))),"package matches tested sources");await Build(extracted);} if(args.Contains("--register"))await (Task)assembly.GetType("XFEToolBox.Client.Utilities.ToolProjectWorkspaceService",true)!.GetMethod("RememberProjectAsync",BindingFlags.Public|BindingFlags.Static)!.Invoke(null,[Workspace])!; } private static void SaveView(FrameworkElement page,string name){page.UpdateLayout();var image=new RenderTargetBitmap((int)page.ActualWidth,(int)page.ActualHeight,96,96,PixelFormats.Pbgra32);image.Render(page);var encoder=new PngBitmapEncoder();encoder.Frames.Add(BitmapFrame.Create(image));using var file=File.Create(Path.Combine(Artifacts,name));encoder.Save(file);} private static void MakeIcon() { var v=new DrawingVisual();using(var d=v.RenderOpen()){ d.DrawRoundedRectangle(new LinearGradientBrush(Color.FromRgb(42,194,235),Color.FromRgb(76,70,197),65),null,new Rect(9,9,106,106),24,24); var pen=new Pen(new SolidColorBrush(Color.FromArgb(235,245,252,255)),4);d.DrawEllipse(null,pen,new Point(62,60),36,36);d.DrawEllipse(null,pen,new Point(62,60),17,36);d.DrawLine(pen,new Point(26,60),new Point(98,60));d.DrawLine(pen,new Point(32,43),new Point(92,43));d.DrawLine(pen,new Point(32,77),new Point(92,77)); d.DrawEllipse(new LinearGradientBrush(Color.FromRgb(129,237,165),Color.FromRgb(21,160,132),90),new Pen(Brushes.White,3),new Point(97,98),23,23); var check=new Pen(Brushes.White,6){StartLineCap=PenLineCap.Round,EndLineCap=PenLineCap.Round};d.DrawLine(check,new Point(85,99),new Point(94,107));d.DrawLine(check,new Point(94,107),new Point(109,90));} var bitmap=new RenderTargetBitmap(128,128,96,96,PixelFormats.Pbgra32);bitmap.Render(v);var encoder=new PngBitmapEncoder();encoder.Frames.Add(BitmapFrame.Create(bitmap));Directory.CreateDirectory(Path.Combine(Workspace,"Assets"));using var file=File.Create(Path.Combine(Workspace,"Assets","icon.png"));encoder.Save(file); } private sealed class FakeRunner:ICommandRunner{public List Commands{get;}=[];public Func OnRun{get;set;}=_=>new(0,"ok","");public Task RunAsync(CommandSpec c,CancellationToken t){Commands.Add(c);return Task.FromResult(OnRun(c));}} private sealed class ReadOnlyRunner:ICommandRunner{private readonly WindowsCommands real=new();public List Commands{get;}=[];public Task RunAsync(CommandSpec c,CancellationToken t){if(c.Mutates)throw new InvalidOperationException("Live tests forbid mutations");Commands.Add(c);return real.RunAsync(c,t);}} private sealed class InlineProgress(Action action):IProgress{public void Report(T value)=>action(value);} private sealed class BindingTrace:TraceListener{public List Errors{get;}=[];public override void Write(string? text){if(!string.IsNullOrEmpty(text))Errors.Add(text);}public override void WriteLine(string? text)=>Write(text);} }