using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;
using XFEEDToolBox.Controls;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Views;
namespace XFEEDToolBox;
public partial class App
{
private static async Task<object> RunMarketCacheSettingsProbeAsync(string directory)
{
var exercise=Environment.GetCommandLineArgs().Contains("--exercise-fixture-cache-clear");
var fixturePath=Path.GetFullPath(Path.Combine(directory,"isolated-data"));
if(exercise&&(!ViewModel.OfflinePreview||!Path.GetFullPath(Runtime.DataDirectory).Equals(fixturePath,StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException("Cache cleanup acceptance may mutate only its explicit offline report-directory fixture.");
await File.WriteAllTextAsync(Path.Combine(directory,"requirements.json"),JsonSerializer.Serialize(new{requirements=new[]{
new{id="identity",kind="behavior",description="清楚区分隔离清理夹具与真实数据目录,只在显式隔离夹具执行删除。"},
new{id="scope",kind="text",description="显示真实占用、默认500ly与可选中心的规则、旧存量与个人数据保留说明。"},
new{id="clear",kind="behavior",description="实际按钮清理夹具行情及旧备份,并显示结果;生产只读取图不据此声称大库已清理。"},
new{id="guard",kind="behavior",description="运行状态禁用清理,恢复可用后仅清除行情数据,设置、事件与交易记录字节保持。"},
new{id="layout",kind="layout",description="宽窗和窄窗中说明、占用、按钮及状态无重叠与不可访问裁切。"}
}},new JsonSerializerOptions{WriteIndented=true}));
var checks=new List<object>();var captures=new List<object>();var protectedFiles=new Dictionary<string,string>();
void Check(string item,bool passed,string evidence)
{checks.Add(new{item,passed,evidence});if(!passed)throw new InvalidOperationException(item+": "+evidence);}
static string Hash(string path)
{
using var bytes=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
return Convert.ToHexStringLower(SHA256.HashData(bytes));
}
var legacy=Path.Combine(Runtime.DataDirectory,"markets.db.verified.db");
const long fixtureStation=7000000000001;
if(exercise)
{
Runtime.Settings.Save(Runtime.SettingsPath);
var history=Path.Combine(Runtime.DataDirectory,"trade-ledgers");Directory.CreateDirectory(history);
var sentinel=Path.Combine(history,"cache-probe-preserve.json");await File.WriteAllTextAsync(sentinel,"{\"fixture\":true}");
foreach(var file in new[]{Runtime.SettingsPath,Path.Combine(Runtime.DataDirectory,"game-events.db"),sentinel})
if(File.Exists(file))protectedFiles[file]=Hash(file);
var now=DateTimeOffset.UtcNow;
var station=new Station{MarketId=fixtureStation,Name="Cache fixture",SystemName="Isolated fixture",Position=new(0,0,0),UpdatedAt=now};
var seeded=await Runtime.Markets.SaveSnapshotAsync(new(station,[new CommodityQuote{MarketId=fixtureStation,Commodity="fixture",DisplayName="Cache fixture",BuyPrice=1,SellPrice=2,CollectedAt=now,Source=DataSource.Journal}],now,DataSource.Journal));
Check("the explicit local-market fixture was seeded",seeded&&Runtime.Markets.GetStation(fixtureStation) is not null,"This is synthetic offline cache data using the local-observation bypass, never a live Journal or game transaction.");
await File.WriteAllBytesAsync(legacy,new byte[1024*1024]);
}
AppShellPage.Current!.Navigate(typeof(SettingPage));await Task.Delay(180);
var page=(SettingPage)AppShellPage.Current.CurrentPage!;
var data=(CommunityToolkit.WinUI.Controls.SettingsExpander)page.FindName("DataSettings");data.IsExpanded=true;
await ViewModel.RefreshMarketCacheUsageCommand.ExecuteAsync(null);await Task.Delay(140);page.UpdateLayout();
var card=(FrameworkElement)page.FindName("MarketCacheCard");
var scroll=(ScrollViewer)page.FindName("SettingsScrollViewer");
var button=Descendants(page).OfType<Button>().Single(b=>ReferenceEquals(b.Command,ViewModel.ClearMarketCacheCommand)||ReferenceEquals(AsyncAction.GetCommand(b),ViewModel.ClearMarketCacheCommand));
Check("cache card and exact cleanup command are present",card.ActualWidth>100&&button.IsLoaded,"The rendered Settings card is bound to the production cleanup command.");
var beforeText=ViewModel.MarketCacheUsageText;
Check("actual usage is displayed",!string.IsNullOrWhiteSpace(beforeText),beforeText);
async Task Capture(string name)
{
page.UpdateLayout();var point=card.TransformToVisual(scroll).TransformPoint(new(0,0));
scroll.ChangeView(null,Math.Max(0,scroll.VerticalOffset+point.Y-16),null,true);await Task.Delay(140);page.UpdateLayout();
var path=Path.Combine(directory,name+".png");await CaptureRefinementElementAsync((FrameworkElement)MainWindow.Content,path);
var bounds=card.TransformToVisual(scroll).TransformPoint(new(0,0));
captures.Add(new{name,path,sha256=Hash(path),window=new{MainWindow.AppWindow.Size.Width,MainWindow.AppWindow.Size.Height},card=new{bounds.X,bounds.Y,card.ActualWidth,card.ActualHeight},scroll=new{scroll.ActualWidth,scroll.ActualHeight,scroll.VerticalOffset}});
}
await Capture("cache-settings-wide");
MainWindow.AppWindow.Resize(new(1280,1040));await Task.Delay(180);await Capture("cache-settings-compact");
if(exercise)
{
var previous=ViewModel.RunActive;
try
{
ViewModel.RunActive=true;await Task.Delay(100);
Check("cleanup is blocked by the running UI state",!ViewModel.ClearMarketCacheCommand.CanExecute(null)&&!button.IsEnabled,"This is an isolated UI-state fixture; no trade engine or game action was started.");
await Capture("cache-settings-blocked");
}
finally{ViewModel.RunActive=previous;}
await Task.Delay(100);Check("cleanup is available after the fixture stops",button.IsEnabled&&ViewModel.ClearMarketCacheCommand.CanExecute(null),ViewModel.MarketCacheBlockReason);
((IInvokeProvider)new ButtonAutomationPeer(button).GetPattern(PatternInterface.Invoke)).Invoke();
await Task.Delay(20);if(ViewModel.ClearMarketCacheCommand.ExecutionTask is{} task)await task;
await ViewModel.RefreshMarketCacheUsageCommand.ExecuteAsync(null);
Check("the original cache backup was removed",!File.Exists(legacy),"Only the explicit isolated 1 MiB market backup was seeded for this action.");
Check("active market data was reset",Runtime.Markets.GetStation(fixtureStation) is null,"The seeded station and quote are absent after the actual UI cleanup action.");
Check("settings event index and trade ledger were preserved",protectedFiles.All(pair=>File.Exists(pair.Key)&&Hash(pair.Key)==pair.Value),"The original bytes of every protected fixture file agree.");
await Capture("cache-settings-cleared");
}
var result=new{checks,captures,fixture=exercise,productionCacheCleared=false,Runtime.DataDirectory,beforeText,afterText=ViewModel.MarketCacheUsageText,status=ViewModel.MarketCacheStatus,legacyNotice=ViewModel.MarketCacheLegacyNotice,protectedFiles,gameCommandsInvoked=false,tradeOrdersSubmitted=false,osInputsSent=false,
visualReview="Actual pixel/ROI and text inspection is still required; a successful capture is not a visual pass."};
await File.WriteAllTextAsync(Path.Combine(directory,"cache-settings-evidence.json"),JsonSerializer.Serialize(result,new JsonSerializerOptions{WriteIndented=true}));
return result;
}
}
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;
using XFEEDToolBox.Controls;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Views;
namespace XFEEDToolBox;
public partial class App
{
private static async Task<object> RunMarketCacheSettingsProbeAsync(string directory)
{
var exercise=Environment.GetCommandLineArgs().Contains("--exercise-fixture-cache-clear");
var fixturePath=Path.GetFullPath(Path.Combine(directory,"isolated-data"));
if(exercise&&(!ViewModel.OfflinePreview||!Path.GetFullPath(Runtime.DataDirectory).Equals(fixturePath,StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException("Cache cleanup acceptance may mutate only its explicit offline report-directory fixture.");
await File.WriteAllTextAsync(Path.Combine(directory,"requirements.json"),JsonSerializer.Serialize(new{requirements=new[]{
new{id="identity",kind="behavior",description="清楚区分隔离清理夹具与真实数据目录,只在显式隔离夹具执行删除。"},
new{id="scope",kind="text",description="显示真实占用、默认500ly与可选中心的规则、旧存量与个人数据保留说明。"},
new{id="clear",kind="behavior",description="实际按钮清理夹具行情及旧备份,并显示结果;生产只读取图不据此声称大库已清理。"},
new{id="guard",kind="behavior",description="运行状态禁用清理,恢复可用后仅清除行情数据,设置、事件与交易记录字节保持。"},
new{id="layout",kind="layout",description="宽窗和窄窗中说明、占用、按钮及状态无重叠与不可访问裁切。"}
}},new JsonSerializerOptions{WriteIndented=true}));
var checks=new List<object>();var captures=new List<object>();var protectedFiles=new Dictionary<string,string>();
void Check(string item,bool passed,string evidence)
{checks.Add(new{item,passed,evidence});if(!passed)throw new InvalidOperationException(item+": "+evidence);}
static string Hash(string path)
{
using var bytes=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
return Convert.ToHexStringLower(SHA256.HashData(bytes));
}
var legacy=Path.Combine(Runtime.DataDirectory,"markets.db.verified.db");
const long fixtureStation=7000000000001;
if(exercise)
{
Runtime.Settings.Save(Runtime.SettingsPath);
var history=Path.Combine(Runtime.DataDirectory,"trade-ledgers");Directory.CreateDirectory(history);
var sentinel=Path.Combine(history,"cache-probe-preserve.json");await File.WriteAllTextAsync(sentinel,"{\"fixture\":true}");
foreach(var file in new[]{Runtime.SettingsPath,Path.Combine(Runtime.DataDirectory,"game-events.db"),sentinel})
if(File.Exists(file))protectedFiles[file]=Hash(file);
var now=DateTimeOffset.UtcNow;
var station=new Station{MarketId=fixtureStation,Name="Cache fixture",SystemName="Isolated fixture",Position=new(0,0,0),UpdatedAt=now};
var seeded=await Runtime.Markets.SaveSnapshotAsync(new(station,[new CommodityQuote{MarketId=fixtureStation,Commodity="fixture",DisplayName="Cache fixture",BuyPrice=1,SellPrice=2,CollectedAt=now,Source=DataSource.Journal}],now,DataSource.Journal));
Check("the explicit local-market fixture was seeded",seeded&&Runtime.Markets.GetStation(fixtureStation) is not null,"This is synthetic offline cache data using the local-observation bypass, never a live Journal or game transaction.");
await File.WriteAllBytesAsync(legacy,new byte[1024*1024]);
}
AppShellPage.Current!.Navigate(typeof(SettingPage));await Task.Delay(180);
var page=(SettingPage)AppShellPage.Current.CurrentPage!;
var data=(CommunityToolkit.WinUI.Controls.SettingsExpander)page.FindName("DataSettings");data.IsExpanded=true;
await ViewModel.RefreshMarketCacheUsageCommand.ExecuteAsync(null);await Task.Delay(140);page.UpdateLayout();
var card=(FrameworkElement)page.FindName("MarketCacheCard");
var scroll=(ScrollViewer)page.FindName("SettingsScrollViewer");
var button=Descendants(page).OfType<Button>().Single(b=>ReferenceEquals(b.Command,ViewModel.ClearMarketCacheCommand)||ReferenceEquals(AsyncAction.GetCommand(b),ViewModel.ClearMarketCacheCommand));
Check("cache card and exact cleanup command are present",card.ActualWidth>100&&button.IsLoaded,"The rendered Settings card is bound to the production cleanup command.");
var beforeText=ViewModel.MarketCacheUsageText;
Check("actual usage is displayed",!string.IsNullOrWhiteSpace(beforeText),beforeText);
async Task Capture(string name)
{
page.UpdateLayout();var point=card.TransformToVisual(scroll).TransformPoint(new(0,0));
scroll.ChangeView(null,Math.Max(0,scroll.VerticalOffset+point.Y-16),null,true);await Task.Delay(140);page.UpdateLayout();
var path=Path.Combine(directory,name+".png");await CaptureRefinementElementAsync((FrameworkElement)MainWindow.Content,path);
var bounds=card.TransformToVisual(scroll).TransformPoint(new(0,0));
captures.Add(new{name,path,sha256=Hash(path),window=new{MainWindow.AppWindow.Size.Width,MainWindow.AppWindow.Size.Height},card=new{bounds.X,bounds.Y,card.ActualWidth,card.ActualHeight},scroll=new{scroll.ActualWidth,scroll.ActualHeight,scroll.VerticalOffset}});
}
await Capture("cache-settings-wide");
MainWindow.AppWindow.Resize(new(1280,1040));await Task.Delay(180);await Capture("cache-settings-compact");
if(exercise)
{
var previous=ViewModel.RunActive;
try
{
ViewModel.RunActive=true;await Task.Delay(100);
Check("cleanup is blocked by the running UI state",!ViewModel.ClearMarketCacheCommand.CanExecute(null)&&!button.IsEnabled,"This is an isolated UI-state fixture; no trade engine or game action was started.");
await Capture("cache-settings-blocked");
}
finally{ViewModel.RunActive=previous;}
await Task.Delay(100);Check("cleanup is available after the fixture stops",button.IsEnabled&&ViewModel.ClearMarketCacheCommand.CanExecute(null),ViewModel.MarketCacheBlockReason);
((IInvokeProvider)new ButtonAutomationPeer(button).GetPattern(PatternInterface.Invoke)).Invoke();
await Task.Delay(20);if(ViewModel.ClearMarketCacheCommand.ExecutionTask is{} task)await task;
await ViewModel.RefreshMarketCacheUsageCommand.ExecuteAsync(null);
Check("the original cache backup was removed",!File.Exists(legacy),"Only the explicit isolated 1 MiB market backup was seeded for this action.");
Check("active market data was reset",Runtime.Markets.GetStation(fixtureStation) is null,"The seeded station and quote are absent after the actual UI cleanup action.");
Check("settings event index and trade ledger were preserved",protectedFiles.All(pair=>File.Exists(pair.Key)&&Hash(pair.Key)==pair.Value),"The original bytes of every protected fixture file agree.");
await Capture("cache-settings-cleared");
}
var result=new{checks,captures,fixture=exercise,productionCacheCleared=false,Runtime.DataDirectory,beforeText,afterText=ViewModel.MarketCacheUsageText,status=ViewModel.MarketCacheStatus,legacyNotice=ViewModel.MarketCacheLegacyNotice,protectedFiles,gameCommandsInvoked=false,tradeOrdersSubmitted=false,osInputsSent=false,
visualReview="Actual pixel/ROI and text inspection is still required; a successful capture is not a visual pass."};
await File.WriteAllTextAsync(Path.Combine(directory,"cache-settings-evidence.json"),JsonSerializer.Serialize(result,new JsonSerializerOptions{WriteIndented=true}));
return result;
}
}