using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class TradeRecoveryTests
{
private sealed class ReadOnlyChannel(string identity,long credits,int cargo=0):IOriginalCommerceChannel
{
public string Identity=>identity;
public readonly List<string> Calls=[];
public Task<JsonElement> CallAsync(object request,CancellationToken token)
{
token.ThrowIfCancellationRequested();var row=Check.Event(request);var op=row.GetProperty("op").GetString()!;Calls.Add(op);
return Task.FromResult(op switch
{
"commerce.inventory"=>Check.Event(new{verified=true,credits,count=cargo,capacity=2}),
"commerce.market.read"=>Check.Event(new{generation=1}),
"commerce.market.status"=>Check.Event(new{state=3,cargo=cargo==0?new Dictionary<string,int>():new(){["gold"]=cargo},serverCredits=credits}),
_=>throw new InvalidOperationException("Reconciliation attempted a mutating request: "+op)
});
}
}
private static readonly object Failure=new{state=4,error=500,mutationsSent=1,serviceKind=1,serverConfirmed=false,clientApplied=false};
private static async Task Save(string path,object value)=>await File.WriteAllTextAsync(path,JsonSerializer.Serialize(value));
private static async Task<string> Pending(TemporaryDirectory temp,DateTimeOffset at,string identity="1:1")
{
var path=temp.File("original.pending.json");await Save(path,new{created=at,channel=identity,CommanderId="F1",CurrentShipId=7,intent=new{market=2,Commodity="gold",delta=-2,before=new{credits=9920,count=2,capacity=2,verified=true}}});
await Save(temp.File("original.status.json"),Failure);return path;
}
[Test] public async Task SameOriginalProcessRequiresOfficialRecoveryWithoutAnyTradeRequest()
{
using var temp=new TemporaryDirectory();var path=await Pending(temp,DateTimeOffset.UtcNow.AddMinutes(-2));var channel=new ReadOnlyChannel("1:1",9920,2);var caught=false;
try{await NativeOrderReconciliation.WaitForResolutionAsync(path,channel,()=>new(),null,null,default);}catch(NativeOrderReconciliationRequiredException){caught=true;}
Check.That(caught);Check.Equal(0,channel.Calls.Count);Check.That(!File.Exists(temp.File("original.not-applied-after-reload.json")));
}
[Test] public async Task FreshOfficialSessionAndExactServerCargoProveOriginalRequestWasNotApplied()
{
using var temp=new TemporaryDirectory();var at=DateTimeOffset.UtcNow.AddMinutes(-2);var path=await Pending(temp,at);var identity=$"2:{at.AddMinutes(1).UtcTicks}";
var channel=new ReadOnlyChannel(identity,9920,2);var player=new PlayerState{CommanderId="F1",CurrentShipId=7,InGame=true,Docked=true,MarketId=2,CargoReliable=true,CreditsReliable=true,CargoCapacity=2,CargoCount=2,Credits=9920,LoadGameAt=at.AddMinutes(1)};
var pending=await File.ReadAllTextAsync(path);var receipt=await File.ReadAllTextAsync(temp.File("original.status.json"));
await NativeOrderReconciliation.WaitForResolutionAsync(path,channel,()=>player,null,null,default);
Check.That(NativeOrderReconciliation.IsResolved(path,player));Check.Equal(pending,await File.ReadAllTextAsync(path));Check.Equal(receipt,await File.ReadAllTextAsync(temp.File("original.status.json")));
Check.That(channel.Calls.SequenceEqual(new[]{"commerce.inventory","commerce.market.read","commerce.market.status"}));
}
[Test] public async Task DifferentWalletWaitsForEvidenceUntilCancellationAndNeverReplays()
{
using var temp=new TemporaryDirectory();var at=DateTimeOffset.UtcNow.AddMinutes(-2);var path=await Pending(temp,at);var channel=new ReadOnlyChannel($"2:{at.AddMinutes(1).UtcTicks}",9919,2);
var player=new PlayerState{CommanderId="F1",CurrentShipId=7,InGame=true,Docked=true,MarketId=2,CargoReliable=true,CreditsReliable=true,CargoCapacity=2,CargoCount=2,Credits=9919,LoadGameAt=at.AddMinutes(1)};
using var stop=new CancellationTokenSource(150);var cancelled=false;
try{await NativeOrderReconciliation.WaitForResolutionAsync(path,channel,()=>player,null,null,stop.Token);}catch(OperationCanceledException){cancelled=true;}
Check.That(cancelled);Check.That(!NativeOrderReconciliation.IsResolved(path,player));Check.That(channel.Calls.All(op=>op=="commerce.inventory"));
}
private static async Task<(string Directory,PlayerState Player)> ManualFixture(TemporaryDirectory temp)
{
var at=DateTimeOffset.UtcNow.AddMinutes(-3);var directory=temp.File("journey-fixture");Directory.CreateDirectory(directory);Directory.CreateDirectory(temp.File("operations"));
var bought=new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"),1,"gold",2,40,10000,9920,0,2,"F1",7,at.AddMinutes(-1),at,true,true);
TradeJourneyCheckpoints.Save(Path.Combine(directory,"journey.json"),new("F1",7,at,bought));await Save(Path.Combine(directory,"leg.json"),new TradeLeg{From=new(){MarketId=1},To=new(){MarketId=2},Orders=[new("gold","Gold",2,40,100)]});
await Save(Path.Combine(directory,"opening-wallet.json"),new{credits=10000});await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),"{\"passed\":false}");
var pending=temp.File("operations/original.pending.json");await Save(pending,new{created=at.AddSeconds(1),CommanderId="F1",CurrentShipId=7,intent=new{market=2,Commodity="gold",delta=-2}});await Save(temp.File("operations/original.status.json"),Failure);
object[] rows=[new{timestamp=at.AddSeconds(-1),@event="LoadGame",FID="F1",ShipID=7},new{timestamp=at.AddSeconds(1),@event="Docked",MarketID=2},
new{timestamp=at.AddSeconds(2),@event="RefuelAll",Cost=23},new{timestamp=at.AddSeconds(3),@event="PayFines",Amount=40,ShipID=7},
new{timestamp=at.AddSeconds(4),@event="MarketSell",MarketID=2,Type="gold",Count=2,SellPrice=100,TotalSale=200,AvgPricePaid=40}];
await File.WriteAllLinesAsync(temp.File("Journal.fixture.log"),rows.Select(r=>JsonSerializer.Serialize(r)));await File.AppendAllTextAsync(temp.File("Journal.fixture.log"),"{partial");
return(directory,new(){InGame=true,Docked=true,CommanderId="F1",CurrentShipId=7,MarketId=3,CargoReliable=true,CreditsReliable=true,CargoCount=0,CargoCapacity=2,Credits=10057});
}
[Test] public async Task ActualManualSaleAndPaidCostsSettleFinanciallyWithoutRewritingFailedNativeJourney()
{
using var temp=new TemporaryDirectory();var (directory,player)=await ManualFixture(temp);var channel=new ReadOnlyChannel("2:1",10057);
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
Check.That(File.Exists(Path.Combine(directory,"settled.json")));Check.Equal("{\"passed\":false}",await File.ReadAllTextAsync(Path.Combine(directory,"result.json")));
using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));Check.Equal(57m,ledger.Snapshot.NetProfit);Check.Equal(40m,ledger.Snapshot.OtherExpenses);Check.Equal(23m,ledger.Snapshot.Fuel);
Check.That(File.Exists(temp.File("operations/original.externally-settled.json")));Check.Equal(3,channel.Calls.Count);
}
[Test] public async Task ProvenManualSaleReleasesTradingButUnexplainedMoneyIsNotRecognizedAsAutomationProfit()
{
using var temp=new TemporaryDirectory();var (directory,player)=await ManualFixture(temp);var channel=new ReadOnlyChannel("2:1",10056);player=player with{Credits=10056};
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
Check.That(File.Exists(Path.Combine(directory,"settled.json")));Check.That(File.Exists(temp.File("operations/original.externally-settled.json")));
using var result=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory,"external-result.json")));
Check.That(result.RootElement.GetProperty("tradeSettled").GetBoolean());Check.That(!result.RootElement.GetProperty("passed").GetBoolean());
using var proof=JsonDocument.Parse(await File.ReadAllTextAsync(temp.File("operations/original.externally-settled.json")));Check.Equal(-1m,proof.RootElement.GetProperty("unexplainedWalletDifference").GetDecimal());
}
[Test] public async Task LaterManualRoundTripExplainsCurrentWalletWithoutInflatingAutomationProfit()
{
using var temp=new TemporaryDirectory();var (directory,player)=await ManualFixture(temp);var at=DateTimeOffset.UtcNow.AddMinutes(-1);
object[] later=[new{timestamp=at,@event="LoadGame",FID="F1",ShipID=7},new{timestamp=at.AddSeconds(1),@event="Docked",MarketID=2},
new{timestamp=at.AddSeconds(2),@event="MarketBuy",MarketID=2,Type="silver",Count=2,BuyPrice=40,TotalCost=80},
new{timestamp=at.AddSeconds(3),@event="MarketSell",MarketID=1,Type="silver",Count=2,SellPrice=100,TotalSale=200,AvgPricePaid=40},new{timestamp=at.AddSeconds(4),@event="RefuelAll",Cost=7}];
await File.AppendAllTextAsync(temp.File("Journal.fixture.log"),"\n");await File.AppendAllLinesAsync(temp.File("Journal.fixture.log"),later.Select(r=>JsonSerializer.Serialize(r)));
player=player with{Credits=10170};var channel=new ReadOnlyChannel("2:1",10170);
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
Check.That(File.Exists(Path.Combine(directory,"settled.json")));using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));Check.Equal(57m,ledger.Snapshot.NetProfit);
using var proof=JsonDocument.Parse(await File.ReadAllTextAsync(temp.File("operations/original.externally-settled.json")));Check.Equal(113m,proof.RootElement.GetProperty("afterSettlementCashChange").GetDecimal());
}
[Test] public async Task PaidStationFineClearsTheWaitButAnotherCommandersPaymentDoesNot()
{
using var temp=new TemporaryDirectory();object[] rows=[new{@event="LoadGame",FID="F1"},new{@event="Docked",MarketID=2,StationFaction=new{Name="Authority"}},new{@event="CommitCrime",Faction="Authority",Fine=400},
new{@event="LoadGame",FID="F2"},new{@event="PayFines",Faction="Authority",Amount=400,AllFines=true}];
await File.WriteAllLinesAsync(temp.File("Journal.fixture.log"),rows.Select(r=>JsonSerializer.Serialize(r)));
Check.Equal(400L,await StationAccessWaiter.KnownFineAsync(temp.Path,"F1",2,default));
await File.AppendAllLinesAsync(temp.File("Journal.fixture.log"),new[]{JsonSerializer.Serialize(new{@event="LoadGame",FID="F1"}),JsonSerializer.Serialize(new{@event="PayFines",Faction="Authority",Amount=400})});
Check.Equal(0L,await StationAccessWaiter.KnownFineAsync(temp.Path,"F1",2,default));
}
[Test] public void CurrentAutomationProfitSurvivesRecoveryAndANewRunStartsFromZero()
{
using var temp=new TemporaryDirectory();var first=AutomaticTradeRunSession.Open(temp.Path,5000);Check.Equal(0m,first.Profit(5000));
var recovered=AutomaticTradeRunSession.Open(temp.Path,8000);Check.Equal(first.Id,recovered.Id);Check.Equal(3000m,recovered.Profit(8000));
AutomaticTradeRunSession.Finish(temp.Path);var next=AutomaticTradeRunSession.Open(temp.Path,8000);Check.That(first.Id!=next.Id);Check.Equal(0m,next.Profit(8000));
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class TradeRecoveryTests
{
private sealed class ReadOnlyChannel(string identity,long credits,int cargo=0):IOriginalCommerceChannel
{
public string Identity=>identity;
public readonly List<string> Calls=[];
public Task<JsonElement> CallAsync(object request,CancellationToken token)
{
token.ThrowIfCancellationRequested();var row=Check.Event(request);var op=row.GetProperty("op").GetString()!;Calls.Add(op);
return Task.FromResult(op switch
{
"commerce.inventory"=>Check.Event(new{verified=true,credits,count=cargo,capacity=2}),
"commerce.market.read"=>Check.Event(new{generation=1}),
"commerce.market.status"=>Check.Event(new{state=3,cargo=cargo==0?new Dictionary<string,int>():new(){["gold"]=cargo},serverCredits=credits}),
_=>throw new InvalidOperationException("Reconciliation attempted a mutating request: "+op)
});
}
}
private static readonly object Failure=new{state=4,error=500,mutationsSent=1,serviceKind=1,serverConfirmed=false,clientApplied=false};
private static async Task Save(string path,object value)=>await File.WriteAllTextAsync(path,JsonSerializer.Serialize(value));
private static async Task<string> Pending(TemporaryDirectory temp,DateTimeOffset at,string identity="1:1")
{
var path=temp.File("original.pending.json");await Save(path,new{created=at,channel=identity,CommanderId="F1",CurrentShipId=7,intent=new{market=2,Commodity="gold",delta=-2,before=new{credits=9920,count=2,capacity=2,verified=true}}});
await Save(temp.File("original.status.json"),Failure);return path;
}
[Test] public async Task SameOriginalProcessRequiresOfficialRecoveryWithoutAnyTradeRequest()
{
using var temp=new TemporaryDirectory();var path=await Pending(temp,DateTimeOffset.UtcNow.AddMinutes(-2));var channel=new ReadOnlyChannel("1:1",9920,2);var caught=false;
try{await NativeOrderReconciliation.WaitForResolutionAsync(path,channel,()=>new(),null,null,default);}catch(NativeOrderReconciliationRequiredException){caught=true;}
Check.That(caught);Check.Equal(0,channel.Calls.Count);Check.That(!File.Exists(temp.File("original.not-applied-after-reload.json")));
}
[Test] public async Task FreshOfficialSessionAndExactServerCargoProveOriginalRequestWasNotApplied()
{
using var temp=new TemporaryDirectory();var at=DateTimeOffset.UtcNow.AddMinutes(-2);var path=await Pending(temp,at);var identity=$"2:{at.AddMinutes(1).UtcTicks}";
var channel=new ReadOnlyChannel(identity,9920,2);var player=new PlayerState{CommanderId="F1",CurrentShipId=7,InGame=true,Docked=true,MarketId=2,CargoReliable=true,CreditsReliable=true,CargoCapacity=2,CargoCount=2,Credits=9920,LoadGameAt=at.AddMinutes(1)};
var pending=await File.ReadAllTextAsync(path);var receipt=await File.ReadAllTextAsync(temp.File("original.status.json"));
await NativeOrderReconciliation.WaitForResolutionAsync(path,channel,()=>player,null,null,default);
Check.That(NativeOrderReconciliation.IsResolved(path,player));Check.Equal(pending,await File.ReadAllTextAsync(path));Check.Equal(receipt,await File.ReadAllTextAsync(temp.File("original.status.json")));
Check.That(channel.Calls.SequenceEqual(new[]{"commerce.inventory","commerce.market.read","commerce.market.status"}));
}
[Test] public async Task DifferentWalletWaitsForEvidenceUntilCancellationAndNeverReplays()
{
using var temp=new TemporaryDirectory();var at=DateTimeOffset.UtcNow.AddMinutes(-2);var path=await Pending(temp,at);var channel=new ReadOnlyChannel($"2:{at.AddMinutes(1).UtcTicks}",9919,2);
var player=new PlayerState{CommanderId="F1",CurrentShipId=7,InGame=true,Docked=true,MarketId=2,CargoReliable=true,CreditsReliable=true,CargoCapacity=2,CargoCount=2,Credits=9919,LoadGameAt=at.AddMinutes(1)};
using var stop=new CancellationTokenSource(150);var cancelled=false;
try{await NativeOrderReconciliation.WaitForResolutionAsync(path,channel,()=>player,null,null,stop.Token);}catch(OperationCanceledException){cancelled=true;}
Check.That(cancelled);Check.That(!NativeOrderReconciliation.IsResolved(path,player));Check.That(channel.Calls.All(op=>op=="commerce.inventory"));
}
private static async Task<(string Directory,PlayerState Player)> ManualFixture(TemporaryDirectory temp)
{
var at=DateTimeOffset.UtcNow.AddMinutes(-3);var directory=temp.File("journey-fixture");Directory.CreateDirectory(directory);Directory.CreateDirectory(temp.File("operations"));
var bought=new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"),1,"gold",2,40,10000,9920,0,2,"F1",7,at.AddMinutes(-1),at,true,true);
TradeJourneyCheckpoints.Save(Path.Combine(directory,"journey.json"),new("F1",7,at,bought));await Save(Path.Combine(directory,"leg.json"),new TradeLeg{From=new(){MarketId=1},To=new(){MarketId=2},Orders=[new("gold","Gold",2,40,100)]});
await Save(Path.Combine(directory,"opening-wallet.json"),new{credits=10000});await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),"{\"passed\":false}");
var pending=temp.File("operations/original.pending.json");await Save(pending,new{created=at.AddSeconds(1),CommanderId="F1",CurrentShipId=7,intent=new{market=2,Commodity="gold",delta=-2}});await Save(temp.File("operations/original.status.json"),Failure);
object[] rows=[new{timestamp=at.AddSeconds(-1),@event="LoadGame",FID="F1",ShipID=7},new{timestamp=at.AddSeconds(1),@event="Docked",MarketID=2},
new{timestamp=at.AddSeconds(2),@event="RefuelAll",Cost=23},new{timestamp=at.AddSeconds(3),@event="PayFines",Amount=40,ShipID=7},
new{timestamp=at.AddSeconds(4),@event="MarketSell",MarketID=2,Type="gold",Count=2,SellPrice=100,TotalSale=200,AvgPricePaid=40}];
await File.WriteAllLinesAsync(temp.File("Journal.fixture.log"),rows.Select(r=>JsonSerializer.Serialize(r)));await File.AppendAllTextAsync(temp.File("Journal.fixture.log"),"{partial");
return(directory,new(){InGame=true,Docked=true,CommanderId="F1",CurrentShipId=7,MarketId=3,CargoReliable=true,CreditsReliable=true,CargoCount=0,CargoCapacity=2,Credits=10057});
}
[Test] public async Task ActualManualSaleAndPaidCostsSettleFinanciallyWithoutRewritingFailedNativeJourney()
{
using var temp=new TemporaryDirectory();var (directory,player)=await ManualFixture(temp);var channel=new ReadOnlyChannel("2:1",10057);
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
Check.That(File.Exists(Path.Combine(directory,"settled.json")));Check.Equal("{\"passed\":false}",await File.ReadAllTextAsync(Path.Combine(directory,"result.json")));
using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));Check.Equal(57m,ledger.Snapshot.NetProfit);Check.Equal(40m,ledger.Snapshot.OtherExpenses);Check.Equal(23m,ledger.Snapshot.Fuel);
Check.That(File.Exists(temp.File("operations/original.externally-settled.json")));Check.Equal(3,channel.Calls.Count);
}
[Test] public async Task ProvenManualSaleReleasesTradingButUnexplainedMoneyIsNotRecognizedAsAutomationProfit()
{
using var temp=new TemporaryDirectory();var (directory,player)=await ManualFixture(temp);var channel=new ReadOnlyChannel("2:1",10056);player=player with{Credits=10056};
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
Check.That(File.Exists(Path.Combine(directory,"settled.json")));Check.That(File.Exists(temp.File("operations/original.externally-settled.json")));
using var result=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory,"external-result.json")));
Check.That(result.RootElement.GetProperty("tradeSettled").GetBoolean());Check.That(!result.RootElement.GetProperty("passed").GetBoolean());
using var proof=JsonDocument.Parse(await File.ReadAllTextAsync(temp.File("operations/original.externally-settled.json")));Check.Equal(-1m,proof.RootElement.GetProperty("unexplainedWalletDifference").GetDecimal());
}
[Test] public async Task LaterManualRoundTripExplainsCurrentWalletWithoutInflatingAutomationProfit()
{
using var temp=new TemporaryDirectory();var (directory,player)=await ManualFixture(temp);var at=DateTimeOffset.UtcNow.AddMinutes(-1);
object[] later=[new{timestamp=at,@event="LoadGame",FID="F1",ShipID=7},new{timestamp=at.AddSeconds(1),@event="Docked",MarketID=2},
new{timestamp=at.AddSeconds(2),@event="MarketBuy",MarketID=2,Type="silver",Count=2,BuyPrice=40,TotalCost=80},
new{timestamp=at.AddSeconds(3),@event="MarketSell",MarketID=1,Type="silver",Count=2,SellPrice=100,TotalSale=200,AvgPricePaid=40},new{timestamp=at.AddSeconds(4),@event="RefuelAll",Cost=7}];
await File.AppendAllTextAsync(temp.File("Journal.fixture.log"),"\n");await File.AppendAllLinesAsync(temp.File("Journal.fixture.log"),later.Select(r=>JsonSerializer.Serialize(r)));
player=player with{Credits=10170};var channel=new ReadOnlyChannel("2:1",10170);
await ExternalNativeSaleSettlement.ReconcileAsync(temp.Path,temp.Path,player,Check.Event(new{verified=true,count=0}),channel,default);
Check.That(File.Exists(Path.Combine(directory,"settled.json")));using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));Check.Equal(57m,ledger.Snapshot.NetProfit);
using var proof=JsonDocument.Parse(await File.ReadAllTextAsync(temp.File("operations/original.externally-settled.json")));Check.Equal(113m,proof.RootElement.GetProperty("afterSettlementCashChange").GetDecimal());
}
[Test] public async Task PaidStationFineClearsTheWaitButAnotherCommandersPaymentDoesNot()
{
using var temp=new TemporaryDirectory();object[] rows=[new{@event="LoadGame",FID="F1"},new{@event="Docked",MarketID=2,StationFaction=new{Name="Authority"}},new{@event="CommitCrime",Faction="Authority",Fine=400},
new{@event="LoadGame",FID="F2"},new{@event="PayFines",Faction="Authority",Amount=400,AllFines=true}];
await File.WriteAllLinesAsync(temp.File("Journal.fixture.log"),rows.Select(r=>JsonSerializer.Serialize(r)));
Check.Equal(400L,await StationAccessWaiter.KnownFineAsync(temp.Path,"F1",2,default));
await File.AppendAllLinesAsync(temp.File("Journal.fixture.log"),new[]{JsonSerializer.Serialize(new{@event="LoadGame",FID="F1"}),JsonSerializer.Serialize(new{@event="PayFines",Faction="Authority",Amount=400})});
Check.Equal(0L,await StationAccessWaiter.KnownFineAsync(temp.Path,"F1",2,default));
}
[Test] public void CurrentAutomationProfitSurvivesRecoveryAndANewRunStartsFromZero()
{
using var temp=new TemporaryDirectory();var first=AutomaticTradeRunSession.Open(temp.Path,5000);Check.Equal(0m,first.Profit(5000));
var recovered=AutomaticTradeRunSession.Open(temp.Path,8000);Check.Equal(first.Id,recovered.Id);Check.Equal(3000m,recovered.Profit(8000));
AutomaticTradeRunSession.Finish(temp.Path);var next=AutomaticTradeRunSession.Open(temp.Path,8000);Check.That(first.Id!=next.Id);Check.Equal(0m,next.Profit(8000));
}
}