refactor(kv): 整理存档接线并清理迁移冗余实现
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public static class KVGameLoadValidation
|
||||
{
|
||||
// Fixed assembly/type/method and explicit case allow-list; no arbitrary reflection arguments.
|
||||
public static async Task<string> RunAsync(string caseId)
|
||||
{
|
||||
if (caseId != "M08-M23" && caseId != "M21-M22-M24" && caseId != "M09-M10-M12" && caseId != "M14" && caseId != "M07" && caseId != "M20-M21-M22-M24-M28-M29" && caseId != "M16-M17" && caseId != "M06-M13" && caseId != "M15-M26-M27" && caseId != "M30" && caseId != "M19" && caseId != "M11" && caseId != "M25" && caseId != "M01-M02-M21-M22-M24" && caseId != "M04-M21-M22-M24" && caseId != "M05-M21-M22-M24" && caseId != "M03" && caseId != "M18" && caseId != "M31" && caseId != "B2-20" && caseId != "M01-Immediate" && caseId != "Module02-OriginalLoad" && caseId != "M03B-LoginRetry" && caseId != "M03B-LoginRetry-Cleanup" && caseId != "M03C-CustomId" && caseId != "M03D-Destroy")
|
||||
{
|
||||
if (caseId != "P6A-InitializationLastLogin" && caseId != "P6B-OrdinarySave" && caseId != "P6B-OrdinarySave-Cleanup" && caseId != "P8A-Leaderboard" && caseId != "P8B-Friends" && caseId != "P8C-TitleNews" && caseId != "P8C-TitleNews-Cleanup" && caseId != "P8D-Matchmaking" && caseId != "P12A-OrdinaryCadence" && caseId != "P12B-FormalReadAdapters" && caseId != "P12C-StartupSubscriptions" && caseId != "P14-FormalRealtime" && caseId != "P15-FormalMatchmaking" && caseId != "P15-FormalMatchState")
|
||||
{
|
||||
throw new ArgumentException("Unknown fixed validation case.");
|
||||
}
|
||||
}
|
||||
|
||||
var assembly = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "HotUpdate");
|
||||
var type = assembly.GetType("game.PlayerDataStorage.PlayerKVEditorValidationHost", true);
|
||||
var method = type.GetMethod("RunAsync", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(string) }, null);
|
||||
if (method == null)
|
||||
{
|
||||
throw new InvalidOperationException("Validation entry unavailable.");
|
||||
}
|
||||
|
||||
return await (Task<string>)method.Invoke(null, new object[] { caseId });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ce05f2f22c2492f8a3c51a82a208647
|
||||
@@ -1,74 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
// Fixed CLI helpers; no arbitrary method names or process-marker deletion.
|
||||
public static class KVGameplayLoopSetup
|
||||
{
|
||||
static string Root => Path.GetFullPath(Path.Combine(Application.dataPath,"../../CodexOutput/upserver/KVGameplayLoopTests"));
|
||||
static Type Find(string name) => AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("game.PlayerDataStorage."+name)).FirstOrDefault(t=>t!=null)
|
||||
?? throw new InvalidOperationException("Gameplay assembly not loaded.");
|
||||
static object ReadProperty(Type type,string name) => type.GetProperty(name,BindingFlags.Static|BindingFlags.NonPublic)?.GetValue(null);
|
||||
|
||||
public static async System.Threading.Tasks.Task<string> RunValidationAsync(string caseId)
|
||||
{
|
||||
string methodName;
|
||||
switch (caseId)
|
||||
{
|
||||
case "equipment-chain":
|
||||
methodName = "VerifyEquipmentForEditorAsync";
|
||||
break;
|
||||
case "fishing-consume":
|
||||
methodName = "VerifyFishingForEditorAsync";
|
||||
break;
|
||||
case "fishing-reward":
|
||||
methodName = "VerifyFishingRewardForEditorAsync";
|
||||
break;
|
||||
case "fishing-original-reward":
|
||||
methodName = "VerifyOriginalFishingRewardForEditorAsync";
|
||||
break;
|
||||
case "fishing-original-consume":
|
||||
methodName = "VerifyOriginalFishingForEditorAsync";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown fixed validation case.");
|
||||
}
|
||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)
|
||||
{
|
||||
throw new InvalidOperationException("Idle editor required.");
|
||||
}
|
||||
var method = Find("PlayerKVGameplayLoop").GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic);
|
||||
if (method == null)
|
||||
{
|
||||
throw new InvalidOperationException("Fixed equipment validation entry unavailable.");
|
||||
}
|
||||
return await (System.Threading.Tasks.Task<string>)method.Invoke(null,null);
|
||||
}
|
||||
|
||||
public static string State()
|
||||
{
|
||||
var type=Find("PlayerKVGameplayLoop");
|
||||
var history=Path.Combine(Root,"run-control");
|
||||
return JsonConvert.SerializeObject(new{project=Application.dataPath,playing=EditorApplication.isPlaying,
|
||||
enabled=ReadProperty(type,"Enabled"),ready=ReadProperty(type,"Ready"),error=ReadProperty(type,"Error"),run=ReadProperty(type,"RunPath"),
|
||||
permitExists=File.Exists(Path.Combine(Root,"permit.json")),
|
||||
processMarkers=Directory.Exists(history)?Directory.GetFiles(history,"*.claimed").Select(Path.GetFileName).OrderBy(x=>x).ToArray():Array.Empty<string>()});
|
||||
}
|
||||
|
||||
public static string InspectPFBackup(string path)
|
||||
{
|
||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)
|
||||
throw new InvalidOperationException("Idle editor required.");
|
||||
var method=Find("PlayerKVGameplayLoop").GetMethod("InspectPFBackupForEditor",BindingFlags.Static|BindingFlags.NonPublic);
|
||||
try { return (string)method.Invoke(null,new object[]{path}); }
|
||||
catch(TargetInvocationException e) { throw e.InnerException ?? e; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff2728a480374b609aaed9ea0742215e
|
||||
@@ -1,632 +0,0 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using game;
|
||||
using game.Account;
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
// Fixed B01 synthetic cases only. No global container replacement, permission UI or Play mode.
|
||||
public static class KVPromptStateValidation
|
||||
{
|
||||
private static bool running;
|
||||
private static object failedCleanup;
|
||||
private static readonly BindingFlags PrivateInstance = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
|
||||
public static async Task<string> RunAsync(string caseId)
|
||||
{
|
||||
if ((caseId != "B01" && caseId != "CP3A" && caseId != "P6D") || EditorApplication.isPlayingOrWillChangePlaymode || running || failedCleanup != null)
|
||||
{
|
||||
throw new InvalidOperationException("B01 requires an idle Editor and completed prior cleanup.");
|
||||
}
|
||||
running = true;
|
||||
var run = new FixedRun();
|
||||
var staticField = typeof(RedPointManager).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic);
|
||||
object previous = null;
|
||||
var isolated = false;
|
||||
var errors = new List<Exception>();
|
||||
string result = null;
|
||||
try
|
||||
{
|
||||
previous = staticField.GetValue(null);
|
||||
staticField.SetValue(null, new RedPointManager());
|
||||
isolated = true;
|
||||
result = caseId == "P6D" ? await run.ExecuteP6D() : caseId == "B01" ? await run.Execute() : await run.ExecuteCP3A();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await run.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedCleanup = run;
|
||||
errors.Add(ex);
|
||||
}
|
||||
try
|
||||
{
|
||||
if (isolated)
|
||||
{
|
||||
staticField.SetValue(null, previous);
|
||||
if (!ReferenceEquals(previous, staticField.GetValue(null)))
|
||||
{
|
||||
throw new InvalidOperationException("Red point instance was not restored.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedCleanup = run;
|
||||
errors.Add(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
throw new AggregateException(errors);
|
||||
}
|
||||
var report = Newtonsoft.Json.Linq.JObject.Parse(result);
|
||||
report["restored"] = true;
|
||||
report["remainingTestObjects"] = 0;
|
||||
return report.ToString(Formatting.None);
|
||||
}
|
||||
|
||||
sealed class Handler : HttpMessageHandler
|
||||
{
|
||||
readonly Func<HttpRequestMessage, HttpResponseMessage> send;
|
||||
internal Handler(Func<HttpRequestMessage, HttpResponseMessage> send) { this.send = send; }
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken t) => Task.FromResult(send(r));
|
||||
}
|
||||
static HttpResponseMessage Json(object value) => new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{ Content = new StringContent(JsonConvert.SerializeObject(value)) };
|
||||
|
||||
sealed class FixedRun
|
||||
{
|
||||
private readonly List<FixedRun> children = new List<FixedRun>();
|
||||
private readonly List<Action> restoreFaults = new List<Action>();
|
||||
readonly Uri api = new Uri("http://b01-editor.invalid/");
|
||||
readonly string cache = Path.GetFullPath(Path.Combine(Application.dataPath,
|
||||
"../../CodexOutput/upserver/B3BatchMigration/B01Tests/unity-runs", Guid.NewGuid().ToString("N")));
|
||||
readonly List<S101AccountSession> sessions = new List<S101AccountSession>();
|
||||
readonly List<GameObject> objects = new List<GameObject>();
|
||||
PlayerKVGameSessionBridge bridge;
|
||||
long epoch = 1;
|
||||
int assertions, requests;
|
||||
void Check(bool value, string label) { assertions++; if (!value) throw new InvalidOperationException("B01: " + label); }
|
||||
static object Call(object target, string method, params object[] args)
|
||||
{
|
||||
try { return target.GetType().GetMethod(method, PrivateInstance | BindingFlags.Public).Invoke(target, args); }
|
||||
catch (TargetInvocationException ex) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); throw; }
|
||||
}
|
||||
static void Field(object target, string name, object value) => target.GetType().GetField(name, PrivateInstance).SetValue(target, value);
|
||||
static T Bound<T>(PlayerKVGameSessionHandle handle) => (T)typeof(T).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(PlayerKVGameSessionHandle) }, null).Invoke(new object[] { handle });
|
||||
void Reject(Action action)
|
||||
{
|
||||
try { action(); }
|
||||
catch (InvalidOperationException) { assertions++; return; }
|
||||
throw new InvalidOperationException("B01: expected owner rejection");
|
||||
}
|
||||
async Task<PlayerKVGameSessionHandle> Open()
|
||||
{
|
||||
var session = new S101AccountSession(api, jar => new Handler(r =>
|
||||
{
|
||||
Check(r.RequestUri == new Uri(api, "account/Login"), "synthetic login only");
|
||||
jar.Add(api, new Cookie("sid", "101"));
|
||||
return Json(new { state = 0, internalId = 101, newCreate = false,
|
||||
createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-01T00:00:00Z" });
|
||||
}));
|
||||
sessions.Add(session);
|
||||
Check((await session.LoginAsync("101")).Success, "synthetic login");
|
||||
var opened = await bridge.OpenAsync(session, cache, "101", api, epoch, e => e == epoch);
|
||||
Check(opened.Success, "synthetic owner open");
|
||||
return opened.Handle;
|
||||
}
|
||||
T Panel<T>() where T : Component
|
||||
{
|
||||
var go = new GameObject("B01-" + typeof(T).Name) { hideFlags = HideFlags.HideAndDontSave };
|
||||
go.SetActive(false); objects.Add(go);
|
||||
return go.AddComponent<T>();
|
||||
}
|
||||
internal async Task<string> Execute()
|
||||
{
|
||||
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
||||
{
|
||||
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "no write/no real transport");
|
||||
requests++;
|
||||
return Json(new { data = new Dictionary<string, string>() });
|
||||
});
|
||||
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
||||
var handle = await Open();
|
||||
var settings = Bound<DefaultSettingService>(handle);
|
||||
var club = Bound<ClubService>(handle);
|
||||
Check(!settings.ShowNotice, "valid missing key");
|
||||
settings.ShowNotice = true;
|
||||
Check(settings.ShowNotice && requests == 1, "accepted local only");
|
||||
var original = settings.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(settings);
|
||||
settings.ShowNotice = true;
|
||||
Check(ReferenceEquals(original, settings.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(settings)), "same value no duplicate");
|
||||
// A fresh real ClubService retains its original initialized flag.
|
||||
Check(club.IsOpenClubRed, "fresh real club retains original flag");
|
||||
await bridge.CloseAsync();
|
||||
Reject(() => settings.ShowNotice = false);
|
||||
Reject(club.SetOpenClubRed);
|
||||
Check(club.IsOpenClubRed, "stale real club preserves flag");
|
||||
|
||||
// Notification constructor validates the current owner. Create a fresh fixed instance, then revoke it.
|
||||
epoch++;
|
||||
var nextHandle = await Open();
|
||||
var nextSettings = Bound<DefaultSettingService>(nextHandle);
|
||||
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(
|
||||
Path.Combine(Application.dataPath, "../../cfgData", name + ".json"))));
|
||||
var notifications = (LocalNotificationService)typeof(LocalNotificationService).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(DefaultSettingService), typeof(cfg.Tables) }, null).Invoke(new object[] { nextSettings, tables });
|
||||
var pendingInit = notifications.Init();
|
||||
await bridge.CloseAsync();
|
||||
try { await pendingInit; throw new InvalidOperationException("B01: stale init succeeded"); }
|
||||
catch (InvalidOperationException ex) { Check(!ex.Message.Contains("stale init succeeded"), "stale Editor permission continuation rejected"); }
|
||||
|
||||
var grade = Panel<SettingNotificationsGradePopupPanel>();
|
||||
Field(grade, "settings", nextSettings); Field(grade, "notifications", notifications);
|
||||
var popup = Panel<SettingNotificationsPopupPanel>(); Field(popup, "notifications", notifications);
|
||||
var settingPanel = Panel<SettingPopupPanel>(); Field(settingPanel, "settingService", nextSettings); Field(settingPanel, "notificationService", notifications);
|
||||
var gift = Panel<GiftNoticePanel>(); Field(gift, "notificationService", notifications);
|
||||
var social = Panel<FishingSocialPanel>(); Field(social, "_socialData", club);
|
||||
var errors = new List<string>();
|
||||
void Capture(string message, string stack, LogType type) { if (type == LogType.Error || type == LogType.Exception) errors.Add(message); }
|
||||
Application.logMessageReceived += Capture;
|
||||
try
|
||||
{
|
||||
Reject(() => Call(grade, "DestroyUI"));
|
||||
Reject(() => Call(grade, "OnBtnOk"));
|
||||
Reject(() => Call(popup, "OnBtnOk"));
|
||||
Reject(() => Call(popup, "DestroyUI"));
|
||||
Reject(() => Call(settingPanel, "OnToggleNotification", true));
|
||||
Reject(() => Call(settingPanel, "OnToggleNotification", false));
|
||||
Reject(() => Call(gift, "OnClickYES"));
|
||||
Call(social, "Start");
|
||||
Check(errors.Count == 1, "only unchanged social entry logs its owner rejection");
|
||||
Check(errors.TrueForAll(e => e.Contains("ShowNotice owner=") || e.Contains("IsOpenClubRed owner=")), "no unexpected exception");
|
||||
foreach (var go in objects) Check(go != null, "stale callback did not destroy another UI");
|
||||
}
|
||||
finally { Application.logMessageReceived -= Capture; }
|
||||
Check(requests == 2, "read-only synthetic transport");
|
||||
return JsonConvert.SerializeObject(new { success = true, assertions, requests, formalRequests = 0,
|
||||
playing = EditorApplication.isPlaying, globalContainerChanged = false, caseId = "B01", cache });
|
||||
}
|
||||
internal async Task<string> ExecuteP6D()
|
||||
{
|
||||
foreach (var value in new[] { null, "0", "1", "" })
|
||||
{
|
||||
var fixture = new FixedRun();
|
||||
children.Add(fixture);
|
||||
await fixture.HealthyPrompt(value);
|
||||
}
|
||||
foreach (var club in new[] { false, true })
|
||||
{
|
||||
foreach (var stage in new[] { "BeforeWrite", "AfterReplace" })
|
||||
{
|
||||
var fixture = new FixedRun();
|
||||
children.Add(fixture);
|
||||
await fixture.FaultPrompt(club, stage);
|
||||
}
|
||||
var revoked = new FixedRun();
|
||||
children.Add(revoked);
|
||||
await revoked.RevokedDuringPrompt(club);
|
||||
}
|
||||
var callback = new FixedRun();
|
||||
children.Add(callback);
|
||||
await callback.RevokedDuringClubCallback();
|
||||
foreach (var fixture in children)
|
||||
{
|
||||
assertions += fixture.assertions;
|
||||
requests += fixture.requests;
|
||||
}
|
||||
return JsonConvert.SerializeObject(new { success = true, caseId = "P6D", assertions,
|
||||
cases = children.Count, requests, formalRequests = 0, playing = EditorApplication.isPlaying,
|
||||
globalContainerChanged = false });
|
||||
}
|
||||
|
||||
private async Task<PlayerKVGameSessionHandle> OpenPrompt()
|
||||
{
|
||||
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
||||
{
|
||||
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "P6D synthetic Get only");
|
||||
requests++;
|
||||
return Json(new { data = new Dictionary<string, string>() });
|
||||
});
|
||||
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
||||
return await Open();
|
||||
}
|
||||
|
||||
private static PlayerKVEditResult Edit(object service)
|
||||
{
|
||||
return (PlayerKVEditResult)service.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(service);
|
||||
}
|
||||
|
||||
private static bool? Cached(DefaultSettingService settings)
|
||||
{
|
||||
return (bool?)typeof(DefaultSettingService).GetField("_showNotice", PrivateInstance).GetValue(settings);
|
||||
}
|
||||
|
||||
private sealed class RedProbe : IUIRedPoint
|
||||
{
|
||||
private readonly Action<bool> callback;
|
||||
internal RedProbe(Action<bool> callback)
|
||||
{
|
||||
this.callback = callback;
|
||||
}
|
||||
public void SetRedPointState(bool state)
|
||||
{
|
||||
callback(state);
|
||||
}
|
||||
}
|
||||
|
||||
private void ObserveRed(Action<bool> callback)
|
||||
{
|
||||
RedPointManager.Instance.RemoveRedPoint(RedPointName.Home_Club);
|
||||
RedPointManager.Instance.AddRedPoint(RedPointName.Home_Club, new RedProbe(callback));
|
||||
}
|
||||
|
||||
private static Delegate FaultDelegate<T>(Action<string> callback)
|
||||
{
|
||||
return new Action<T>(stage => callback(stage.ToString()));
|
||||
}
|
||||
|
||||
private void InjectFault(PlayerKVGameSessionHandle handle, Action<string> callback)
|
||||
{
|
||||
var owner = handle.GetType().GetProperty("Owner", PrivateInstance).GetValue(handle);
|
||||
var state = owner.GetType().GetField("state", PrivateInstance).GetValue(owner);
|
||||
var store = state.GetType().GetField("store", PrivateInstance).GetValue(state);
|
||||
var field = store.GetType().GetField("fault", PrivateInstance);
|
||||
var previous = field.GetValue(store);
|
||||
var method = typeof(FixedRun).GetMethod("FaultDelegate", BindingFlags.Static | BindingFlags.NonPublic)
|
||||
.MakeGenericMethod(field.FieldType.GetGenericArguments()[0]);
|
||||
var fault = method.Invoke(null, new object[] { callback });
|
||||
restoreFaults.Add(() => field.SetValue(store, previous));
|
||||
field.SetValue(store, fault);
|
||||
Check(ReferenceEquals(field.GetValue(store), fault), "actual cache fault installed");
|
||||
}
|
||||
|
||||
private async Task HealthyPrompt(string initial)
|
||||
{
|
||||
var handle = await OpenPrompt();
|
||||
if (initial != null)
|
||||
{
|
||||
Check(handle.Update("ShowNotice", initial).Accepted, "seed initial value");
|
||||
}
|
||||
var settings = Bound<DefaultSettingService>(handle);
|
||||
var expected = initial == "1";
|
||||
Check(settings.ShowNotice == expected, "first read follows original string comparison");
|
||||
Check(handle.Update("ShowNotice", expected ? "0" : "1").Accepted, "other consumer updates actual cache");
|
||||
Check(settings.ShowNotice == expected, "first-read nullable cache survives other consumer writes");
|
||||
var fresh = Bound<DefaultSettingService>(handle);
|
||||
var target = !expected;
|
||||
var observedWrites = 0;
|
||||
InjectFault(handle, stage =>
|
||||
{
|
||||
if (stage == "BeforeWrite")
|
||||
{
|
||||
observedWrites++;
|
||||
Check(Cached(fresh) == target, "nullable cache assigned before actual disk write");
|
||||
}
|
||||
});
|
||||
fresh.ShowNotice = target;
|
||||
Check(Edit(fresh)?.Accepted == true && observedWrites == 1, "setter before getter writes even when stored value matches");
|
||||
var receipt = Edit(fresh);
|
||||
fresh.ShowNotice = target;
|
||||
Check(ReferenceEquals(receipt, Edit(fresh)) && observedWrites == 1, "same cached value does not write");
|
||||
target = !target;
|
||||
fresh.ShowNotice = target;
|
||||
Check(observedWrites == 2 && !ReferenceEquals(receipt, Edit(fresh)), "changed cached value writes");
|
||||
RestoreFaults();
|
||||
|
||||
Check(handle.Update("IsOpenClubRed", "0").Accepted, "seed present club value");
|
||||
var club = Bound<ClubService>(handle);
|
||||
var calls = 0;
|
||||
ObserveRed(red =>
|
||||
{
|
||||
calls++;
|
||||
Check(!red && !club.IsOpenClubRed, "flag clears before red callback");
|
||||
Check(handle.ReadLocalSnapshot().Data["IsOpenClubRed"] == (calls == 1 ? "0" : "1"), "red callback precedes write");
|
||||
});
|
||||
club.SetOpenClubRed();
|
||||
Check(calls == 1 && Edit(club)?.Accepted == true && handle.ReadLocalSnapshot().Data["IsOpenClubRed"] == "1", "present key does not replace original flag gate");
|
||||
var clubReceipt = Edit(club);
|
||||
club.SetOpenClubRed();
|
||||
Check(calls == 1 && ReferenceEquals(clubReceipt, Edit(club)), "false flag skips red and write");
|
||||
club.IsOpenClubRed = true;
|
||||
club.SetOpenClubRed();
|
||||
Check(calls == 2 && !ReferenceEquals(clubReceipt, Edit(club)), "true flag writes existing value");
|
||||
|
||||
// A diagnostic result is not a second owner state. Do not unlock a real failed owner.
|
||||
var rejected = handle.Update(null, "1");
|
||||
Check(!rejected.Accepted, "actual rejected diagnostic");
|
||||
fresh.GetType().GetProperty("LastPromptEdit", PrivateInstance).SetValue(fresh, rejected);
|
||||
club.GetType().GetProperty("LastPromptEdit", PrivateInstance).SetValue(club, rejected);
|
||||
Check(fresh.ShowNotice == target, "healthy owner ignores old rejected diagnostic");
|
||||
club.SetOpenClubRed();
|
||||
Check(calls == 2, "healthy false club ignores diagnostic without business effects");
|
||||
await bridge.CloseAsync();
|
||||
Reject(() =>
|
||||
{
|
||||
_ = fresh.ShowNotice;
|
||||
});
|
||||
Reject(() => fresh.ShowNotice = target);
|
||||
Reject(club.SetOpenClubRed);
|
||||
Check(calls == 2 && Cached(fresh) == target && !club.IsOpenClubRed, "stale cache-hit entries cause no effects");
|
||||
}
|
||||
|
||||
private async Task FaultPrompt(bool useClub, string failureStage)
|
||||
{
|
||||
var handle = await OpenPrompt();
|
||||
var settings = Bound<DefaultSettingService>(handle);
|
||||
var club = Bound<ClubService>(handle);
|
||||
var redCalls = 0;
|
||||
ObserveRed(red =>
|
||||
{
|
||||
redCalls++;
|
||||
Check(!red && !club.IsOpenClubRed, "failed write still follows original red order");
|
||||
});
|
||||
var faults = 0;
|
||||
InjectFault(handle, stage =>
|
||||
{
|
||||
if (stage == failureStage)
|
||||
{
|
||||
faults++;
|
||||
Check(useClub ? !club.IsOpenClubRed && redCalls == 1 : Cached(settings) == true,
|
||||
"business state precedes actual storage failure");
|
||||
throw new IOException("P6D synthetic " + failureStage);
|
||||
}
|
||||
});
|
||||
Reject(() =>
|
||||
{
|
||||
if (useClub)
|
||||
{
|
||||
club.SetOpenClubRed();
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.ShowNotice = true;
|
||||
}
|
||||
});
|
||||
var edit = Edit(useClub ? (object)club : settings);
|
||||
Check(faults == 1 && edit != null && !edit.Accepted, "actual rejected write preserved");
|
||||
Check(edit.StateResult.Error == PlayerKVChangeError.StorageError &&
|
||||
edit.StateResult.LocalCommitMayHaveSucceeded == (failureStage == "AfterReplace"), "actual uncertainty preserved");
|
||||
Check(handle.Status == PlayerKVSyncStatus.StorageError, "actual storage terminal state retained");
|
||||
Reject(() =>
|
||||
{
|
||||
_ = settings.ShowNotice;
|
||||
});
|
||||
Reject(() => settings.ShowNotice = true);
|
||||
Reject(club.SetOpenClubRed);
|
||||
Check(faults == 1 && redCalls == (useClub ? 1 : 0), "actual unreadable owner blocks repeat effects");
|
||||
Check(useClub ? !club.IsOpenClubRed : Cached(settings) == true, "no added rollback");
|
||||
}
|
||||
|
||||
private async Task RevokedDuringPrompt(bool useClub)
|
||||
{
|
||||
var handle = await OpenPrompt();
|
||||
var settings = Bound<DefaultSettingService>(handle);
|
||||
var club = Bound<ClubService>(handle);
|
||||
var redCalls = 0;
|
||||
ObserveRed(red =>
|
||||
{
|
||||
redCalls++;
|
||||
});
|
||||
InjectFault(handle, stage =>
|
||||
{
|
||||
if (stage == "AfterReplace")
|
||||
{
|
||||
epoch++;
|
||||
}
|
||||
});
|
||||
Reject(() =>
|
||||
{
|
||||
if (useClub)
|
||||
{
|
||||
club.SetOpenClubRed();
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.ShowNotice = true;
|
||||
}
|
||||
});
|
||||
Check(Edit(useClub ? (object)club : settings)?.Accepted == true, "completed disk write not disguised as unaccepted");
|
||||
Check(useClub ? !club.IsOpenClubRed && redCalls == 1 : Cached(settings) == true, "revocation does not roll back original front effects");
|
||||
Check(!handle.ReadLocalSnapshot().Success && requests == 1, "revoked old owner has no replay or transport write");
|
||||
}
|
||||
|
||||
private async Task RevokedDuringClubCallback()
|
||||
{
|
||||
var handle = await OpenPrompt();
|
||||
var club = Bound<ClubService>(handle);
|
||||
var redCalls = 0;
|
||||
ObserveRed(red =>
|
||||
{
|
||||
redCalls++;
|
||||
epoch++;
|
||||
});
|
||||
var writes = 0;
|
||||
InjectFault(handle, stage =>
|
||||
{
|
||||
writes++;
|
||||
});
|
||||
Reject(club.SetOpenClubRed);
|
||||
Check(!club.IsOpenClubRed && redCalls == 1, "callback revocation preserves front effects");
|
||||
Check(Edit(club)?.Accepted == false && writes == 0 && requests == 1, "callback revocation refuses old write without replay");
|
||||
}
|
||||
internal async Task<string> ExecuteCP3A()
|
||||
{
|
||||
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
||||
{
|
||||
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "CP3A synthetic Get only");
|
||||
requests++;
|
||||
return Json(new { data = new Dictionary<string, string>() });
|
||||
});
|
||||
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
||||
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(
|
||||
Path.Combine(Application.dataPath, "../../cfgData", name + ".json"))));
|
||||
var settingCtor = typeof(DefaultSettingService).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(bool) }, null);
|
||||
var clubCtor = typeof(ClubService).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(bool) }, null);
|
||||
var notificationCtor = typeof(LocalNotificationService).GetConstructor(PrivateInstance,
|
||||
null, new[] { typeof(cfg.Tables) }, null);
|
||||
Check(settingCtor != null && clubCtor != null && notificationCtor != null,
|
||||
"formal service constructors exist");
|
||||
var settings = (DefaultSettingService)settingCtor.Invoke(new object[] { true });
|
||||
var club = (ClubService)clubCtor.Invoke(new object[] { true });
|
||||
var notifications = (LocalNotificationService)notificationCtor.Invoke(new object[] { tables });
|
||||
var pendingNotifications = (LocalNotificationService)notificationCtor.Invoke(new object[] { tables });
|
||||
var pendingInit = pendingNotifications.Init();
|
||||
Reject(() => Call(pendingNotifications, "PreflightFormalPromptSettings", settings));
|
||||
await pendingInit;
|
||||
await notifications.Init();
|
||||
Reject(() => { _ = settings.ShowNotice; });
|
||||
Reject(() => settings.ShowNotice = true);
|
||||
Reject(club.SetOpenClubRed);
|
||||
var handle = await Open();
|
||||
Call(settings, "BindFormalPromptOwner", handle);
|
||||
Call(club, "BindFormalPromptOwner", handle);
|
||||
Call(notifications, "BindFormalPromptSettings", settings);
|
||||
Call(settings, "BindFormalPromptOwner", handle);
|
||||
Call(club, "BindFormalPromptOwner", handle);
|
||||
Call(notifications, "BindFormalPromptSettings", settings);
|
||||
Check(!settings.ShowNotice, "bound missing ShowNotice");
|
||||
settings.ShowNotice = true;
|
||||
club.SetOpenClubRed();
|
||||
Check(settings.ShowNotice && !club.IsOpenClubRed, "bound prompt effects");
|
||||
Check(requests == 1, "one synthetic KV Get and no transport write");
|
||||
await bridge.CloseAsync();
|
||||
Reject(() => { _ = settings.ShowNotice; });
|
||||
Reject(club.SetOpenClubRed);
|
||||
epoch++;
|
||||
var nextHandle = await Open();
|
||||
Reject(() => Call(settings, "BindFormalPromptOwner", nextHandle));
|
||||
Reject(() => Call(club, "BindFormalPromptOwner", nextHandle));
|
||||
var partialSetting = (DefaultSettingService)settingCtor.Invoke(new object[] { true });
|
||||
var partialClub = (ClubService)clubCtor.Invoke(new object[] { true });
|
||||
Call(partialSetting, "BindFormalPromptOwner", nextHandle);
|
||||
await bridge.CloseAsync();
|
||||
Reject(() => Call(partialClub, "BindFormalPromptOwner", nextHandle));
|
||||
Reject(() => { _ = partialSetting.ShowNotice; });
|
||||
epoch++;
|
||||
var thirdHandle = await Open();
|
||||
Reject(() => Call(partialSetting, "BindFormalPromptOwner", thirdHandle));
|
||||
Check(requests == 3, "same owner binds and partial failure add no transport requests");
|
||||
return JsonConvert.SerializeObject(new { success = true, caseId = "CP3A", assertions,
|
||||
requests, formalRequests = 0, globalContainerChanged = false });
|
||||
}
|
||||
internal async Task Close()
|
||||
{
|
||||
var errors = new List<Exception>();
|
||||
foreach (var child in children)
|
||||
{
|
||||
try
|
||||
{
|
||||
await child.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
RestoreFaults();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
try
|
||||
{
|
||||
if (bridge != null)
|
||||
{
|
||||
await bridge.CloseAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
try
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
}
|
||||
foreach (var go in objects)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (go != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
}
|
||||
if (go != null)
|
||||
{
|
||||
throw new InvalidOperationException("Test object survived cleanup.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
}
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
throw new AggregateException(errors);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreFaults()
|
||||
{
|
||||
var errors = new List<Exception>();
|
||||
foreach (var restore in restoreFaults)
|
||||
{
|
||||
try
|
||||
{
|
||||
restore();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
}
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
throw new AggregateException(errors);
|
||||
}
|
||||
restoreFaults.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67d5bde18c99423aa2168a5af1c2b0da
|
||||
@@ -1,74 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using game.Account;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class S101LoginWindow : EditorWindow
|
||||
{
|
||||
string playerId = "101";
|
||||
string status = "输入涂游测试 ID,验证 s101 认证及 Cookie。";
|
||||
bool busy;
|
||||
S101AccountSession session;
|
||||
CancellationTokenSource cancellation;
|
||||
|
||||
[MenuItem("Tools/Debug/S101 Minimal Login")]
|
||||
static void Open() => GetWindow<S101LoginWindow>("S101 最小登录");
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("API", S101AccountSession.DefaultApiUrl);
|
||||
using (new EditorGUI.DisabledScope(busy))
|
||||
{
|
||||
playerId = EditorGUILayout.TextField("涂游测试 ID", playerId);
|
||||
if (GUILayout.Button("登录并验证 Cookie")) Login();
|
||||
}
|
||||
EditorGUILayout.HelpBox(status, MessageType.Info);
|
||||
EditorGUILayout.LabelField("此入口只验证认证,不初始化玩法或写入存档。");
|
||||
}
|
||||
|
||||
async void Login()
|
||||
{
|
||||
busy = true;
|
||||
session?.Dispose();
|
||||
cancellation?.Dispose();
|
||||
cancellation = new CancellationTokenSource();
|
||||
var attempt = new S101AccountSession();
|
||||
session = attempt;
|
||||
var token = cancellation.Token;
|
||||
status = "正在登录…";
|
||||
try
|
||||
{
|
||||
var login = await attempt.LoginAsync(playerId, token);
|
||||
if (token.IsCancellationRequested) return;
|
||||
if (!login.Success)
|
||||
{
|
||||
status = $"登录失败:{login.Error},HTTP {login.StatusCode}";
|
||||
return;
|
||||
}
|
||||
var probe = await attempt.CheckAuthorizationAsync(token);
|
||||
if (token.IsCancellationRequested) return;
|
||||
status = probe.Success
|
||||
? $"认证通过:涂游 ID {attempt.PlayerId},内部 ID {attempt.InternalId};Cookie 授权读取通过。"
|
||||
: $"登录成功,但授权读取未通过:{probe.Error},HTTP {probe.StatusCode}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status = "验证异常:" + ex.GetType().Name;
|
||||
}
|
||||
finally
|
||||
{
|
||||
busy = false;
|
||||
if (this != null) Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
cancellation?.Cancel();
|
||||
session?.Dispose();
|
||||
cancellation?.Dispose();
|
||||
cancellation = null;
|
||||
session = null;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8f49f7440c141e89d8519ff3aa1c034
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user