Files
ft/CodexOutput/upserver/KVSyncReviewTests/Program.cs

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 ")));