132 lines
10 KiB
C#
132 lines
10 KiB
C#
using game.PlayerDataStorage;
|
|
using Newtonsoft.Json.Linq;
|
|
using System.Security.Cryptography;
|
|
|
|
var suiteRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../Evidence", DateTime.UtcNow.ToString("yyyyMMdd-HHmmss-fff")));
|
|
Directory.CreateDirectory(suiteRoot);
|
|
var api = new Uri("http://s101.local:18080/"); // Identity only; this program has no HTTP dependency.
|
|
var outcomes = new List<string>();
|
|
int count = 0;
|
|
void Check(bool condition, string name) { if (!condition) throw new Exception(name); outcomes.Add("PASS " + name); }
|
|
void Case(string name, Action<string> test) { var root = Path.Combine(suiteRoot, (++count).ToString("D2")); Directory.CreateDirectory(root); test(root); Console.WriteLine("PASS " + name); }
|
|
Dictionary<string,string> Values(params string[] pairs) { var d = new Dictionary<string,string>(StringComparer.Ordinal); for(int n=0;n<pairs.Length;n+=2)d.Add(pairs[n],pairs[n+1]);return d; }
|
|
PlayerKVSyncState Open(string root, Action<KVCacheWriteStage> fault = null) { var r=PlayerKVSyncState.Open(root,api,"101",fault);Check(r.Success,"Open succeeds");return r.State; }
|
|
string MainFile(string root) => Path.Combine(new KVIdentity(root,api,"101").DirectoryPath,"state.json");
|
|
Case("absolute vs drive-relative root identity", root => {
|
|
var normal = new KVIdentity(root,api,"101");
|
|
Check(Path.IsPathFullyQualified(normal.DirectoryPath),"normal absolute root remains absolute");
|
|
var driveRelative = "C:CodexOutput/upserver/KVSyncReviewTests";
|
|
Check(Path.IsPathRooted(driveRelative) && !Path.IsPathFullyQualified(driveRelative),"Windows drive-relative input is rooted but not absolute");
|
|
bool rejected = false;
|
|
try { _ = new KVIdentity(driveRelative,api,"101"); } catch (ArgumentException) { rejected = true; }
|
|
outcomes.Add("OBSERVED DriveRelativeRejected="+rejected);
|
|
Check(rejected,"drive-relative root rejected after reviewed fix");
|
|
Check(PlayerKVSyncState.Open(@"\relative",api,"101").Error==PlayerKVOpenError.Validation,"root-relative path rejected after reviewed fix");
|
|
File.WriteAllText(Path.Combine(suiteRoot,"root-validation-observation.txt"),"DriveRelativeRejected="+rejected+Environment.NewLine);
|
|
});
|
|
Case("active old version / newer pending / restart unknown", root => {
|
|
using(var s=Open(root)) {
|
|
Check(s.Reconcile(Values("k","base")).Success,"baseline persisted");
|
|
var v1=s.Update("k","one"); Check(v1.Success,"v1 accepted");
|
|
Check(s.BeginBatch(long.MaxValue,out var b).Success && b.Items.Count==1,"active v1 persisted");
|
|
Check(s.Update("k","two").Success,"v2 accepted during v1");
|
|
}
|
|
using(var s=Open(root)) {
|
|
Check(s.Status==PlayerKVSyncStatus.UnknownOutcome,"active recovery freezes");
|
|
Check(s.Inspect().UnknownBatch.Items[0].Target.Value=="one" && s.Inspect().Pending["k"].Target.Value=="two","both versions survive restart");
|
|
Check(s.Reconcile(Values("k","base")).Success && s.Status==PlayerKVSyncStatus.UnknownOutcome,"R=B cannot unlock unknown");
|
|
Check(s.Reconcile(Values("k","two")).Success && s.Status==PlayerKVSyncStatus.UnknownOutcome,"R=L cannot unlock unknown");
|
|
Check(!s.Update("k","three").Success,"unknown refuses editing");
|
|
}
|
|
});
|
|
Case("confirmation advances newer pending base and preserves localOnly", root=>{
|
|
using var s=Open(root); s.Reconcile(Values("k","base"));s.Update("k","one");s.BeginBatch(long.MaxValue,out var b);
|
|
s.Update("k","two");s.Update("k","local",false);
|
|
Check(s.ConfirmBatch(b.Id).Success,"confirm v1 succeeds");
|
|
Check(s.Inspect().Pending["k"].BaseValue.Value=="one" && s.Inspect().Pending["k"].Target.Value=="two","v2 base advanced and target preserved");
|
|
Check(s.ReadLocal()["k"]=="local","localOnly survives remote confirmation");
|
|
Check(s.Delete("k").Success && !s.ReadLocal().ContainsKey("k"),"Delete removes localOnly atomically");
|
|
});
|
|
foreach(var stage in Enum.GetValues<KVCacheWriteStage>()) Case("disk fault "+stage,root=>{
|
|
bool armed=false;
|
|
using(var s=Open(root, at=>{if(armed && at==stage)throw new IOException("Injected review fault");})) {
|
|
Check(s.Reconcile(Values("k","base")).Success,"seed baseline before disk fault");armed=true;
|
|
var edit=s.Update("k","target");
|
|
Check(!edit.Success && s.Status==PlayerKVSyncStatus.StorageError,"fault freezes without accepted edit");
|
|
Check(s.Inspect().Pending.Count==0,"failed commit never publishes memory");
|
|
Check(edit.LocalCommitMayHaveSucceeded==(stage==KVCacheWriteStage.AfterReplace),"commit uncertainty matches injection boundary");
|
|
Check(!s.Update("k","third").Success,"frozen edit is blocked");
|
|
}
|
|
using(var s=Open(root)) {
|
|
Check(s.Inspect().Pending.Count==(stage==KVCacheWriteStage.AfterReplace?1:0),"reopen reads actually committed revision");
|
|
}
|
|
});
|
|
Case("corrupt snapshots reject and release file lock",root=>{
|
|
using(var s=Open(root)){s.Reconcile(Values("k","base"));s.Update("k","target");}
|
|
var file=MainFile(root);var valid=File.ReadAllText(file);
|
|
var corruptions = new List<string>{"[{}]","null","{}",valid+" {}",valid.Replace("\"formatVersion\":1","\"formatVersion\":1,\"formatVersion\":1")};
|
|
foreach(var field in new[]{"revision","nextSequence","formatVersion"}) {
|
|
var j=JObject.Parse(valid);j[field]=JToken.Parse("9223372036854775808");corruptions.Add(j.ToString());
|
|
j=JObject.Parse(valid);j[field]=JValue.CreateNull();corruptions.Add(j.ToString());
|
|
}
|
|
foreach(var malformed in corruptions) {
|
|
File.WriteAllText(file,malformed);var result=PlayerKVSyncState.Open(root,api,"101");
|
|
Check(result.Error==PlayerKVOpenError.RecoveryRequired,"malformed snapshot rejects with RecoveryRequired");
|
|
File.WriteAllText(file,valid);using var recovered=Open(root);
|
|
Check(recovered.Inspect().Pending.Count==1,"failed open released exclusive lock and preserved source log");
|
|
}
|
|
});
|
|
Case("backup is evidence only / invalid backup does not veto valid main",root=>{
|
|
using(var s=Open(root)){s.Reconcile(Values());s.Update("k","target");}
|
|
var file=MainFile(root);var valid=File.ReadAllText(file);File.WriteAllText(file+".bak","corrupt");
|
|
using(var s=Open(root)){Check(s.Inspect().Pending.Count==1,"valid main wins over corrupt backup");}
|
|
File.Delete(file);
|
|
Check(PlayerKVSyncState.Open(root,api,"101").Error==PlayerKVOpenError.RecoveryRequired,"missing main plus corrupt backup cannot create empty cache");
|
|
File.WriteAllText(file,valid);using var restored=Open(root);
|
|
});
|
|
Case("exclusive lock and repeated dispose",root=>{
|
|
var first=Open(root);Check(PlayerKVSyncState.Open(root,api,"101").Error==PlayerKVOpenError.Busy,"duplicate owner returns Busy");
|
|
first.Dispose();first.Dispose();Check(!first.Reconcile(Values()).Success,"closed state rejects writes");
|
|
using var second=Open(root);Check(second.Reconcile(Values()).Success,"new owner opens after Dispose");
|
|
});
|
|
Case("batch association corruption and legal same/newer sequence",root=>{
|
|
using(var s=Open(root)) { s.Reconcile(Values("k","base"));s.Update("k","one");s.BeginBatch(long.MaxValue,out _);s.Update("k","two"); }
|
|
var file=MainFile(root);var valid=File.ReadAllText(file);
|
|
var mutations=new (string,Action<JObject>)[] {
|
|
("active plus unknown",j=>j["unknownBatch"]=j["activeBatch"].DeepClone()),
|
|
("missing corresponding pending",j=>j["pending"]=new JArray()),
|
|
("active newer than pending",j=>j["activeBatch"]["items"][0]["sequence"]=j["nextSequence"].DeepClone()),
|
|
("same sequence with unequal targets",j=>j["pending"][0]["sequence"]=j["activeBatch"]["items"][0]["sequence"].DeepClone()),
|
|
("active and pending base mismatch",j=>j["activeBatch"]["items"][0]["base"]["value"]="wrong"),
|
|
("active key absent from pending",j=>j["activeBatch"]["items"][0]["key"]="other"),
|
|
("duplicate batch key",j=>((JArray)j["activeBatch"]["items"]).Add(j["activeBatch"]["items"][0].DeepClone())),
|
|
("empty batch",j=>j["activeBatch"]["items"]=new JArray()),
|
|
("invalid batch id",j=>j["activeBatch"]["id"]="not-guid"),
|
|
("batch null items",j=>j["activeBatch"]["items"]=null),
|
|
("scalar batch",j=>j["activeBatch"]=1),
|
|
("no baseline with active",j=>j["hasBaseline"]=false)
|
|
};
|
|
foreach(var (name,mutate) in mutations) {
|
|
var json=JObject.Parse(valid);mutate(json);File.WriteAllText(file,json.ToString());
|
|
var result=PlayerKVSyncState.Open(root,api,"101");
|
|
Check(result.Error==PlayerKVOpenError.RecoveryRequired,"relation corruption rejected: "+name);
|
|
}
|
|
File.WriteAllText(file,valid);using(var s=Open(root)) {Check(s.Inspect().Pending["k"].Target.Value=="two","valid newer sequence is accepted");}
|
|
var same=JObject.Parse(valid);same["pending"][0]=same["activeBatch"]["items"][0].DeepClone();File.WriteAllText(file,same.ToString());
|
|
using(var s=Open(root)) { Check(s.Inspect().Pending["k"].Sequence==s.Inspect().UnknownBatch.Items[0].Sequence,"legal same sequence reference is accepted"); }
|
|
});
|
|
Case("ordinary conflict survives restart with edit base distinct from last observation",root=>{
|
|
using(var s=Open(root)){s.Reconcile(Values("k","base"));s.Update("k","target");Check(s.Reconcile(Values("k","other")).Success && s.Status==PlayerKVSyncStatus.Conflict,"conflict records new baseline observation");}
|
|
using(var s=Open(root)) {
|
|
Check(s.Inspect().Baseline["k"]=="other" && s.Inspect().Pending["k"].BaseValue.Value=="base","baseline observation differs legitimately from edit base");
|
|
Check(s.Reconcile(Values("k","other")).Success && s.Status==PlayerKVSyncStatus.Conflict,"ordinary conflict does not disappear on reopen");
|
|
Check(s.Reconcile(Values("k","base")).Success && s.Status==PlayerKVSyncStatus.Ready,"ordinary conflict can resolve on matching edit base");
|
|
}
|
|
});
|
|
var sourceFiles=new[]{"PlayerKVSyncModels.cs","PlayerKVCacheStore.cs","PlayerKVSyncState.cs"};
|
|
foreach(var source in sourceFiles) { var path=Path.GetFullPath(Path.Combine(AppContext.BaseDirectory,"../../../../../../Client/Assets/Scripts/Services/PlayerData",source)); outcomes.Add("SHA256 "+source+" "+Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)))); }
|
|
outcomes.Add("CompletedCases="+count);
|
|
File.WriteAllLines(Path.Combine(suiteRoot,"Latest-review.log"),outcomes);
|
|
Console.WriteLine("Evidence="+suiteRoot);
|
|
Console.WriteLine("Assertions="+outcomes.Count(s=>s.StartsWith("PASS ")));
|